1pub mod aiger_obligation;
8pub mod btor2;
9pub mod btor2_bitblast;
10pub mod btor2_bounded;
11pub mod btor2_braking;
12pub mod btor2_component;
13pub mod btor2_family;
14pub mod btor2_family_orbit;
15pub mod btor2_family_proof;
16pub mod btor2_invariant_chain;
17pub mod btor2_motion;
18pub mod btor2_phase;
19pub mod btor2_predicate_set;
20pub mod btor2_region;
21pub mod btor2_region_equivalence;
22pub mod btor2_region_extract;
23pub mod btor2_region_property;
24pub mod btor2_search;
25pub mod compiled_mmio_branching_dag;
26pub mod compiled_mmio_certificate;
27pub mod compiled_mmio_decode_graph;
28pub mod compiled_mmio_explicit_transcript;
29pub mod compiled_mmio_file;
30pub mod compiled_mmio_predicate_certificate;
31pub mod compiled_mmio_predicate_portfolio;
32pub mod compiled_mmio_quotient;
33pub mod compiled_mmio_rtl_certificate;
34pub mod compiled_mmio_rtl_mapping;
35pub mod composed_witness;
36pub mod controller_mtbdd;
37pub mod controller_mtbdd_proof;
38pub mod controller_plant;
39pub mod controller_plant_aiger;
40pub mod controller_plant_artifact;
41pub mod controller_transducer;
42pub mod dense_relation;
43pub mod firmware_transaction_contract;
44#[cfg(feature = "research-qatq-transport")]
45pub mod qatq_transport;
46pub mod revision_batch;
47pub mod revision_impact;
48pub mod revision_local;
49pub mod riscv32imc;
50pub mod riscv32imc_predicate;
51pub mod riscv32imc_predicate_checker;
52pub mod source_model_attestation;
53pub mod unsat_proof;
54
55use std::collections::BTreeMap;
56use std::fmt;
57use std::io::Read;
58#[cfg(unix)]
59use std::os::unix::process::CommandExt;
60use std::path::{Path, PathBuf};
61use std::process::{Command, ExitStatus, Stdio};
62use std::thread;
63use std::time::{Duration, Instant};
64
65pub const PREDICATE_API_VERSION: u32 = 1;
67pub const EVENT_CONTRACT_API_VERSION: u32 = 1;
68pub const DEFAULT_EXECUTION_TIMEOUT: Duration = Duration::from_secs(300);
69pub const DEFAULT_OUTPUT_LIMIT_BYTES: usize = 1024 * 1024;
70pub const DEFAULT_FILE_LIMIT_BYTES: u64 = 32 * 1024 * 1024;
71#[cfg(all(unix, not(target_os = "macos")))]
72pub const DEFAULT_MEMORY_LIMIT_BYTES: Option<u64> = Some(2 * 1024 * 1024 * 1024);
73#[cfg(any(not(unix), target_os = "macos"))]
74pub const DEFAULT_MEMORY_LIMIT_BYTES: Option<u64> = None;
75const MAX_OUTPUT_LIMIT_BYTES: usize = 64 * 1024 * 1024;
76const MIN_MEMORY_LIMIT_BYTES: u64 = 64 * 1024 * 1024;
77const MAX_MEMORY_LIMIT_BYTES: u64 = 1024 * 1024 * 1024 * 1024;
78const MAX_FILE_LIMIT_BYTES: u64 = 1024 * 1024 * 1024;
79
80#[derive(Clone, Copy, Debug, Eq, PartialEq)]
82pub struct ExecutionPolicy {
83 timeout: Duration,
84 output_limit_bytes: usize,
85 memory_limit_bytes: Option<u64>,
86 file_limit_bytes: u64,
87}
88
89impl ExecutionPolicy {
90 pub fn new(timeout: Duration, output_limit_bytes: usize) -> Result<Self, PredicateApiError> {
91 if timeout.is_zero() {
92 return Err(PredicateApiError::InvalidPolicy(
93 "timeout must be greater than zero".to_string(),
94 ));
95 }
96 if !(1..=MAX_OUTPUT_LIMIT_BYTES).contains(&output_limit_bytes) {
97 return Err(PredicateApiError::InvalidPolicy(format!(
98 "output limit must be in 1..={MAX_OUTPUT_LIMIT_BYTES} bytes"
99 )));
100 }
101 Ok(Self {
102 timeout,
103 output_limit_bytes,
104 memory_limit_bytes: DEFAULT_MEMORY_LIMIT_BYTES,
105 file_limit_bytes: DEFAULT_FILE_LIMIT_BYTES,
106 })
107 }
108
109 pub fn timeout(self) -> Duration {
110 self.timeout
111 }
112
113 pub fn output_limit_bytes(self) -> usize {
114 self.output_limit_bytes
115 }
116
117 pub fn memory_limit_bytes(self) -> Option<u64> {
118 self.memory_limit_bytes
119 }
120
121 pub fn file_limit_bytes(self) -> u64 {
122 self.file_limit_bytes
123 }
124
125 #[cfg(all(unix, not(target_os = "macos")))]
126 pub fn with_memory_limit(mut self, bytes: u64) -> Result<Self, PredicateApiError> {
127 if !(MIN_MEMORY_LIMIT_BYTES..=MAX_MEMORY_LIMIT_BYTES).contains(&bytes) {
128 return Err(PredicateApiError::InvalidPolicy(format!(
129 "memory limit must be in {MIN_MEMORY_LIMIT_BYTES}..={MAX_MEMORY_LIMIT_BYTES} bytes"
130 )));
131 }
132 self.memory_limit_bytes = Some(bytes);
133 Ok(self)
134 }
135
136 #[cfg(any(not(unix), target_os = "macos"))]
137 pub fn with_memory_limit(self, bytes: u64) -> Result<Self, PredicateApiError> {
138 if !(MIN_MEMORY_LIMIT_BYTES..=MAX_MEMORY_LIMIT_BYTES).contains(&bytes) {
139 return Err(PredicateApiError::InvalidPolicy(format!(
140 "memory limit must be in {MIN_MEMORY_LIMIT_BYTES}..={MAX_MEMORY_LIMIT_BYTES} bytes"
141 )));
142 }
143 Err(PredicateApiError::InvalidPolicy(
144 "address-space limits are unavailable on this platform".to_string(),
145 ))
146 }
147
148 pub fn with_file_limit(mut self, bytes: u64) -> Result<Self, PredicateApiError> {
149 if !(1..=MAX_FILE_LIMIT_BYTES).contains(&bytes) {
150 return Err(PredicateApiError::InvalidPolicy(format!(
151 "file limit must be in 1..={MAX_FILE_LIMIT_BYTES} bytes"
152 )));
153 }
154 self.file_limit_bytes = bytes;
155 Ok(self)
156 }
157}
158
159impl Default for ExecutionPolicy {
160 fn default() -> Self {
161 Self {
162 timeout: DEFAULT_EXECUTION_TIMEOUT,
163 output_limit_bytes: DEFAULT_OUTPUT_LIMIT_BYTES,
164 memory_limit_bytes: DEFAULT_MEMORY_LIMIT_BYTES,
165 file_limit_bytes: DEFAULT_FILE_LIMIT_BYTES,
166 }
167 }
168}
169
170pub const INVOCATION_METRICS_SCHEMA_VERSION: u32 = 1;
171
172#[derive(Clone, Copy, Debug, Eq, PartialEq)]
173pub enum OperationKind {
174 Discover,
175 DiscoverEventContract,
176 CertifyV1,
177 CertifyV2,
178 CertifyEventContractV3,
179 VerifyV1,
180 VerifyV2,
181 VerifyEventContractV3,
182 EventContractPortfolio,
183 VerifyEventContractPortfolioReport,
184 DiscoverControllerMtbdd,
185 CertifyControllerMtbddPlantBatch,
186 VerifyControllerMtbddPlantBatch,
187 DiscoverControllerProofMtbdd,
188 CertifyControllerProofMtbddPlantBatch,
189 VerifyControllerProofMtbddPlantBatch,
190 DiscoverControllerPlantPortfolio,
191 CertifyControllerPlantPortfolio,
192 VerifyControllerPlantPortfolio,
193 DiscoverControllerPlantResource,
194 VerifyControllerPlantPortfolioResources,
195 DiscoverControllerProofMtbddResource,
196 VerifyControllerProofMtbddPlantResources,
197 DiscoverControllerProofMtbddPortfolio,
198 VerifyControllerProofMtbddPortfolioResources,
199 VerifyControllerProofMtbddPortfolioResourcesAttested,
200 DiscoverControllerSplitEvidence,
201 CertifyControllerProofEvidence,
202 CertifyBoundPlantResults,
203 VerifyBoundPlantResultSet,
204 DiscoverControllerSplitResource,
205 VerifyBoundPlantResultSetResources,
206 DiscoverControllerSplitObservability,
207 VerifyBoundPlantResultSetResourcesObserved,
208 DiscoverControllerSplitAllocationObservability,
209 VerifyBoundPlantResultSetResourcesAllocationObserved,
210 DiscoverControllerSplitCacheObservability,
211 VerifyBoundPlantResultSetResourcesCacheObserved,
212 DiscoverRevisionImpact,
213 CertifyRevisionImpact,
214 VerifyRevisionImpact,
215 DiscoverBtor2ChannelProperty,
216 CertifyBtor2ChannelProperty,
217 VerifyBtor2ChannelProperty,
218 DiscoverBtor2ChannelPropertyObservability,
219 CertifyBtor2ChannelPropertyObserved,
220 VerifyBtor2ChannelPropertyObserved,
221}
222
223impl OperationKind {
224 pub fn as_str(self) -> &'static str {
225 match self {
226 Self::Discover => "discover",
227 Self::DiscoverEventContract => "discover_event_contract",
228 Self::CertifyV1 => "certify_v1",
229 Self::CertifyV2 => "certify_v2",
230 Self::CertifyEventContractV3 => "certify_event_contract_v3",
231 Self::VerifyV1 => "verify_v1",
232 Self::VerifyV2 => "verify_v2",
233 Self::VerifyEventContractV3 => "verify_event_contract_v3",
234 Self::EventContractPortfolio => "event_contract_portfolio",
235 Self::VerifyEventContractPortfolioReport => "verify_event_contract_portfolio_report",
236 Self::DiscoverControllerMtbdd => "discover_controller_mtbdd",
237 Self::CertifyControllerMtbddPlantBatch => "certify_controller_mtbdd_plant_batch",
238 Self::VerifyControllerMtbddPlantBatch => "verify_controller_mtbdd_plant_batch",
239 Self::DiscoverControllerProofMtbdd => "discover_controller_proof_mtbdd",
240 Self::CertifyControllerProofMtbddPlantBatch => {
241 "certify_controller_proof_mtbdd_plant_batch"
242 }
243 Self::VerifyControllerProofMtbddPlantBatch => {
244 "verify_controller_proof_mtbdd_plant_batch"
245 }
246 Self::DiscoverControllerPlantPortfolio => "discover_controller_plant_portfolio",
247 Self::CertifyControllerPlantPortfolio => "certify_controller_plant_portfolio",
248 Self::VerifyControllerPlantPortfolio => "verify_controller_plant_portfolio",
249 Self::DiscoverControllerPlantResource => "discover_controller_plant_resource",
250 Self::VerifyControllerPlantPortfolioResources => {
251 "verify_controller_plant_portfolio_resources"
252 }
253 Self::DiscoverControllerProofMtbddResource => {
254 "discover_controller_proof_mtbdd_resource"
255 }
256 Self::VerifyControllerProofMtbddPlantResources => {
257 "verify_controller_proof_mtbdd_plant_resources"
258 }
259 Self::DiscoverControllerProofMtbddPortfolio => {
260 "discover_controller_proof_mtbdd_portfolio"
261 }
262 Self::VerifyControllerProofMtbddPortfolioResources => {
263 "verify_controller_proof_mtbdd_portfolio_resources"
264 }
265 Self::VerifyControllerProofMtbddPortfolioResourcesAttested => {
266 "verify_controller_proof_mtbdd_portfolio_resources_attested"
267 }
268 Self::DiscoverControllerSplitEvidence => "discover_controller_split_evidence",
269 Self::CertifyControllerProofEvidence => "certify_controller_proof_evidence",
270 Self::CertifyBoundPlantResults => "certify_bound_plant_results",
271 Self::VerifyBoundPlantResultSet => "verify_bound_plant_result_set",
272 Self::DiscoverControllerSplitResource => "discover_controller_split_resource",
273 Self::VerifyBoundPlantResultSetResources => "verify_bound_plant_result_set_resources",
274 Self::DiscoverControllerSplitObservability => "discover_controller_split_observability",
275 Self::VerifyBoundPlantResultSetResourcesObserved => {
276 "verify_bound_plant_result_set_resources_observed"
277 }
278 Self::DiscoverControllerSplitAllocationObservability => {
279 "discover_controller_split_allocation_observability"
280 }
281 Self::VerifyBoundPlantResultSetResourcesAllocationObserved => {
282 "verify_bound_plant_result_set_resources_allocation_observed"
283 }
284 Self::DiscoverControllerSplitCacheObservability => {
285 "discover_controller_split_cache_observability"
286 }
287 Self::VerifyBoundPlantResultSetResourcesCacheObserved => {
288 "verify_bound_plant_result_set_resources_cache_observed"
289 }
290 Self::DiscoverRevisionImpact => "discover_revision_impact",
291 Self::CertifyRevisionImpact => "certify_revision_impact",
292 Self::VerifyRevisionImpact => "verify_revision_impact",
293 Self::DiscoverBtor2ChannelProperty => "discover_btor2_channel_property",
294 Self::CertifyBtor2ChannelProperty => "certify_btor2_channel_property",
295 Self::VerifyBtor2ChannelProperty => "verify_btor2_channel_property",
296 Self::DiscoverBtor2ChannelPropertyObservability => {
297 "discover_btor2_channel_property_observability"
298 }
299 Self::CertifyBtor2ChannelPropertyObserved => "certify_btor2_channel_property_observed",
300 Self::VerifyBtor2ChannelPropertyObserved => "verify_btor2_channel_property_observed",
301 }
302 }
303}
304
305#[derive(Clone, Copy, Debug, Eq, PartialEq)]
306pub enum FailureClass {
307 Policy,
308 Io,
309 Timeout,
310 OutputLimit,
311 ExitStatus,
312 ResourceRefusal,
313 Compatibility,
314 Response,
315}
316
317impl FailureClass {
318 pub fn as_str(self) -> &'static str {
319 match self {
320 Self::Policy => "policy",
321 Self::Io => "io",
322 Self::Timeout => "timeout",
323 Self::OutputLimit => "output_limit",
324 Self::ExitStatus => "exit_status",
325 Self::ResourceRefusal => "resource_refusal",
326 Self::Compatibility => "compatibility",
327 Self::Response => "response",
328 }
329 }
330}
331
332#[derive(Clone, Copy, Debug, Eq, PartialEq)]
333pub enum InvocationStatus {
334 Success,
335 Failed(FailureClass),
336}
337
338#[derive(Clone, Debug, Eq, PartialEq)]
339pub struct InvocationMetrics {
340 pub schema_version: u32,
341 pub operation: OperationKind,
342 pub duration: Duration,
343 pub stdout_bytes: usize,
344 pub stderr_bytes: usize,
345 pub timeout: Duration,
346 pub output_limit_bytes: usize,
347 pub memory_limit_bytes: Option<u64>,
348 pub file_limit_bytes: u64,
349 pub process_group_containment: bool,
350 pub exit_code: Option<i32>,
351 pub status: InvocationStatus,
352}
353
354impl InvocationMetrics {
355 pub const fn csv_header() -> &'static str {
356 "schema_version,operation,duration_ns,stdout_bytes,stderr_bytes,timeout_ms,output_limit_bytes,exit_code,status,failure_class"
357 }
358
359 pub fn to_csv_row(&self) -> String {
360 let (status, failure_class) = match self.status {
361 InvocationStatus::Success => ("ok", "-"),
362 InvocationStatus::Failed(class) => ("error", class.as_str()),
363 };
364 format!(
365 "{},{},{},{},{},{},{},{},{},{}",
366 self.schema_version,
367 self.operation.as_str(),
368 self.duration.as_nanos(),
369 self.stdout_bytes,
370 self.stderr_bytes,
371 self.timeout.as_millis(),
372 self.output_limit_bytes,
373 self.exit_code
374 .map(|code| code.to_string())
375 .unwrap_or_else(|| "-".to_string()),
376 status,
377 failure_class,
378 )
379 }
380}
381
382pub const INVOCATION_METRICS_AGGREGATE_SCHEMA_VERSION: u32 = 1;
383pub const MAX_AGGREGATED_INVOCATIONS: usize = 1_000_000;
384
385#[derive(Clone, Debug, Eq, PartialEq)]
387#[non_exhaustive]
388pub struct InvocationMetricsAggregate {
389 pub schema_version: u32,
390 pub jobs: u64,
391 pub successes: u64,
392 pub failures: u64,
393 pub total_duration_ns: u128,
394 pub maximum_duration_ns: u128,
395 pub total_stdout_bytes: u128,
396 pub total_stderr_bytes: u128,
397 pub process_group_contained_jobs: u64,
398 pub memory_limited_jobs: u64,
399 pub operation_counts: BTreeMap<&'static str, u64>,
400 pub failure_counts: BTreeMap<&'static str, u64>,
401}
402
403impl InvocationMetricsAggregate {
404 pub const fn csv_header() -> &'static str {
405 "schema_version,jobs,successes,failures,total_duration_ns,maximum_duration_ns,total_stdout_bytes,total_stderr_bytes,process_group_contained_jobs,memory_limited_jobs,operation_counts,failure_counts"
406 }
407
408 pub fn to_csv_row(&self) -> String {
409 let counts = |values: &BTreeMap<&'static str, u64>| {
410 if values.is_empty() {
411 "none".to_string()
412 } else {
413 values
414 .iter()
415 .map(|(name, count)| format!("{name}={count}"))
416 .collect::<Vec<_>>()
417 .join(";")
418 }
419 };
420 format!(
421 "{},{},{},{},{},{},{},{},{},{},{},{}",
422 self.schema_version,
423 self.jobs,
424 self.successes,
425 self.failures,
426 self.total_duration_ns,
427 self.maximum_duration_ns,
428 self.total_stdout_bytes,
429 self.total_stderr_bytes,
430 self.process_group_contained_jobs,
431 self.memory_limited_jobs,
432 counts(&self.operation_counts),
433 counts(&self.failure_counts),
434 )
435 }
436}
437
438#[derive(Clone, Copy, Debug, Eq, PartialEq)]
439#[non_exhaustive]
440pub enum MetricsAggregationError {
441 Empty,
442 TooManyJobs,
443 UnsupportedSchema(u32),
444 Overflow,
445}
446
447impl fmt::Display for MetricsAggregationError {
448 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
449 match self {
450 Self::Empty => write!(formatter, "invocation metrics set is empty"),
451 Self::TooManyJobs => write!(
452 formatter,
453 "invocation metrics set exceeds {MAX_AGGREGATED_INVOCATIONS} jobs"
454 ),
455 Self::UnsupportedSchema(version) => {
456 write!(formatter, "unsupported invocation metrics schema {version}")
457 }
458 Self::Overflow => write!(formatter, "invocation metrics aggregate overflows"),
459 }
460 }
461}
462
463impl std::error::Error for MetricsAggregationError {}
464
465pub fn aggregate_invocation_metrics<'a>(
467 metrics: impl IntoIterator<Item = &'a InvocationMetrics>,
468) -> Result<InvocationMetricsAggregate, MetricsAggregationError> {
469 let mut aggregate = InvocationMetricsAggregate {
470 schema_version: INVOCATION_METRICS_AGGREGATE_SCHEMA_VERSION,
471 jobs: 0,
472 successes: 0,
473 failures: 0,
474 total_duration_ns: 0,
475 maximum_duration_ns: 0,
476 total_stdout_bytes: 0,
477 total_stderr_bytes: 0,
478 process_group_contained_jobs: 0,
479 memory_limited_jobs: 0,
480 operation_counts: BTreeMap::new(),
481 failure_counts: BTreeMap::new(),
482 };
483 for metric in metrics {
484 if metric.schema_version != INVOCATION_METRICS_SCHEMA_VERSION {
485 return Err(MetricsAggregationError::UnsupportedSchema(
486 metric.schema_version,
487 ));
488 }
489 if aggregate.jobs as usize == MAX_AGGREGATED_INVOCATIONS {
490 return Err(MetricsAggregationError::TooManyJobs);
491 }
492 aggregate.jobs = aggregate
493 .jobs
494 .checked_add(1)
495 .ok_or(MetricsAggregationError::Overflow)?;
496 let duration = metric.duration.as_nanos();
497 aggregate.total_duration_ns = aggregate
498 .total_duration_ns
499 .checked_add(duration)
500 .ok_or(MetricsAggregationError::Overflow)?;
501 aggregate.maximum_duration_ns = aggregate.maximum_duration_ns.max(duration);
502 aggregate.total_stdout_bytes = aggregate
503 .total_stdout_bytes
504 .checked_add(metric.stdout_bytes as u128)
505 .ok_or(MetricsAggregationError::Overflow)?;
506 aggregate.total_stderr_bytes = aggregate
507 .total_stderr_bytes
508 .checked_add(metric.stderr_bytes as u128)
509 .ok_or(MetricsAggregationError::Overflow)?;
510 if metric.process_group_containment {
511 aggregate.process_group_contained_jobs = aggregate
512 .process_group_contained_jobs
513 .checked_add(1)
514 .ok_or(MetricsAggregationError::Overflow)?;
515 }
516 if metric.memory_limit_bytes.is_some() {
517 aggregate.memory_limited_jobs = aggregate
518 .memory_limited_jobs
519 .checked_add(1)
520 .ok_or(MetricsAggregationError::Overflow)?;
521 }
522 let operation_count = aggregate
523 .operation_counts
524 .entry(metric.operation.as_str())
525 .or_default();
526 *operation_count = operation_count
527 .checked_add(1)
528 .ok_or(MetricsAggregationError::Overflow)?;
529 match metric.status {
530 InvocationStatus::Success => {
531 aggregate.successes = aggregate
532 .successes
533 .checked_add(1)
534 .ok_or(MetricsAggregationError::Overflow)?;
535 }
536 InvocationStatus::Failed(class) => {
537 aggregate.failures = aggregate
538 .failures
539 .checked_add(1)
540 .ok_or(MetricsAggregationError::Overflow)?;
541 let failure_count = aggregate.failure_counts.entry(class.as_str()).or_default();
542 *failure_count = failure_count
543 .checked_add(1)
544 .ok_or(MetricsAggregationError::Overflow)?;
545 }
546 }
547 }
548 if aggregate.jobs == 0 {
549 return Err(MetricsAggregationError::Empty);
550 }
551 if aggregate.successes.checked_add(aggregate.failures) != Some(aggregate.jobs) {
552 return Err(MetricsAggregationError::Overflow);
553 }
554 Ok(aggregate)
555}
556
557#[derive(Clone, Debug, Eq, PartialEq)]
558pub struct Observed<T> {
559 pub value: T,
560 pub metrics: InvocationMetrics,
561}
562
563#[derive(Clone, Copy, Debug, Eq, PartialEq)]
565pub enum CertificateVersion {
566 V1,
567 V2,
568}
569
570impl CertificateVersion {
571 fn producer_command(self) -> &'static str {
572 match self {
573 Self::V1 => "certify-aiger-predicate",
574 Self::V2 => "certify-aiger-predicate-v2",
575 }
576 }
577
578 fn verifier_command(self) -> &'static str {
579 match self {
580 Self::V1 => "verify-aiger-predicate-certificate",
581 Self::V2 => "verify-aiger-predicate-certificate-v2",
582 }
583 }
584
585 fn certify_operation(self) -> OperationKind {
586 match self {
587 Self::V1 => OperationKind::CertifyV1,
588 Self::V2 => OperationKind::CertifyV2,
589 }
590 }
591
592 fn verify_operation(self) -> OperationKind {
593 match self {
594 Self::V1 => OperationKind::VerifyV1,
595 Self::V2 => OperationKind::VerifyV2,
596 }
597 }
598}
599
600#[derive(Clone, Copy, Debug, Eq, PartialEq)]
602pub enum PredicateResult {
603 Avoidable,
604 Unavoidable,
605}
606
607#[derive(Clone, Debug, Eq, PartialEq)]
609pub struct PredicateCapabilities {
610 pub cli_version: u32,
611 pub certificate_versions: Vec<CertificateVersion>,
612 pub portfolio_certificate_version: CertificateVersion,
613 pub proof_format: String,
614 pub min_relevant_inputs: usize,
615 pub max_relevant_inputs: usize,
616 pub max_latches: usize,
617 pub max_horizon: usize,
618 pub max_certificate_v2_bytes: u64,
619 pub max_proof_bytes: usize,
620 pub max_total_proof_bytes: usize,
621}
622
623#[derive(Clone, Debug, Eq, PartialEq)]
625pub struct EventContractCapabilities {
626 pub cli_version: u32,
627 pub certificate_version: u32,
628 pub portfolio_version: u32,
629 pub semantics: String,
630 pub proof_format: String,
631 pub min_relevant_inputs: usize,
632 pub max_relevant_inputs: usize,
633 pub max_latches: usize,
634 pub max_horizon: usize,
635 pub max_contract_bytes: u64,
636 pub max_certificate_bytes: u64,
637 pub max_proof_bytes: usize,
638 pub max_total_proof_bytes: usize,
639}
640
641#[derive(Clone, Debug, Eq, PartialEq)]
643pub struct ControllerMtbddCapabilities {
644 pub cli_version: u32,
645 pub mtbdd_version: u32,
646 pub plant_artifact_version: u32,
647 pub manifest_version: u32,
648 pub max_manifest_bytes: usize,
649 pub max_artifact_bytes: usize,
650 pub max_members: usize,
651 pub max_state_bits: usize,
652 pub max_inputs: usize,
653 pub max_outputs: usize,
654 pub max_nodes: usize,
655 pub max_terminals: usize,
656 pub max_assignments: usize,
657 pub max_horizon: usize,
658}
659
660#[derive(Clone, Debug, Eq, PartialEq)]
662pub struct RevisionImpactCapabilities {
663 pub cli_version: u32,
664 pub impact_version: u32,
665 pub query_manifest_version: u32,
666 pub max_query_manifest_bytes: usize,
667 pub max_input_bytes: usize,
668 pub max_evidence_bytes: usize,
669 pub max_bundle_bytes: usize,
670 pub max_atoms: usize,
671 pub max_combinations: usize,
672 pub max_queries: usize,
673}
674
675#[derive(Clone, Debug)]
677pub struct RevisionImpactFiles<'a> {
678 pub left_old: &'a Path,
679 pub left_new: &'a Path,
680 pub left_outputs: &'a [u64],
681 pub right_old: &'a Path,
682 pub right_new: &'a Path,
683 pub right_outputs: &'a [u64],
684 pub interface_old: &'a Path,
685 pub interface_new: &'a Path,
686 pub queries: &'a Path,
687}
688
689#[derive(Clone, Debug, Eq, PartialEq)]
691pub struct RevisionImpactProcessSummary {
692 pub impact_version: u32,
693 pub atoms: usize,
694 pub queries: usize,
695 pub combinations: usize,
696 pub reusable_observations: usize,
697 pub invalidated_observations: usize,
698 pub minimal_invalidating_sets: usize,
699 pub minimal_semantic_change_sets: usize,
700 pub evidence_members: usize,
701 pub certificate_bytes: usize,
702 pub parsed_evidence_bytes: usize,
703 pub semantic_replays: usize,
704 pub component_validations: usize,
705 pub composed_pair_checks: usize,
706 pub final_transition_checks: usize,
707 pub result_comparisons: usize,
708 pub elapsed_micros: usize,
709 pub transitions: Vec<RevisionImpactQueryTransition>,
710 pub semantic_change_sets: Vec<RevisionImpactSemanticChangeSet>,
711}
712
713#[derive(Clone, Debug, Eq, PartialEq)]
714pub struct RevisionImpactQueryTransition {
715 pub index: usize,
716 pub horizon: u32,
717 pub bad_side: revision_local::ComponentSide,
718 pub bad_output: u64,
719 pub old_result: revision_local::BoundedResult,
720 pub new_result: revision_local::BoundedResult,
721}
722
723#[derive(Clone, Debug, Eq, PartialEq)]
724pub struct RevisionImpactSemanticChangeSet {
725 pub query_index: usize,
726 pub changed_mask: u16,
727 pub baseline_result: revision_local::BoundedResult,
728 pub changed_result: revision_local::BoundedResult,
729}
730
731#[derive(Clone, Debug, Eq, PartialEq)]
732pub struct Btor2ChannelPropertyCapabilities {
733 pub cli_version: u32,
734 pub artifact_version: u32,
735 pub query_manifest_version: u32,
736 pub policy_version: u32,
737 pub max_query_manifest_bytes: usize,
738 pub max_policy_bytes: usize,
739 pub max_model_bytes: usize,
740 pub max_channels: usize,
741 pub max_queries: usize,
742 pub max_evidence_bytes: usize,
743 pub max_artifact_bytes: usize,
744 pub max_projected_work: u64,
745 pub refusal_exit_code: i32,
746}
747
748#[derive(Clone, Debug)]
749pub struct Btor2ChannelPropertyFiles<'a> {
750 pub model: &'a Path,
751 pub queries: &'a Path,
752 pub policy: &'a Path,
753}
754
755#[derive(Clone, Copy, Debug, Eq, PartialEq)]
756pub enum Btor2ChannelPropertyAnswer {
757 Safe,
758 Unsafe,
759}
760
761#[derive(Clone, Copy, Debug, Eq, PartialEq)]
762pub enum Btor2ChannelPropertyKind {
763 OutputHigh,
764 OutputLow,
765}
766
767#[derive(Clone, Copy, Debug, Eq, PartialEq)]
768pub enum Btor2ChannelPropertyProcessBackend {
769 RepresentativeClass,
770 DirectExact,
771}
772
773#[derive(Clone, Copy, Debug, Eq, PartialEq)]
774pub enum Btor2ChannelPropertyProcessSolver {
775 ExplicitState,
776 BitblastCnf,
777}
778
779#[derive(Clone, Debug, Eq, PartialEq)]
780pub struct Btor2ChannelPropertyProcessResult {
781 pub index: usize,
782 pub query_id: u32,
783 pub channel: usize,
784 pub property: Btor2ChannelPropertyKind,
785 pub horizon: u32,
786 pub answer: Btor2ChannelPropertyAnswer,
787 pub bad_frame: Option<u32>,
788 pub backend: Btor2ChannelPropertyProcessBackend,
789 pub solver: Btor2ChannelPropertyProcessSolver,
790 pub representative_channel: usize,
791 pub witness_valuations: usize,
792}
793
794#[derive(Clone, Debug, Eq, PartialEq)]
795pub struct Btor2ChannelPropertyProcessSummary {
796 pub artifact_version: u32,
797 pub channels: usize,
798 pub logical_queries: usize,
799 pub proof_members: usize,
800 pub reused_queries: usize,
801 pub explicit_members: usize,
802 pub bitblast_members: usize,
803 pub evidence_bytes: usize,
804 pub artifact_bytes: usize,
805 pub projected_work: Option<u64>,
806 pub elapsed_micros: usize,
807 pub results: Vec<Btor2ChannelPropertyProcessResult>,
808}
809
810#[derive(Clone, Copy, Debug, Eq, PartialEq)]
811pub struct Btor2ChannelPropertyObservabilityCapabilities {
812 pub cli_version: u32,
813 pub phase_metrics_version: u32,
814}
815
816#[derive(Clone, Copy, Debug, Eq, PartialEq)]
817pub enum Btor2ChannelPropertyObservedOperation {
818 Certify,
819 Verify,
820}
821
822#[derive(Clone, Copy, Debug, Eq, PartialEq)]
823pub struct Btor2ChannelPropertyPhaseMetrics {
824 pub version: u32,
825 pub operation: Btor2ChannelPropertyObservedOperation,
826 pub input_micros: u128,
827 pub structural_admission_micros: u128,
828 pub preflight_micros: u128,
829 pub proof_construction_micros: u128,
830 pub encoding_micros: u128,
831 pub artifact_decode_micros: u128,
832 pub source_replay_micros: u128,
833 pub publication_micros: u128,
834 pub total_micros: u128,
835}
836
837#[derive(Clone, Debug, Eq, PartialEq)]
838pub struct Btor2ChannelPropertyPhaseObservedSummary {
839 pub verification: Btor2ChannelPropertyProcessSummary,
840 pub phases: Btor2ChannelPropertyPhaseMetrics,
841}
842
843#[derive(Clone, Copy, Debug, Eq, PartialEq)]
844pub enum Btor2ChannelPropertyResourceRefusalReason {
845 QueryCount,
846 MemberCount,
847 ProjectedWork,
848 EvidenceBytes,
849 ArtifactBytes,
850}
851
852impl Btor2ChannelPropertyResourceRefusalReason {
853 pub fn as_str(self) -> &'static str {
854 match self {
855 Self::QueryCount => "query-count",
856 Self::MemberCount => "member-count",
857 Self::ProjectedWork => "projected-work",
858 Self::EvidenceBytes => "evidence-bytes",
859 Self::ArtifactBytes => "artifact-bytes",
860 }
861 }
862}
863
864#[derive(Clone, Debug, Eq, PartialEq)]
866pub struct ControllerProofMtbddCapabilities {
867 pub cli_version: u32,
868 pub mtbdd_version: u32,
869 pub equivalence_proof_version: u32,
870 pub plant_artifact_version: u32,
871 pub manifest_version: u32,
872 pub max_manifest_bytes: usize,
873 pub max_artifact_bytes: usize,
874 pub max_equivalence_artifact_bytes: usize,
875 pub max_unsat_proof_bytes: usize,
876 pub max_members: usize,
877 pub max_state_bits: usize,
878 pub max_inputs: usize,
879 pub max_outputs: usize,
880 pub max_nodes: usize,
881 pub max_terminals: usize,
882 pub max_horizon: usize,
883}
884
885impl ControllerProofMtbddCapabilities {
886 fn common(&self) -> ControllerMtbddCapabilities {
887 ControllerMtbddCapabilities {
888 cli_version: self.cli_version,
889 mtbdd_version: self.mtbdd_version,
890 plant_artifact_version: self.plant_artifact_version,
891 manifest_version: self.manifest_version,
892 max_manifest_bytes: self.max_manifest_bytes,
893 max_artifact_bytes: self.max_artifact_bytes,
894 max_members: self.max_members,
895 max_state_bits: self.max_state_bits,
896 max_inputs: self.max_inputs,
897 max_outputs: self.max_outputs,
898 max_nodes: self.max_nodes,
899 max_terminals: self.max_terminals,
900 max_assignments: 0,
901 max_horizon: self.max_horizon,
902 }
903 }
904}
905
906#[derive(Clone, Debug, Eq, PartialEq)]
907pub struct ControllerSplitEvidenceCapabilities {
908 pub cli_version: u32,
909 pub controller_artifact_version: u32,
910 pub plant_artifact_version: u32,
911 pub manifest_version: u32,
912 pub max_manifest_bytes: usize,
913 pub max_artifact_bytes: usize,
914 pub max_batches: usize,
915}
916
917#[derive(Clone, Debug, Eq, PartialEq)]
918pub struct ControllerSplitArtifactSummary {
919 pub artifact_version: u32,
920 pub members: Option<usize>,
921 pub mtbdd_nodes: Option<usize>,
922 pub mtbdd_terminals: Option<usize>,
923 pub artifact_bytes: usize,
924 pub elapsed_micros: usize,
925}
926
927#[derive(Clone, Debug, Eq, PartialEq)]
928pub struct ControllerSplitBatchSummary {
929 pub index: usize,
930 pub members: usize,
931 pub safe: usize,
932 pub unsafe_count: usize,
933 pub reachable_product_states: usize,
934 pub explored_transitions: usize,
935 pub artifact_bytes: usize,
936 pub verification_micros: usize,
937}
938
939#[derive(Clone, Debug, Eq, PartialEq)]
940pub struct ControllerSplitSetSummary {
941 pub controller_admissions: usize,
942 pub members: usize,
943 pub safe: usize,
944 pub unsafe_count: usize,
945 pub reachable_product_states: usize,
946 pub explored_transitions: usize,
947 pub controller_evidence_bytes: usize,
948 pub admission_micros: usize,
949 pub elapsed_micros: usize,
950 pub batches: Vec<ControllerSplitBatchSummary>,
951}
952
953#[derive(Clone, Debug, Eq, PartialEq)]
954pub struct ControllerSplitResourceCapabilities {
955 pub cli_version: u32,
956 pub policy_version: u32,
957 pub controller_envelope_version: u32,
958 pub plant_envelope_version: u32,
959 pub controller_artifact_version: u32,
960 pub plant_artifact_version: u32,
961 pub manifest_version: u32,
962 pub max_policy_bytes: usize,
963 pub max_controller_artifact_bytes: usize,
964 pub max_unsat_proof_bytes: usize,
965 pub max_plant_artifact_bytes: usize,
966 pub max_batches: usize,
967 pub max_members_per_batch: usize,
968 pub max_horizon: usize,
969 pub max_product_states: usize,
970 pub refusal_exit_code: i32,
971}
972
973#[derive(Clone, Debug, Eq, PartialEq)]
974pub struct ControllerSplitResourceBatchSummary {
975 pub index: usize,
976 pub members: usize,
977 pub maximum_member_horizon: usize,
978 pub maximum_product_states: usize,
979 pub transition_evaluation_bound: usize,
980 pub safe: usize,
981 pub unsafe_count: usize,
982 pub reachable_product_states: usize,
983 pub explored_transitions: usize,
984 pub artifact_bytes: usize,
985 pub verification_micros: usize,
986}
987
988#[derive(Clone, Debug, Eq, PartialEq)]
989pub struct ControllerSplitResourceSetSummary {
990 pub controller_admissions: usize,
991 pub members: usize,
992 pub safe: usize,
993 pub unsafe_count: usize,
994 pub reachable_product_states: usize,
995 pub explored_transitions: usize,
996 pub controller_evidence_bytes: usize,
997 pub controller_mtbdd_bytes: usize,
998 pub equivalence_artifact_bytes: usize,
999 pub unsat_proof_bytes: usize,
1000 pub total_plant_artifact_bytes: usize,
1001 pub total_transition_evaluation_bound: usize,
1002 pub admission_micros: usize,
1003 pub elapsed_micros: usize,
1004 pub batches: Vec<ControllerSplitResourceBatchSummary>,
1005}
1006
1007#[derive(Clone, Debug, Eq, PartialEq)]
1008pub struct ControllerSplitObservabilityCapabilities {
1009 pub cli_version: u32,
1010 pub phase_metrics_version: u32,
1011 pub resource: ControllerSplitResourceCapabilities,
1012}
1013
1014#[derive(Clone, Debug, Eq, PartialEq)]
1015pub struct ControllerSplitPhaseMetrics {
1016 pub version: u32,
1017 pub policy_and_input_micros: u128,
1018 pub controller_admission_micros: u128,
1019 pub complete_set_preflight_micros: u128,
1020 pub semantic_replay_micros: u128,
1021 pub total_micros: u128,
1022 pub controller_admissions: usize,
1023 pub manifest_loads: usize,
1024 pub plant_artifact_reads: usize,
1025 pub resource_assessments: usize,
1026 pub batch_verifications: usize,
1027 pub buffered_result_rows: usize,
1028 pub prepared_batches: usize,
1029 pub prepared_members: usize,
1030 pub controller_evidence_bytes: usize,
1031 pub total_plant_artifact_bytes: usize,
1032 pub total_transition_evaluation_bound: usize,
1033}
1034
1035#[derive(Clone, Debug, Eq, PartialEq)]
1036pub struct ControllerSplitObservedSummary {
1037 pub verification: ControllerSplitResourceSetSummary,
1038 pub phases: ControllerSplitPhaseMetrics,
1039}
1040
1041#[derive(Clone, Debug, Eq, PartialEq)]
1042pub struct ControllerSplitAllocationObservabilityCapabilities {
1043 pub cli_version: u32,
1044 pub observability: ControllerSplitObservabilityCapabilities,
1045}
1046
1047#[derive(Clone, Debug, Eq, PartialEq)]
1048pub struct ControllerSplitAllocationMetrics {
1049 pub version: u32,
1050 pub allocation_calls: u64,
1051 pub allocated_bytes: u64,
1052 pub deallocation_calls: u64,
1053 pub deallocated_bytes: u64,
1054 pub reallocation_calls: u64,
1055 pub reallocated_bytes: u64,
1056}
1057
1058#[derive(Clone, Debug, Eq, PartialEq)]
1059pub struct ControllerSplitAllocationObservedSummary {
1060 pub observed: ControllerSplitObservedSummary,
1061 pub allocations: ControllerSplitAllocationMetrics,
1062}
1063
1064#[derive(Clone, Debug, Eq, PartialEq)]
1065pub struct ControllerSplitCacheObservabilityCapabilities {
1066 pub cli_version: u32,
1067 pub allocation_observability: ControllerSplitAllocationObservabilityCapabilities,
1068}
1069
1070#[derive(Clone, Debug, Eq, PartialEq)]
1071pub struct ControllerSplitCacheMetrics {
1072 pub version: u32,
1073 pub lookups: usize,
1074 pub hits: usize,
1075 pub misses: usize,
1076 pub entries: usize,
1077}
1078
1079#[derive(Clone, Debug, Eq, PartialEq)]
1080pub struct ControllerSplitCacheObservedSummary {
1081 pub observed: ControllerSplitAllocationObservedSummary,
1082 pub cache: ControllerSplitCacheMetrics,
1083}
1084
1085#[derive(Clone, Copy, Debug, Eq, PartialEq)]
1086pub enum ControllerMtbddAnswer {
1087 Safe,
1088 Unsafe,
1089}
1090
1091#[derive(Clone, Debug, Eq, PartialEq)]
1092pub struct ControllerMtbddMemberResult {
1093 pub index: usize,
1094 pub answer: ControllerMtbddAnswer,
1095 pub horizon: usize,
1096 pub bad_frame: Option<usize>,
1097 pub trace_steps: usize,
1098 pub reachable_product_states: usize,
1099 pub explored_transitions: usize,
1100}
1101
1102#[derive(Clone, Debug, Eq, PartialEq)]
1103pub struct ControllerMtbddBatchSummary {
1104 pub artifact_version: u32,
1105 pub safe: usize,
1106 pub unsafe_count: usize,
1107 pub mtbdd_nodes: usize,
1108 pub mtbdd_terminals: usize,
1109 pub assignments_checked: usize,
1110 pub reachable_product_states: usize,
1111 pub explored_transitions: usize,
1112 pub artifact_bytes: usize,
1113 pub elapsed_micros: usize,
1114 pub members: Vec<ControllerMtbddMemberResult>,
1115}
1116
1117pub type ControllerMtbddApiError = PredicateApiError;
1118pub type ControllerMtbddOperationError = PredicateOperationError;
1119
1120#[derive(Clone, Debug, Eq, PartialEq)]
1121pub struct ControllerPlantPortfolioCapabilities {
1122 pub cli_version: u32,
1123 pub artifact_version: u32,
1124 pub manifest_version: u32,
1125 pub max_manifest_bytes: usize,
1126 pub max_artifact_bytes: usize,
1127 pub max_members: usize,
1128}
1129
1130#[derive(Clone, Copy, Debug, Eq, PartialEq)]
1131pub enum ControllerPlantPortfolioBackend {
1132 Mtbdd,
1133 DirectExact,
1134}
1135
1136#[derive(Clone, Copy, Debug, Eq, PartialEq)]
1137pub enum ControllerPlantPortfolioReason {
1138 MtbddAdmitted,
1139 BoundaryLimit,
1140 TerminalLimit,
1141 NodeLimit,
1142}
1143
1144#[derive(Clone, Debug, Eq, PartialEq)]
1145pub struct ControllerPlantPortfolioBatchSummary {
1146 pub artifact_version: u32,
1147 pub backend: ControllerPlantPortfolioBackend,
1148 pub reason: ControllerPlantPortfolioReason,
1149 pub safe: usize,
1150 pub unsafe_count: usize,
1151 pub reachable_product_states: usize,
1152 pub explored_transitions: usize,
1153 pub artifact_bytes: usize,
1154 pub load_micros: usize,
1155 pub artifact_micros: usize,
1156 pub verification_micros: usize,
1157 pub write_micros: usize,
1158 pub elapsed_micros: usize,
1159 pub members: Vec<ControllerMtbddMemberResult>,
1160}
1161
1162pub type ControllerPlantPortfolioApiError = PredicateApiError;
1163pub type ControllerPlantPortfolioOperationError = PredicateOperationError;
1164
1165#[derive(Clone, Debug, Eq, PartialEq)]
1166pub struct ControllerPlantResourceCapabilities {
1167 pub cli_version: u32,
1168 pub policy_version: u32,
1169 pub envelope_version: u32,
1170 pub manifest_version: u32,
1171 pub portfolio_artifact_version: u32,
1172 pub max_policy_bytes: usize,
1173 pub max_artifact_bytes: usize,
1174 pub max_members: usize,
1175 pub max_horizon: usize,
1176 pub max_product_states: usize,
1177 pub refusal_exit_code: i32,
1178}
1179
1180#[derive(Clone, Debug, Eq, PartialEq)]
1181pub struct ControllerProofMtbddResourceCapabilities {
1182 pub cli_version: u32,
1183 pub policy_version: u32,
1184 pub envelope_version: u32,
1185 pub manifest_version: u32,
1186 pub artifact_version: u32,
1187 pub max_policy_bytes: usize,
1188 pub max_artifact_bytes: usize,
1189 pub max_equivalence_artifact_bytes: usize,
1190 pub max_unsat_proof_bytes: usize,
1191 pub max_members: usize,
1192 pub max_horizon: usize,
1193 pub max_product_states: usize,
1194 pub refusal_exit_code: i32,
1195}
1196
1197#[derive(Clone, Debug, Eq, PartialEq)]
1198pub struct ControllerProofMtbddResourceSummary {
1199 pub policy_version: u32,
1200 pub envelope_version: u32,
1201 pub artifact_version: u32,
1202 pub members: usize,
1203 pub maximum_member_horizon: usize,
1204 pub maximum_product_states: usize,
1205 pub transition_evaluation_bound: usize,
1206 pub equivalence_artifact_bytes: usize,
1207 pub unsat_proof_bytes: usize,
1208 pub safe: usize,
1209 pub unsafe_count: usize,
1210 pub reachable_product_states: usize,
1211 pub explored_transitions: usize,
1212 pub artifact_bytes: usize,
1213 pub assignments_checked: usize,
1214 pub load_micros: usize,
1215 pub artifact_micros: usize,
1216 pub verification_micros: usize,
1217 pub elapsed_micros: usize,
1218 pub member_results: Vec<ControllerMtbddMemberResult>,
1219}
1220
1221#[derive(Clone, Debug, Eq, PartialEq)]
1222pub struct ControllerProofMtbddPortfolioCapabilities {
1223 pub cli_version: u32,
1224 pub policy_version: u32,
1225 pub envelope_version: u32,
1226 pub artifact_version: u32,
1227 pub proof_artifact_version: u32,
1228 pub direct_artifact_version: u32,
1229 pub manifest_version: u32,
1230 pub source_model_attestation_version: u32,
1231 pub max_policy_bytes: usize,
1232 pub max_artifact_bytes: usize,
1233 pub max_equivalence_artifact_bytes: usize,
1234 pub max_unsat_proof_bytes: usize,
1235 pub max_members: usize,
1236 pub max_horizon: usize,
1237 pub max_product_states: usize,
1238 pub max_attestation_bytes: usize,
1239 pub refusal_exit_code: i32,
1240}
1241
1242#[derive(Clone, Debug, Eq, PartialEq)]
1243pub struct ControllerProofMtbddPortfolioResourceSummary {
1244 pub policy_version: u32,
1245 pub envelope_version: u32,
1246 pub artifact_version: u32,
1247 pub backend: ControllerPlantPortfolioBackend,
1248 pub reason: ControllerPlantPortfolioReason,
1249 pub members: usize,
1250 pub maximum_member_horizon: usize,
1251 pub maximum_product_states: usize,
1252 pub transition_evaluation_bound: usize,
1253 pub equivalence_artifact_bytes: usize,
1254 pub unsat_proof_bytes: usize,
1255 pub safe: usize,
1256 pub unsafe_count: usize,
1257 pub reachable_product_states: usize,
1258 pub explored_transitions: usize,
1259 pub artifact_bytes: usize,
1260 pub assignments_checked: usize,
1261 pub load_micros: usize,
1262 pub artifact_micros: usize,
1263 pub verification_micros: usize,
1264 pub elapsed_micros: usize,
1265 pub member_results: Vec<ControllerMtbddMemberResult>,
1266}
1267
1268#[derive(Clone, Debug, Eq, PartialEq)]
1269pub struct ControllerPlantResourceSummary {
1270 pub policy_version: u32,
1271 pub envelope_version: u32,
1272 pub artifact_version: u32,
1273 pub backend: ControllerPlantPortfolioBackend,
1274 pub members: usize,
1275 pub maximum_member_horizon: usize,
1276 pub maximum_product_states: usize,
1277 pub transition_evaluation_bound: usize,
1278 pub safe: usize,
1279 pub unsafe_count: usize,
1280 pub reachable_product_states: usize,
1281 pub explored_transitions: usize,
1282 pub artifact_bytes: usize,
1283 pub load_micros: usize,
1284 pub artifact_micros: usize,
1285 pub verification_micros: usize,
1286 pub elapsed_micros: usize,
1287 pub member_results: Vec<ControllerMtbddMemberResult>,
1288}
1289
1290#[derive(Clone, Copy, Debug, Eq, PartialEq)]
1291pub enum ControllerPlantResourceRefusalReason {
1292 ArtifactBytes,
1293 ControllerArtifactBytes,
1294 PlantArtifactBytes,
1295 EquivalenceArtifactBytes,
1296 UnsatProofBytes,
1297 Batches,
1298 Members,
1299 MembersPerBatch,
1300 Horizon,
1301 ProductStates,
1302 TransitionEvaluations,
1303 TransitionsPerBatch,
1304 TotalPlantArtifactBytes,
1305 TotalMembers,
1306 TotalTransitionEvaluations,
1307}
1308
1309impl ControllerPlantResourceRefusalReason {
1310 pub fn as_str(self) -> &'static str {
1311 match self {
1312 Self::ArtifactBytes => "artifact-bytes",
1313 Self::ControllerArtifactBytes => "controller-artifact-bytes",
1314 Self::PlantArtifactBytes => "plant-artifact-bytes",
1315 Self::EquivalenceArtifactBytes => "equivalence-artifact-bytes",
1316 Self::UnsatProofBytes => "unsat-proof-bytes",
1317 Self::Batches => "batches",
1318 Self::Members => "members",
1319 Self::MembersPerBatch => "members-per-batch",
1320 Self::Horizon => "horizon",
1321 Self::ProductStates => "product-states",
1322 Self::TransitionEvaluations => "transition-evaluations",
1323 Self::TransitionsPerBatch => "transitions-per-batch",
1324 Self::TotalPlantArtifactBytes => "total-plant-artifact-bytes",
1325 Self::TotalMembers => "total-members",
1326 Self::TotalTransitionEvaluations => "total-transition-evaluations",
1327 }
1328 }
1329}
1330
1331pub type ControllerPlantResourceApiError = PredicateApiError;
1332pub type ControllerPlantResourceOperationError = PredicateOperationError;
1333
1334pub type EventContractResult = PredicateResult;
1336pub type EventContractApiError = PredicateApiError;
1337pub type EventContractOperationError = PredicateOperationError;
1338
1339#[derive(Debug)]
1341pub enum PredicateApiError {
1342 Io(std::io::Error),
1343 InvalidPolicy(String),
1344 TimedOut {
1345 timeout: Duration,
1346 },
1347 OutputLimitExceeded {
1348 stream: &'static str,
1349 limit_bytes: usize,
1350 },
1351 CommandFailed {
1352 exit_code: Option<i32>,
1353 stderr: String,
1354 },
1355 ResourceRefused {
1356 reason: ControllerPlantResourceRefusalReason,
1357 },
1358 Btor2ChannelPropertyResourceRefused {
1359 reason: Btor2ChannelPropertyResourceRefusalReason,
1360 },
1361 IncompatibleContract(String),
1362 InvalidResponse(String),
1363}
1364
1365impl PredicateApiError {
1366 pub fn failure_class(&self) -> FailureClass {
1367 match self {
1368 Self::InvalidPolicy(_) => FailureClass::Policy,
1369 Self::Io(_) => FailureClass::Io,
1370 Self::TimedOut { .. } => FailureClass::Timeout,
1371 Self::OutputLimitExceeded { .. } => FailureClass::OutputLimit,
1372 Self::CommandFailed { .. } => FailureClass::ExitStatus,
1373 Self::ResourceRefused { .. } | Self::Btor2ChannelPropertyResourceRefused { .. } => {
1374 FailureClass::ResourceRefusal
1375 }
1376 Self::IncompatibleContract(_) => FailureClass::Compatibility,
1377 Self::InvalidResponse(_) => FailureClass::Response,
1378 }
1379 }
1380}
1381
1382#[derive(Debug)]
1383pub struct PredicateOperationError {
1384 pub error: Box<PredicateApiError>,
1385 pub metrics: InvocationMetrics,
1386}
1387
1388impl fmt::Display for PredicateOperationError {
1389 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
1390 self.error.fmt(formatter)
1391 }
1392}
1393
1394impl std::error::Error for PredicateOperationError {
1395 fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
1396 Some(self.error.as_ref())
1397 }
1398}
1399
1400impl fmt::Display for PredicateApiError {
1401 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
1402 match self {
1403 Self::Io(error) => write!(formatter, "predicate tool I/O failed: {error}"),
1404 Self::InvalidPolicy(message) => {
1405 write!(formatter, "invalid execution policy: {message}")
1406 }
1407 Self::TimedOut { timeout } => {
1408 write!(formatter, "predicate tool exceeded {timeout:?} deadline")
1409 }
1410 Self::OutputLimitExceeded {
1411 stream,
1412 limit_bytes,
1413 } => write!(
1414 formatter,
1415 "predicate tool {stream} exceeded {limit_bytes}-byte limit"
1416 ),
1417 Self::CommandFailed { exit_code, stderr } => write!(
1418 formatter,
1419 "predicate tool exited with {}: {}",
1420 exit_code
1421 .map(|code| code.to_string())
1422 .unwrap_or_else(|| "no status".to_string()),
1423 stderr.trim()
1424 ),
1425 Self::ResourceRefused { reason } => write!(
1426 formatter,
1427 "controller plant verification refused by resource policy: {}",
1428 reason.as_str()
1429 ),
1430 Self::Btor2ChannelPropertyResourceRefused { reason } => write!(
1431 formatter,
1432 "BTOR2 channel property production refused by resource policy: {}",
1433 reason.as_str()
1434 ),
1435 Self::IncompatibleContract(message) => {
1436 write!(formatter, "incompatible predicate contract: {message}")
1437 }
1438 Self::InvalidResponse(message) => {
1439 write!(formatter, "invalid predicate tool response: {message}")
1440 }
1441 }
1442 }
1443}
1444
1445impl std::error::Error for PredicateApiError {}
1446
1447impl From<std::io::Error> for PredicateApiError {
1448 fn from(error: std::io::Error) -> Self {
1449 Self::Io(error)
1450 }
1451}
1452
1453#[derive(Clone, Debug)]
1455pub struct PredicateTool {
1456 executable: PathBuf,
1457 capabilities: PredicateCapabilities,
1458 policy: ExecutionPolicy,
1459}
1460
1461impl PredicateTool {
1462 pub fn discover(executable: impl Into<PathBuf>) -> Result<Self, PredicateApiError> {
1464 Self::discover_with_policy(executable, ExecutionPolicy::default())
1465 }
1466
1467 pub fn discover_with_policy(
1469 executable: impl Into<PathBuf>,
1470 policy: ExecutionPolicy,
1471 ) -> Result<Self, PredicateApiError> {
1472 Self::discover_observed(executable, policy)
1473 .map(|observed| observed.value)
1474 .map_err(|failure| *failure.error)
1475 }
1476
1477 pub fn discover_observed(
1478 executable: impl Into<PathBuf>,
1479 policy: ExecutionPolicy,
1480 ) -> Result<Observed<Self>, PredicateOperationError> {
1481 let executable = executable.into();
1482 let mut command = Command::new(&executable);
1483 command.arg("predicate-cli-version");
1484 let output = run_bounded(OperationKind::Discover, command, policy)?;
1485 let (stdout, mut metrics) = successful_stdout(output)?;
1486 let capabilities = parse_capabilities(&stdout).map_err(|error| {
1487 metrics.status = InvocationStatus::Failed(error.failure_class());
1488 PredicateOperationError {
1489 error: Box::new(error),
1490 metrics: metrics.clone(),
1491 }
1492 })?;
1493 Ok(Observed {
1494 value: Self {
1495 executable,
1496 capabilities,
1497 policy,
1498 },
1499 metrics,
1500 })
1501 }
1502
1503 pub fn executable(&self) -> &Path {
1504 &self.executable
1505 }
1506
1507 pub fn capabilities(&self) -> &PredicateCapabilities {
1508 &self.capabilities
1509 }
1510
1511 pub fn execution_policy(&self) -> ExecutionPolicy {
1512 self.policy
1513 }
1514
1515 pub fn with_execution_policy(mut self, policy: ExecutionPolicy) -> Self {
1517 self.policy = policy;
1518 self
1519 }
1520
1521 pub fn certify(
1523 &self,
1524 version: CertificateVersion,
1525 model: &Path,
1526 bad_output: usize,
1527 transcript: &Path,
1528 certificate: &Path,
1529 ) -> Result<PredicateResult, PredicateApiError> {
1530 self.certify_observed(version, model, bad_output, transcript, certificate)
1531 .map(|observed| observed.value)
1532 .map_err(|failure| *failure.error)
1533 }
1534
1535 pub fn certify_observed(
1536 &self,
1537 version: CertificateVersion,
1538 model: &Path,
1539 bad_output: usize,
1540 transcript: &Path,
1541 certificate: &Path,
1542 ) -> Result<Observed<PredicateResult>, PredicateOperationError> {
1543 self.require_version_observed(version, version.certify_operation())?;
1544 let mut command = Command::new(&self.executable);
1545 command
1546 .arg(version.producer_command())
1547 .arg(model)
1548 .arg(bad_output.to_string())
1549 .arg(transcript)
1550 .arg(certificate);
1551 let output = run_bounded(version.certify_operation(), command, self.policy)?;
1552 parse_observed_result(output)
1553 }
1554
1555 pub fn verify(
1557 &self,
1558 version: CertificateVersion,
1559 model: &Path,
1560 certificate: &Path,
1561 ) -> Result<PredicateResult, PredicateApiError> {
1562 self.verify_observed(version, model, certificate)
1563 .map(|observed| observed.value)
1564 .map_err(|failure| *failure.error)
1565 }
1566
1567 pub fn verify_observed(
1568 &self,
1569 version: CertificateVersion,
1570 model: &Path,
1571 certificate: &Path,
1572 ) -> Result<Observed<PredicateResult>, PredicateOperationError> {
1573 self.require_version_observed(version, version.verify_operation())?;
1574 let mut command = Command::new(&self.executable);
1575 command
1576 .arg(version.verifier_command())
1577 .arg(model)
1578 .arg(certificate);
1579 let output = run_bounded(version.verify_operation(), command, self.policy)?;
1580 parse_observed_result(output)
1581 }
1582
1583 fn require_version_observed(
1584 &self,
1585 version: CertificateVersion,
1586 operation: OperationKind,
1587 ) -> Result<(), PredicateOperationError> {
1588 if !self.capabilities.certificate_versions.contains(&version) {
1589 let error = PredicateApiError::IncompatibleContract(format!(
1590 "certificate version {version:?} is not advertised"
1591 ));
1592 return Err(PredicateOperationError {
1593 metrics: empty_metrics(
1594 operation,
1595 self.policy,
1596 InvocationStatus::Failed(error.failure_class()),
1597 ),
1598 error: Box::new(error),
1599 });
1600 }
1601 Ok(())
1602 }
1603}
1604
1605#[derive(Clone, Debug)]
1607pub struct RevisionImpactTool {
1608 executable: PathBuf,
1609 capabilities: RevisionImpactCapabilities,
1610 policy: ExecutionPolicy,
1611}
1612
1613impl RevisionImpactTool {
1614 pub fn discover(executable: impl Into<PathBuf>) -> Result<Self, PredicateApiError> {
1617 let policy = ExecutionPolicy::default()
1618 .with_file_limit(revision_impact::MAX_REVISION_IMPACT_CERTIFICATE_BYTES as u64)?;
1619 Self::discover_with_policy(executable, policy)
1620 }
1621
1622 pub fn discover_with_policy(
1623 executable: impl Into<PathBuf>,
1624 policy: ExecutionPolicy,
1625 ) -> Result<Self, PredicateApiError> {
1626 Self::discover_observed(executable, policy)
1627 .map(|observed| observed.value)
1628 .map_err(|failure| *failure.error)
1629 }
1630
1631 pub fn discover_observed(
1632 executable: impl Into<PathBuf>,
1633 policy: ExecutionPolicy,
1634 ) -> Result<Observed<Self>, PredicateOperationError> {
1635 let executable = executable.into();
1636 let mut command = Command::new(&executable);
1637 command.arg("btor2-revision-impact-cli-version");
1638 let output = run_bounded(OperationKind::DiscoverRevisionImpact, command, policy)?;
1639 let (stdout, mut metrics) = successful_stdout(output)?;
1640 let capabilities = parse_revision_impact_capabilities(&stdout).map_err(|error| {
1641 metrics.status = InvocationStatus::Failed(error.failure_class());
1642 PredicateOperationError {
1643 error: Box::new(error),
1644 metrics: metrics.clone(),
1645 }
1646 })?;
1647 Ok(Observed {
1648 value: Self {
1649 executable,
1650 capabilities,
1651 policy,
1652 },
1653 metrics,
1654 })
1655 }
1656
1657 pub fn executable(&self) -> &Path {
1658 &self.executable
1659 }
1660
1661 pub fn capabilities(&self) -> &RevisionImpactCapabilities {
1662 &self.capabilities
1663 }
1664
1665 pub fn execution_policy(&self) -> ExecutionPolicy {
1666 self.policy
1667 }
1668
1669 pub fn with_execution_policy(mut self, policy: ExecutionPolicy) -> Self {
1670 self.policy = policy;
1671 self
1672 }
1673
1674 pub fn certify(
1675 &self,
1676 files: &RevisionImpactFiles<'_>,
1677 artifact: &Path,
1678 ) -> Result<RevisionImpactProcessSummary, PredicateApiError> {
1679 self.certify_observed(files, artifact)
1680 .map(|observed| observed.value)
1681 .map_err(|failure| *failure.error)
1682 }
1683
1684 pub fn certify_observed(
1685 &self,
1686 files: &RevisionImpactFiles<'_>,
1687 artifact: &Path,
1688 ) -> Result<Observed<RevisionImpactProcessSummary>, PredicateOperationError> {
1689 self.invoke(
1690 OperationKind::CertifyRevisionImpact,
1691 "check-btor2-revision-impact",
1692 "CREATED",
1693 files,
1694 artifact,
1695 )
1696 }
1697
1698 pub fn verify(
1699 &self,
1700 files: &RevisionImpactFiles<'_>,
1701 artifact: &Path,
1702 ) -> Result<RevisionImpactProcessSummary, PredicateApiError> {
1703 self.verify_observed(files, artifact)
1704 .map(|observed| observed.value)
1705 .map_err(|failure| *failure.error)
1706 }
1707
1708 pub fn verify_observed(
1709 &self,
1710 files: &RevisionImpactFiles<'_>,
1711 artifact: &Path,
1712 ) -> Result<Observed<RevisionImpactProcessSummary>, PredicateOperationError> {
1713 self.invoke(
1714 OperationKind::VerifyRevisionImpact,
1715 "verify-btor2-revision-impact",
1716 "VERIFIED",
1717 files,
1718 artifact,
1719 )
1720 }
1721
1722 fn invoke(
1723 &self,
1724 operation: OperationKind,
1725 command_name: &str,
1726 expected_status: &str,
1727 files: &RevisionImpactFiles<'_>,
1728 artifact: &Path,
1729 ) -> Result<Observed<RevisionImpactProcessSummary>, PredicateOperationError> {
1730 let left_outputs =
1731 canonical_node_list(files.left_outputs).map_err(|error| PredicateOperationError {
1732 metrics: empty_metrics(
1733 operation,
1734 self.policy,
1735 InvocationStatus::Failed(error.failure_class()),
1736 ),
1737 error: Box::new(error),
1738 })?;
1739 let right_outputs =
1740 canonical_node_list(files.right_outputs).map_err(|error| PredicateOperationError {
1741 metrics: empty_metrics(
1742 operation,
1743 self.policy,
1744 InvocationStatus::Failed(error.failure_class()),
1745 ),
1746 error: Box::new(error),
1747 })?;
1748 let mut command = Command::new(&self.executable);
1749 command
1750 .arg(command_name)
1751 .arg(files.left_old)
1752 .arg(files.left_new)
1753 .arg(left_outputs)
1754 .arg(files.right_old)
1755 .arg(files.right_new)
1756 .arg(right_outputs)
1757 .arg(files.interface_old)
1758 .arg(files.interface_new)
1759 .arg(files.queries)
1760 .arg(artifact);
1761 let output = run_bounded(operation, command, self.policy)?;
1762 parse_revision_impact_summary(output, expected_status, &self.capabilities)
1763 }
1764}
1765
1766#[derive(Clone, Debug)]
1768pub struct Btor2ChannelPropertyTool {
1769 executable: PathBuf,
1770 capabilities: Btor2ChannelPropertyCapabilities,
1771 policy: ExecutionPolicy,
1772}
1773
1774impl Btor2ChannelPropertyTool {
1775 pub fn discover(executable: impl Into<PathBuf>) -> Result<Self, PredicateApiError> {
1776 let policy = ExecutionPolicy::default()
1777 .with_file_limit(btor2_region_property::MAX_CHANNEL_PROPERTY_ARTIFACT_BYTES as u64)?;
1778 Self::discover_with_policy(executable, policy)
1779 }
1780
1781 pub fn discover_with_policy(
1782 executable: impl Into<PathBuf>,
1783 policy: ExecutionPolicy,
1784 ) -> Result<Self, PredicateApiError> {
1785 Self::discover_observed(executable, policy)
1786 .map(|observed| observed.value)
1787 .map_err(|failure| *failure.error)
1788 }
1789
1790 pub fn discover_observed(
1791 executable: impl Into<PathBuf>,
1792 policy: ExecutionPolicy,
1793 ) -> Result<Observed<Self>, PredicateOperationError> {
1794 let executable = executable.into();
1795 let mut command = Command::new(&executable);
1796 command.arg("btor2-channel-property-cli-version");
1797 let output = run_bounded(OperationKind::DiscoverBtor2ChannelProperty, command, policy)?;
1798 let (stdout, mut metrics) = successful_stdout(output)?;
1799 let capabilities = parse_btor2_channel_property_capabilities(&stdout).map_err(|error| {
1800 metrics.status = InvocationStatus::Failed(error.failure_class());
1801 PredicateOperationError {
1802 error: Box::new(error),
1803 metrics: metrics.clone(),
1804 }
1805 })?;
1806 Ok(Observed {
1807 value: Self {
1808 executable,
1809 capabilities,
1810 policy,
1811 },
1812 metrics,
1813 })
1814 }
1815
1816 pub fn capabilities(&self) -> &Btor2ChannelPropertyCapabilities {
1817 &self.capabilities
1818 }
1819
1820 pub fn execution_policy(&self) -> ExecutionPolicy {
1821 self.policy
1822 }
1823
1824 pub fn with_execution_policy(mut self, policy: ExecutionPolicy) -> Self {
1825 self.policy = policy;
1826 self
1827 }
1828
1829 pub fn observability(
1830 &self,
1831 ) -> Result<Btor2ChannelPropertyObservabilityTool, PredicateApiError> {
1832 self.observability_observed()
1833 .map(|observed| observed.value)
1834 .map_err(|failure| *failure.error)
1835 }
1836
1837 pub fn observability_observed(
1838 &self,
1839 ) -> Result<Observed<Btor2ChannelPropertyObservabilityTool>, PredicateOperationError> {
1840 let mut command = Command::new(&self.executable);
1841 command.arg("btor2-channel-property-observability-cli-version");
1842 let output = run_bounded(
1843 OperationKind::DiscoverBtor2ChannelPropertyObservability,
1844 command,
1845 self.policy,
1846 )?;
1847 let (stdout, mut metrics) = successful_stdout(output)?;
1848 let capabilities =
1849 parse_btor2_channel_property_observability_capabilities(&stdout, &self.capabilities)
1850 .map_err(|error| {
1851 metrics.status = InvocationStatus::Failed(error.failure_class());
1852 PredicateOperationError {
1853 error: Box::new(error),
1854 metrics: metrics.clone(),
1855 }
1856 })?;
1857 Ok(Observed {
1858 value: Btor2ChannelPropertyObservabilityTool {
1859 executable: self.executable.clone(),
1860 base_capabilities: self.capabilities.clone(),
1861 capabilities,
1862 policy: self.policy,
1863 },
1864 metrics,
1865 })
1866 }
1867
1868 pub fn certify(
1869 &self,
1870 files: &Btor2ChannelPropertyFiles<'_>,
1871 artifact: &Path,
1872 ) -> Result<Btor2ChannelPropertyProcessSummary, PredicateApiError> {
1873 self.certify_observed(files, artifact)
1874 .map(|observed| observed.value)
1875 .map_err(|failure| *failure.error)
1876 }
1877
1878 pub fn certify_observed(
1879 &self,
1880 files: &Btor2ChannelPropertyFiles<'_>,
1881 artifact: &Path,
1882 ) -> Result<Observed<Btor2ChannelPropertyProcessSummary>, PredicateOperationError> {
1883 self.invoke(
1884 OperationKind::CertifyBtor2ChannelProperty,
1885 "certify-btor2-channel-properties",
1886 "CREATED",
1887 files,
1888 artifact,
1889 )
1890 }
1891
1892 pub fn verify(
1893 &self,
1894 files: &Btor2ChannelPropertyFiles<'_>,
1895 artifact: &Path,
1896 ) -> Result<Btor2ChannelPropertyProcessSummary, PredicateApiError> {
1897 self.verify_observed(files, artifact)
1898 .map(|observed| observed.value)
1899 .map_err(|failure| *failure.error)
1900 }
1901
1902 pub fn verify_observed(
1903 &self,
1904 files: &Btor2ChannelPropertyFiles<'_>,
1905 artifact: &Path,
1906 ) -> Result<Observed<Btor2ChannelPropertyProcessSummary>, PredicateOperationError> {
1907 self.invoke(
1908 OperationKind::VerifyBtor2ChannelProperty,
1909 "verify-btor2-channel-properties",
1910 "VERIFIED",
1911 files,
1912 artifact,
1913 )
1914 }
1915
1916 fn invoke(
1917 &self,
1918 operation: OperationKind,
1919 command_name: &str,
1920 expected_status: &str,
1921 files: &Btor2ChannelPropertyFiles<'_>,
1922 artifact: &Path,
1923 ) -> Result<Observed<Btor2ChannelPropertyProcessSummary>, PredicateOperationError> {
1924 let mut command = Command::new(&self.executable);
1925 command
1926 .arg(command_name)
1927 .arg(files.model)
1928 .arg(files.queries)
1929 .arg(files.policy)
1930 .arg(artifact);
1931 let output = run_bounded(operation, command, self.policy)?;
1932 parse_btor2_channel_property_summary(output, expected_status, &self.capabilities)
1933 }
1934}
1935
1936#[derive(Clone, Debug)]
1938pub struct Btor2ChannelPropertyObservabilityTool {
1939 executable: PathBuf,
1940 base_capabilities: Btor2ChannelPropertyCapabilities,
1941 capabilities: Btor2ChannelPropertyObservabilityCapabilities,
1942 policy: ExecutionPolicy,
1943}
1944
1945impl Btor2ChannelPropertyObservabilityTool {
1946 pub fn capabilities(&self) -> &Btor2ChannelPropertyObservabilityCapabilities {
1947 &self.capabilities
1948 }
1949
1950 pub fn execution_policy(&self) -> ExecutionPolicy {
1951 self.policy
1952 }
1953
1954 pub fn with_execution_policy(mut self, policy: ExecutionPolicy) -> Self {
1955 self.policy = policy;
1956 self
1957 }
1958
1959 pub fn certify(
1960 &self,
1961 files: &Btor2ChannelPropertyFiles<'_>,
1962 artifact: &Path,
1963 ) -> Result<Btor2ChannelPropertyPhaseObservedSummary, PredicateApiError> {
1964 self.certify_observed(files, artifact)
1965 .map(|observed| observed.value)
1966 .map_err(|failure| *failure.error)
1967 }
1968
1969 pub fn certify_observed(
1970 &self,
1971 files: &Btor2ChannelPropertyFiles<'_>,
1972 artifact: &Path,
1973 ) -> Result<Observed<Btor2ChannelPropertyPhaseObservedSummary>, PredicateOperationError> {
1974 self.invoke(
1975 OperationKind::CertifyBtor2ChannelPropertyObserved,
1976 "certify-btor2-channel-properties-observed",
1977 "CREATED",
1978 Btor2ChannelPropertyObservedOperation::Certify,
1979 files,
1980 artifact,
1981 )
1982 }
1983
1984 pub fn verify(
1985 &self,
1986 files: &Btor2ChannelPropertyFiles<'_>,
1987 artifact: &Path,
1988 ) -> Result<Btor2ChannelPropertyPhaseObservedSummary, PredicateApiError> {
1989 self.verify_observed(files, artifact)
1990 .map(|observed| observed.value)
1991 .map_err(|failure| *failure.error)
1992 }
1993
1994 pub fn verify_observed(
1995 &self,
1996 files: &Btor2ChannelPropertyFiles<'_>,
1997 artifact: &Path,
1998 ) -> Result<Observed<Btor2ChannelPropertyPhaseObservedSummary>, PredicateOperationError> {
1999 self.invoke(
2000 OperationKind::VerifyBtor2ChannelPropertyObserved,
2001 "verify-btor2-channel-properties-observed",
2002 "VERIFIED",
2003 Btor2ChannelPropertyObservedOperation::Verify,
2004 files,
2005 artifact,
2006 )
2007 }
2008
2009 fn invoke(
2010 &self,
2011 operation: OperationKind,
2012 command_name: &str,
2013 expected_status: &str,
2014 expected_operation: Btor2ChannelPropertyObservedOperation,
2015 files: &Btor2ChannelPropertyFiles<'_>,
2016 artifact: &Path,
2017 ) -> Result<Observed<Btor2ChannelPropertyPhaseObservedSummary>, PredicateOperationError> {
2018 let mut command = Command::new(&self.executable);
2019 command
2020 .arg(command_name)
2021 .arg(files.model)
2022 .arg(files.queries)
2023 .arg(files.policy)
2024 .arg(artifact);
2025 let output = run_bounded(operation, command, self.policy)?;
2026 parse_btor2_channel_property_phase_observed_summary(
2027 output,
2028 expected_status,
2029 expected_operation,
2030 &self.base_capabilities,
2031 &self.capabilities,
2032 )
2033 }
2034}
2035
2036#[derive(Clone, Debug)]
2038pub struct ControllerMtbddTool {
2039 executable: PathBuf,
2040 capabilities: ControllerMtbddCapabilities,
2041 policy: ExecutionPolicy,
2042}
2043
2044impl ControllerMtbddTool {
2045 pub fn discover(executable: impl Into<PathBuf>) -> Result<Self, ControllerMtbddApiError> {
2046 Self::discover_with_policy(executable, ExecutionPolicy::default())
2047 }
2048
2049 pub fn discover_with_policy(
2050 executable: impl Into<PathBuf>,
2051 policy: ExecutionPolicy,
2052 ) -> Result<Self, ControllerMtbddApiError> {
2053 Self::discover_observed(executable, policy)
2054 .map(|observed| observed.value)
2055 .map_err(|failure| *failure.error)
2056 }
2057
2058 pub fn discover_observed(
2059 executable: impl Into<PathBuf>,
2060 policy: ExecutionPolicy,
2061 ) -> Result<Observed<Self>, ControllerMtbddOperationError> {
2062 let executable = executable.into();
2063 let mut command = Command::new(&executable);
2064 command.arg("controller-mtbdd-cli-version");
2065 let output = run_bounded(OperationKind::DiscoverControllerMtbdd, command, policy)?;
2066 let (stdout, mut metrics) = successful_stdout(output)?;
2067 let capabilities = parse_controller_mtbdd_capabilities(&stdout).map_err(|error| {
2068 metrics.status = InvocationStatus::Failed(error.failure_class());
2069 PredicateOperationError {
2070 error: Box::new(error),
2071 metrics: metrics.clone(),
2072 }
2073 })?;
2074 Ok(Observed {
2075 value: Self {
2076 executable,
2077 capabilities,
2078 policy,
2079 },
2080 metrics,
2081 })
2082 }
2083
2084 pub fn executable(&self) -> &Path {
2085 &self.executable
2086 }
2087
2088 pub fn capabilities(&self) -> &ControllerMtbddCapabilities {
2089 &self.capabilities
2090 }
2091
2092 pub fn execution_policy(&self) -> ExecutionPolicy {
2093 self.policy
2094 }
2095
2096 pub fn with_execution_policy(mut self, policy: ExecutionPolicy) -> Self {
2097 self.policy = policy;
2098 self
2099 }
2100
2101 pub fn certify(
2102 &self,
2103 manifest: &Path,
2104 artifact: &Path,
2105 ) -> Result<ControllerMtbddBatchSummary, ControllerMtbddApiError> {
2106 self.certify_observed(manifest, artifact)
2107 .map(|observed| observed.value)
2108 .map_err(|failure| *failure.error)
2109 }
2110
2111 pub fn certify_observed(
2112 &self,
2113 manifest: &Path,
2114 artifact: &Path,
2115 ) -> Result<Observed<ControllerMtbddBatchSummary>, ControllerMtbddOperationError> {
2116 let mut command = Command::new(&self.executable);
2117 command
2118 .arg("certify-controller-mtbdd-plant-batch")
2119 .arg(manifest)
2120 .arg(artifact);
2121 let output = run_bounded(
2122 OperationKind::CertifyControllerMtbddPlantBatch,
2123 command,
2124 self.policy,
2125 )?;
2126 parse_controller_mtbdd_summary(
2127 output,
2128 "CREATED",
2129 Some(artifact),
2130 &self.capabilities,
2131 "controller-mtbdd-plant-batch",
2132 "controller-mtbdd-plant-member",
2133 )
2134 }
2135
2136 pub fn verify(
2137 &self,
2138 manifest: &Path,
2139 artifact: &Path,
2140 ) -> Result<ControllerMtbddBatchSummary, ControllerMtbddApiError> {
2141 self.verify_observed(manifest, artifact)
2142 .map(|observed| observed.value)
2143 .map_err(|failure| *failure.error)
2144 }
2145
2146 pub fn verify_observed(
2147 &self,
2148 manifest: &Path,
2149 artifact: &Path,
2150 ) -> Result<Observed<ControllerMtbddBatchSummary>, ControllerMtbddOperationError> {
2151 let mut command = Command::new(&self.executable);
2152 command
2153 .arg("verify-controller-mtbdd-plant-batch")
2154 .arg(manifest)
2155 .arg(artifact);
2156 let output = run_bounded(
2157 OperationKind::VerifyControllerMtbddPlantBatch,
2158 command,
2159 self.policy,
2160 )?;
2161 parse_controller_mtbdd_summary(
2162 output,
2163 "VERIFIED",
2164 None,
2165 &self.capabilities,
2166 "controller-mtbdd-plant-batch",
2167 "controller-mtbdd-plant-member",
2168 )
2169 }
2170}
2171
2172#[derive(Clone, Debug)]
2174pub struct ControllerProofMtbddTool {
2175 executable: PathBuf,
2176 capabilities: ControllerProofMtbddCapabilities,
2177 policy: ExecutionPolicy,
2178}
2179
2180impl ControllerProofMtbddTool {
2181 pub fn discover(executable: impl Into<PathBuf>) -> Result<Self, ControllerMtbddApiError> {
2182 Self::discover_with_policy(executable, ExecutionPolicy::default())
2183 }
2184
2185 pub fn discover_with_policy(
2186 executable: impl Into<PathBuf>,
2187 policy: ExecutionPolicy,
2188 ) -> Result<Self, ControllerMtbddApiError> {
2189 Self::discover_observed(executable, policy)
2190 .map(|observed| observed.value)
2191 .map_err(|failure| *failure.error)
2192 }
2193
2194 pub fn discover_observed(
2195 executable: impl Into<PathBuf>,
2196 policy: ExecutionPolicy,
2197 ) -> Result<Observed<Self>, ControllerMtbddOperationError> {
2198 let executable = executable.into();
2199 let mut command = Command::new(&executable);
2200 command.arg("controller-proof-mtbdd-cli-version");
2201 let output = run_bounded(OperationKind::DiscoverControllerProofMtbdd, command, policy)?;
2202 let (stdout, mut metrics) = successful_stdout(output)?;
2203 let capabilities = parse_controller_proof_mtbdd_capabilities(&stdout).map_err(|error| {
2204 metrics.status = InvocationStatus::Failed(error.failure_class());
2205 PredicateOperationError {
2206 error: Box::new(error),
2207 metrics: metrics.clone(),
2208 }
2209 })?;
2210 Ok(Observed {
2211 value: Self {
2212 executable,
2213 capabilities,
2214 policy,
2215 },
2216 metrics,
2217 })
2218 }
2219
2220 pub fn executable(&self) -> &Path {
2221 &self.executable
2222 }
2223
2224 pub fn capabilities(&self) -> &ControllerProofMtbddCapabilities {
2225 &self.capabilities
2226 }
2227
2228 pub fn execution_policy(&self) -> ExecutionPolicy {
2229 self.policy
2230 }
2231
2232 pub fn with_execution_policy(mut self, policy: ExecutionPolicy) -> Self {
2233 self.policy = policy;
2234 self
2235 }
2236
2237 pub fn certify(
2238 &self,
2239 manifest: &Path,
2240 artifact: &Path,
2241 ) -> Result<ControllerMtbddBatchSummary, ControllerMtbddApiError> {
2242 self.certify_observed(manifest, artifact)
2243 .map(|observed| observed.value)
2244 .map_err(|failure| *failure.error)
2245 }
2246
2247 pub fn certify_observed(
2248 &self,
2249 manifest: &Path,
2250 artifact: &Path,
2251 ) -> Result<Observed<ControllerMtbddBatchSummary>, ControllerMtbddOperationError> {
2252 let mut command = Command::new(&self.executable);
2253 command
2254 .arg("certify-controller-proof-mtbdd-plant-batch")
2255 .arg(manifest)
2256 .arg(artifact);
2257 let output = run_bounded(
2258 OperationKind::CertifyControllerProofMtbddPlantBatch,
2259 command,
2260 self.policy,
2261 )?;
2262 parse_controller_mtbdd_summary(
2263 output,
2264 "CREATED",
2265 Some(artifact),
2266 &self.capabilities.common(),
2267 "controller-proof-mtbdd-plant-batch",
2268 "controller-proof-mtbdd-plant-member",
2269 )
2270 }
2271
2272 pub fn verify(
2273 &self,
2274 manifest: &Path,
2275 artifact: &Path,
2276 ) -> Result<ControllerMtbddBatchSummary, ControllerMtbddApiError> {
2277 self.verify_observed(manifest, artifact)
2278 .map(|observed| observed.value)
2279 .map_err(|failure| *failure.error)
2280 }
2281
2282 pub fn verify_observed(
2283 &self,
2284 manifest: &Path,
2285 artifact: &Path,
2286 ) -> Result<Observed<ControllerMtbddBatchSummary>, ControllerMtbddOperationError> {
2287 let mut command = Command::new(&self.executable);
2288 command
2289 .arg("verify-controller-proof-mtbdd-plant-batch")
2290 .arg(manifest)
2291 .arg(artifact);
2292 let output = run_bounded(
2293 OperationKind::VerifyControllerProofMtbddPlantBatch,
2294 command,
2295 self.policy,
2296 )?;
2297 parse_controller_mtbdd_summary(
2298 output,
2299 "VERIFIED",
2300 None,
2301 &self.capabilities.common(),
2302 "controller-proof-mtbdd-plant-batch",
2303 "controller-proof-mtbdd-plant-member",
2304 )
2305 }
2306}
2307
2308#[derive(Clone, Debug)]
2310pub struct ControllerSplitEvidenceTool {
2311 executable: PathBuf,
2312 capabilities: ControllerSplitEvidenceCapabilities,
2313 policy: ExecutionPolicy,
2314}
2315
2316impl ControllerSplitEvidenceTool {
2317 pub fn discover(executable: impl Into<PathBuf>) -> Result<Self, PredicateApiError> {
2318 Self::discover_with_policy(executable, ExecutionPolicy::default())
2319 }
2320
2321 pub fn discover_with_policy(
2322 executable: impl Into<PathBuf>,
2323 policy: ExecutionPolicy,
2324 ) -> Result<Self, PredicateApiError> {
2325 Self::discover_observed(executable, policy)
2326 .map(|observed| observed.value)
2327 .map_err(|failure| *failure.error)
2328 }
2329
2330 pub fn discover_observed(
2331 executable: impl Into<PathBuf>,
2332 policy: ExecutionPolicy,
2333 ) -> Result<Observed<Self>, PredicateOperationError> {
2334 let executable = executable.into();
2335 let mut command = Command::new(&executable);
2336 command.arg("controller-split-evidence-cli-version");
2337 let output = run_bounded(
2338 OperationKind::DiscoverControllerSplitEvidence,
2339 command,
2340 policy,
2341 )?;
2342 let (stdout, mut metrics) = successful_stdout(output)?;
2343 let capabilities =
2344 parse_controller_split_evidence_capabilities(&stdout).map_err(|error| {
2345 metrics.status = InvocationStatus::Failed(error.failure_class());
2346 PredicateOperationError {
2347 error: Box::new(error),
2348 metrics: metrics.clone(),
2349 }
2350 })?;
2351 Ok(Observed {
2352 value: Self {
2353 executable,
2354 capabilities,
2355 policy,
2356 },
2357 metrics,
2358 })
2359 }
2360
2361 pub fn capabilities(&self) -> &ControllerSplitEvidenceCapabilities {
2362 &self.capabilities
2363 }
2364
2365 pub fn execution_policy(&self) -> ExecutionPolicy {
2366 self.policy
2367 }
2368
2369 pub fn with_execution_policy(mut self, policy: ExecutionPolicy) -> Self {
2370 self.policy = policy;
2371 self
2372 }
2373
2374 pub fn certify_controller_evidence(
2375 &self,
2376 manifest: &Path,
2377 output: &Path,
2378 ) -> Result<ControllerSplitArtifactSummary, PredicateApiError> {
2379 self.certify_controller_evidence_observed(manifest, output)
2380 .map(|observed| observed.value)
2381 .map_err(|failure| *failure.error)
2382 }
2383
2384 pub fn certify_controller_evidence_observed(
2385 &self,
2386 manifest: &Path,
2387 output: &Path,
2388 ) -> Result<Observed<ControllerSplitArtifactSummary>, PredicateOperationError> {
2389 let mut command = Command::new(&self.executable);
2390 command
2391 .arg("certify-controller-proof-evidence-v1")
2392 .arg(manifest)
2393 .arg(output);
2394 let bounded = run_bounded(
2395 OperationKind::CertifyControllerProofEvidence,
2396 command,
2397 self.policy,
2398 )?;
2399 parse_controller_split_artifact_summary(
2400 bounded,
2401 "controller-split-evidence",
2402 false,
2403 output,
2404 &self.capabilities,
2405 )
2406 }
2407
2408 pub fn certify_plant_results(
2409 &self,
2410 manifest: &Path,
2411 evidence: &Path,
2412 output: &Path,
2413 ) -> Result<ControllerSplitArtifactSummary, PredicateApiError> {
2414 self.certify_plant_results_observed(manifest, evidence, output)
2415 .map(|observed| observed.value)
2416 .map_err(|failure| *failure.error)
2417 }
2418
2419 pub fn certify_plant_results_observed(
2420 &self,
2421 manifest: &Path,
2422 evidence: &Path,
2423 output: &Path,
2424 ) -> Result<Observed<ControllerSplitArtifactSummary>, PredicateOperationError> {
2425 let mut command = Command::new(&self.executable);
2426 command
2427 .arg("certify-bound-plant-results-v1")
2428 .arg(manifest)
2429 .arg(evidence)
2430 .arg(output);
2431 let bounded = run_bounded(
2432 OperationKind::CertifyBoundPlantResults,
2433 command,
2434 self.policy,
2435 )?;
2436 parse_controller_split_artifact_summary(
2437 bounded,
2438 "controller-split-plant",
2439 true,
2440 output,
2441 &self.capabilities,
2442 )
2443 }
2444
2445 pub fn verify_set(
2446 &self,
2447 evidence: &Path,
2448 batches: &[(&Path, &Path)],
2449 ) -> Result<ControllerSplitSetSummary, PredicateApiError> {
2450 self.verify_set_observed(evidence, batches)
2451 .map(|observed| observed.value)
2452 .map_err(|failure| *failure.error)
2453 }
2454
2455 pub fn verify_set_observed(
2456 &self,
2457 evidence: &Path,
2458 batches: &[(&Path, &Path)],
2459 ) -> Result<Observed<ControllerSplitSetSummary>, PredicateOperationError> {
2460 if batches.is_empty() || batches.len() > self.capabilities.max_batches {
2461 let error = PredicateApiError::InvalidPolicy(
2462 "split-evidence batch count is outside discovered limits".to_string(),
2463 );
2464 return Err(PredicateOperationError {
2465 metrics: empty_metrics(
2466 OperationKind::VerifyBoundPlantResultSet,
2467 self.policy,
2468 InvocationStatus::Failed(error.failure_class()),
2469 ),
2470 error: Box::new(error),
2471 });
2472 }
2473 let mut command = Command::new(&self.executable);
2474 command
2475 .arg("verify-bound-plant-result-set-v1")
2476 .arg(evidence);
2477 for &(manifest, result) in batches {
2478 command.arg(manifest).arg(result);
2479 }
2480 let bounded = run_bounded(
2481 OperationKind::VerifyBoundPlantResultSet,
2482 command,
2483 self.policy,
2484 )?;
2485 parse_controller_split_set_summary(bounded, batches.len(), &self.capabilities)
2486 }
2487}
2488
2489#[derive(Clone, Debug)]
2491pub struct ControllerSplitResourceTool {
2492 executable: PathBuf,
2493 capabilities: ControllerSplitResourceCapabilities,
2494 policy: ExecutionPolicy,
2495}
2496
2497impl ControllerSplitResourceTool {
2498 pub fn discover(executable: impl Into<PathBuf>) -> Result<Self, PredicateApiError> {
2499 Self::discover_with_policy(executable, ExecutionPolicy::default())
2500 }
2501
2502 pub fn discover_with_policy(
2503 executable: impl Into<PathBuf>,
2504 policy: ExecutionPolicy,
2505 ) -> Result<Self, PredicateApiError> {
2506 Self::discover_observed(executable, policy)
2507 .map(|observed| observed.value)
2508 .map_err(|failure| *failure.error)
2509 }
2510
2511 pub fn discover_observed(
2512 executable: impl Into<PathBuf>,
2513 policy: ExecutionPolicy,
2514 ) -> Result<Observed<Self>, PredicateOperationError> {
2515 let executable = executable.into();
2516 let mut command = Command::new(&executable);
2517 command.arg("controller-split-resource-cli-version");
2518 let output = run_bounded(
2519 OperationKind::DiscoverControllerSplitResource,
2520 command,
2521 policy,
2522 )?;
2523 let (stdout, mut metrics) = successful_stdout(output)?;
2524 let capabilities =
2525 parse_controller_split_resource_capabilities(&stdout).map_err(|error| {
2526 metrics.status = InvocationStatus::Failed(error.failure_class());
2527 PredicateOperationError {
2528 error: Box::new(error),
2529 metrics: metrics.clone(),
2530 }
2531 })?;
2532 Ok(Observed {
2533 value: Self {
2534 executable,
2535 capabilities,
2536 policy,
2537 },
2538 metrics,
2539 })
2540 }
2541
2542 pub fn capabilities(&self) -> &ControllerSplitResourceCapabilities {
2543 &self.capabilities
2544 }
2545
2546 pub fn execution_policy(&self) -> ExecutionPolicy {
2547 self.policy
2548 }
2549
2550 pub fn with_execution_policy(mut self, policy: ExecutionPolicy) -> Self {
2551 self.policy = policy;
2552 self
2553 }
2554
2555 pub fn verify_set(
2556 &self,
2557 evidence: &Path,
2558 resource_policy: &Path,
2559 batches: &[(&Path, &Path)],
2560 ) -> Result<ControllerSplitResourceSetSummary, PredicateApiError> {
2561 self.verify_set_observed(evidence, resource_policy, batches)
2562 .map(|observed| observed.value)
2563 .map_err(|failure| *failure.error)
2564 }
2565
2566 pub fn verify_set_observed(
2567 &self,
2568 evidence: &Path,
2569 resource_policy: &Path,
2570 batches: &[(&Path, &Path)],
2571 ) -> Result<Observed<ControllerSplitResourceSetSummary>, PredicateOperationError> {
2572 if batches.is_empty() || batches.len() > self.capabilities.max_batches {
2573 let error = PredicateApiError::InvalidPolicy(
2574 "governed split-evidence batch count is outside discovered limits".to_string(),
2575 );
2576 return Err(PredicateOperationError {
2577 metrics: empty_metrics(
2578 OperationKind::VerifyBoundPlantResultSetResources,
2579 self.policy,
2580 InvocationStatus::Failed(error.failure_class()),
2581 ),
2582 error: Box::new(error),
2583 });
2584 }
2585 let mut command = Command::new(&self.executable);
2586 command
2587 .arg("verify-bound-plant-result-set-with-resources-v1")
2588 .arg(evidence)
2589 .arg(resource_policy);
2590 for &(manifest, result) in batches {
2591 command.arg(manifest).arg(result);
2592 }
2593 let output = run_bounded(
2594 OperationKind::VerifyBoundPlantResultSetResources,
2595 command,
2596 self.policy,
2597 )?;
2598 parse_controller_split_resource_set_summary(output, batches.len(), &self.capabilities)
2599 .map_err(classify_controller_split_resource_refusal)
2600 }
2601}
2602
2603#[derive(Clone, Debug)]
2605pub struct ControllerSplitObservabilityTool {
2606 executable: PathBuf,
2607 capabilities: ControllerSplitObservabilityCapabilities,
2608 policy: ExecutionPolicy,
2609}
2610
2611impl ControllerSplitObservabilityTool {
2612 pub fn discover(executable: impl Into<PathBuf>) -> Result<Self, PredicateApiError> {
2613 Self::discover_with_policy(executable, ExecutionPolicy::default())
2614 }
2615
2616 pub fn discover_with_policy(
2617 executable: impl Into<PathBuf>,
2618 policy: ExecutionPolicy,
2619 ) -> Result<Self, PredicateApiError> {
2620 Self::discover_observed(executable, policy)
2621 .map(|observed| observed.value)
2622 .map_err(|failure| *failure.error)
2623 }
2624
2625 pub fn discover_observed(
2626 executable: impl Into<PathBuf>,
2627 policy: ExecutionPolicy,
2628 ) -> Result<Observed<Self>, PredicateOperationError> {
2629 let executable = executable.into();
2630 let mut command = Command::new(&executable);
2631 command.arg("controller-split-observability-cli-version");
2632 let output = run_bounded(
2633 OperationKind::DiscoverControllerSplitObservability,
2634 command,
2635 policy,
2636 )?;
2637 let (stdout, mut metrics) = successful_stdout(output)?;
2638 let capabilities =
2639 parse_controller_split_observability_capabilities(&stdout).map_err(|error| {
2640 metrics.status = InvocationStatus::Failed(error.failure_class());
2641 PredicateOperationError {
2642 error: Box::new(error),
2643 metrics: metrics.clone(),
2644 }
2645 })?;
2646 Ok(Observed {
2647 value: Self {
2648 executable,
2649 capabilities,
2650 policy,
2651 },
2652 metrics,
2653 })
2654 }
2655
2656 pub fn capabilities(&self) -> &ControllerSplitObservabilityCapabilities {
2657 &self.capabilities
2658 }
2659
2660 pub fn execution_policy(&self) -> ExecutionPolicy {
2661 self.policy
2662 }
2663
2664 pub fn with_execution_policy(mut self, policy: ExecutionPolicy) -> Self {
2665 self.policy = policy;
2666 self
2667 }
2668
2669 pub fn verify_set(
2670 &self,
2671 evidence: &Path,
2672 resource_policy: &Path,
2673 batches: &[(&Path, &Path)],
2674 ) -> Result<ControllerSplitObservedSummary, PredicateApiError> {
2675 self.verify_set_observed(evidence, resource_policy, batches)
2676 .map(|observed| observed.value)
2677 .map_err(|failure| *failure.error)
2678 }
2679
2680 pub fn verify_set_observed(
2681 &self,
2682 evidence: &Path,
2683 resource_policy: &Path,
2684 batches: &[(&Path, &Path)],
2685 ) -> Result<Observed<ControllerSplitObservedSummary>, PredicateOperationError> {
2686 if batches.is_empty() || batches.len() > self.capabilities.resource.max_batches {
2687 let error = PredicateApiError::InvalidPolicy(
2688 "observed split-evidence batch count is outside discovered limits".to_string(),
2689 );
2690 return Err(PredicateOperationError {
2691 metrics: empty_metrics(
2692 OperationKind::VerifyBoundPlantResultSetResourcesObserved,
2693 self.policy,
2694 InvocationStatus::Failed(error.failure_class()),
2695 ),
2696 error: Box::new(error),
2697 });
2698 }
2699 let mut command = Command::new(&self.executable);
2700 command
2701 .arg("verify-bound-plant-result-set-with-resources-observed-v1")
2702 .arg(evidence)
2703 .arg(resource_policy);
2704 for &(manifest, result) in batches {
2705 command.arg(manifest).arg(result);
2706 }
2707 let output = run_bounded(
2708 OperationKind::VerifyBoundPlantResultSetResourcesObserved,
2709 command,
2710 self.policy,
2711 )?;
2712 parse_controller_split_observed_summary(output, batches.len(), &self.capabilities)
2713 .map_err(classify_controller_split_resource_refusal)
2714 }
2715}
2716
2717#[derive(Clone, Debug)]
2719pub struct ControllerSplitAllocationObservabilityTool {
2720 executable: PathBuf,
2721 capabilities: ControllerSplitAllocationObservabilityCapabilities,
2722 policy: ExecutionPolicy,
2723}
2724
2725impl ControllerSplitAllocationObservabilityTool {
2726 pub fn discover(executable: impl Into<PathBuf>) -> Result<Self, PredicateApiError> {
2727 Self::discover_with_policy(executable, ExecutionPolicy::default())
2728 }
2729
2730 pub fn discover_with_policy(
2731 executable: impl Into<PathBuf>,
2732 policy: ExecutionPolicy,
2733 ) -> Result<Self, PredicateApiError> {
2734 Self::discover_observed(executable, policy)
2735 .map(|observed| observed.value)
2736 .map_err(|failure| *failure.error)
2737 }
2738
2739 pub fn discover_observed(
2740 executable: impl Into<PathBuf>,
2741 policy: ExecutionPolicy,
2742 ) -> Result<Observed<Self>, PredicateOperationError> {
2743 let executable = executable.into();
2744 let mut command = Command::new(&executable);
2745 command.arg("controller-split-allocation-observability-cli-version");
2746 let output = run_bounded(
2747 OperationKind::DiscoverControllerSplitAllocationObservability,
2748 command,
2749 policy,
2750 )?;
2751 let (stdout, mut metrics) = successful_stdout(output)?;
2752 let capabilities = parse_controller_split_allocation_observability_capabilities(&stdout)
2753 .map_err(|error| {
2754 metrics.status = InvocationStatus::Failed(error.failure_class());
2755 PredicateOperationError {
2756 error: Box::new(error),
2757 metrics: metrics.clone(),
2758 }
2759 })?;
2760 Ok(Observed {
2761 value: Self {
2762 executable,
2763 capabilities,
2764 policy,
2765 },
2766 metrics,
2767 })
2768 }
2769
2770 pub fn capabilities(&self) -> &ControllerSplitAllocationObservabilityCapabilities {
2771 &self.capabilities
2772 }
2773
2774 pub fn execution_policy(&self) -> ExecutionPolicy {
2775 self.policy
2776 }
2777
2778 pub fn with_execution_policy(mut self, policy: ExecutionPolicy) -> Self {
2779 self.policy = policy;
2780 self
2781 }
2782
2783 pub fn verify_set(
2784 &self,
2785 evidence: &Path,
2786 resource_policy: &Path,
2787 batches: &[(&Path, &Path)],
2788 ) -> Result<ControllerSplitAllocationObservedSummary, PredicateApiError> {
2789 self.verify_set_observed(evidence, resource_policy, batches)
2790 .map(|observed| observed.value)
2791 .map_err(|failure| *failure.error)
2792 }
2793
2794 pub fn verify_set_observed(
2795 &self,
2796 evidence: &Path,
2797 resource_policy: &Path,
2798 batches: &[(&Path, &Path)],
2799 ) -> Result<Observed<ControllerSplitAllocationObservedSummary>, PredicateOperationError> {
2800 if batches.is_empty()
2801 || batches.len() > self.capabilities.observability.resource.max_batches
2802 {
2803 let error = PredicateApiError::InvalidPolicy(
2804 "allocation-observed split-evidence batch count is outside discovered limits"
2805 .to_string(),
2806 );
2807 return Err(PredicateOperationError {
2808 metrics: empty_metrics(
2809 OperationKind::VerifyBoundPlantResultSetResourcesAllocationObserved,
2810 self.policy,
2811 InvocationStatus::Failed(error.failure_class()),
2812 ),
2813 error: Box::new(error),
2814 });
2815 }
2816 let mut command = Command::new(&self.executable);
2817 command
2818 .arg("verify-bound-plant-result-set-with-resources-allocation-observed-v1")
2819 .arg(evidence)
2820 .arg(resource_policy);
2821 for &(manifest, result) in batches {
2822 command.arg(manifest).arg(result);
2823 }
2824 let output = run_bounded(
2825 OperationKind::VerifyBoundPlantResultSetResourcesAllocationObserved,
2826 command,
2827 self.policy,
2828 )?;
2829 parse_controller_split_allocation_observed_summary(
2830 output,
2831 batches.len(),
2832 &self.capabilities,
2833 )
2834 .map_err(classify_controller_split_resource_refusal)
2835 }
2836}
2837
2838#[derive(Clone, Debug)]
2841pub struct ControllerSplitCacheObservabilityTool {
2842 executable: PathBuf,
2843 capabilities: ControllerSplitCacheObservabilityCapabilities,
2844 policy: ExecutionPolicy,
2845}
2846
2847impl ControllerSplitCacheObservabilityTool {
2848 pub fn discover(executable: impl Into<PathBuf>) -> Result<Self, PredicateApiError> {
2849 Self::discover_with_policy(executable, ExecutionPolicy::default())
2850 }
2851
2852 pub fn discover_with_policy(
2853 executable: impl Into<PathBuf>,
2854 policy: ExecutionPolicy,
2855 ) -> Result<Self, PredicateApiError> {
2856 Self::discover_observed(executable, policy)
2857 .map(|observed| observed.value)
2858 .map_err(|failure| *failure.error)
2859 }
2860
2861 pub fn discover_observed(
2862 executable: impl Into<PathBuf>,
2863 policy: ExecutionPolicy,
2864 ) -> Result<Observed<Self>, PredicateOperationError> {
2865 let executable = executable.into();
2866 let mut command = Command::new(&executable);
2867 command.arg("controller-split-cache-observability-cli-version");
2868 let output = run_bounded(
2869 OperationKind::DiscoverControllerSplitCacheObservability,
2870 command,
2871 policy,
2872 )?;
2873 let (stdout, mut metrics) = successful_stdout(output)?;
2874 let capabilities = parse_controller_split_cache_observability_capabilities(&stdout)
2875 .map_err(|error| {
2876 metrics.status = InvocationStatus::Failed(error.failure_class());
2877 PredicateOperationError {
2878 error: Box::new(error),
2879 metrics: metrics.clone(),
2880 }
2881 })?;
2882 Ok(Observed {
2883 value: Self {
2884 executable,
2885 capabilities,
2886 policy,
2887 },
2888 metrics,
2889 })
2890 }
2891
2892 pub fn capabilities(&self) -> &ControllerSplitCacheObservabilityCapabilities {
2893 &self.capabilities
2894 }
2895
2896 pub fn execution_policy(&self) -> ExecutionPolicy {
2897 self.policy
2898 }
2899
2900 pub fn with_execution_policy(mut self, policy: ExecutionPolicy) -> Self {
2901 self.policy = policy;
2902 self
2903 }
2904
2905 pub fn verify_set(
2906 &self,
2907 evidence: &Path,
2908 resource_policy: &Path,
2909 batches: &[(&Path, &Path)],
2910 ) -> Result<ControllerSplitCacheObservedSummary, PredicateApiError> {
2911 self.verify_set_observed(evidence, resource_policy, batches)
2912 .map(|observed| observed.value)
2913 .map_err(|failure| *failure.error)
2914 }
2915
2916 pub fn verify_set_observed(
2917 &self,
2918 evidence: &Path,
2919 resource_policy: &Path,
2920 batches: &[(&Path, &Path)],
2921 ) -> Result<Observed<ControllerSplitCacheObservedSummary>, PredicateOperationError> {
2922 if batches.is_empty()
2923 || batches.len()
2924 > self
2925 .capabilities
2926 .allocation_observability
2927 .observability
2928 .resource
2929 .max_batches
2930 {
2931 let error = PredicateApiError::InvalidPolicy(
2932 "cache-observed split-evidence batch count is outside discovered limits"
2933 .to_string(),
2934 );
2935 return Err(PredicateOperationError {
2936 metrics: empty_metrics(
2937 OperationKind::VerifyBoundPlantResultSetResourcesCacheObserved,
2938 self.policy,
2939 InvocationStatus::Failed(error.failure_class()),
2940 ),
2941 error: Box::new(error),
2942 });
2943 }
2944 let mut command = Command::new(&self.executable);
2945 command
2946 .arg("verify-bound-plant-result-set-with-resources-cache-observed-v1")
2947 .arg(evidence)
2948 .arg(resource_policy);
2949 for &(manifest, result) in batches {
2950 command.arg(manifest).arg(result);
2951 }
2952 let output = run_bounded(
2953 OperationKind::VerifyBoundPlantResultSetResourcesCacheObserved,
2954 command,
2955 self.policy,
2956 )?;
2957 parse_controller_split_cache_observed_summary(output, batches.len(), &self.capabilities)
2958 .map_err(classify_controller_split_resource_refusal)
2959 }
2960}
2961
2962#[derive(Clone, Debug)]
2964pub struct ControllerPlantPortfolioTool {
2965 executable: PathBuf,
2966 capabilities: ControllerPlantPortfolioCapabilities,
2967 policy: ExecutionPolicy,
2968}
2969
2970impl ControllerPlantPortfolioTool {
2971 pub fn discover(
2972 executable: impl Into<PathBuf>,
2973 ) -> Result<Self, ControllerPlantPortfolioApiError> {
2974 Self::discover_with_policy(executable, ExecutionPolicy::default())
2975 }
2976
2977 pub fn discover_with_policy(
2978 executable: impl Into<PathBuf>,
2979 policy: ExecutionPolicy,
2980 ) -> Result<Self, ControllerPlantPortfolioApiError> {
2981 Self::discover_observed(executable, policy)
2982 .map(|observed| observed.value)
2983 .map_err(|failure| *failure.error)
2984 }
2985
2986 pub fn discover_observed(
2987 executable: impl Into<PathBuf>,
2988 policy: ExecutionPolicy,
2989 ) -> Result<Observed<Self>, ControllerPlantPortfolioOperationError> {
2990 let executable = executable.into();
2991 let mut command = Command::new(&executable);
2992 command.arg("controller-plant-portfolio-cli-version");
2993 let output = run_bounded(
2994 OperationKind::DiscoverControllerPlantPortfolio,
2995 command,
2996 policy,
2997 )?;
2998 let (stdout, mut metrics) = successful_stdout(output)?;
2999 let capabilities =
3000 parse_controller_plant_portfolio_capabilities(&stdout).map_err(|error| {
3001 metrics.status = InvocationStatus::Failed(error.failure_class());
3002 PredicateOperationError {
3003 error: Box::new(error),
3004 metrics: metrics.clone(),
3005 }
3006 })?;
3007 Ok(Observed {
3008 value: Self {
3009 executable,
3010 capabilities,
3011 policy,
3012 },
3013 metrics,
3014 })
3015 }
3016
3017 pub fn executable(&self) -> &Path {
3018 &self.executable
3019 }
3020
3021 pub fn capabilities(&self) -> &ControllerPlantPortfolioCapabilities {
3022 &self.capabilities
3023 }
3024
3025 pub fn execution_policy(&self) -> ExecutionPolicy {
3026 self.policy
3027 }
3028
3029 pub fn with_execution_policy(mut self, policy: ExecutionPolicy) -> Self {
3030 self.policy = policy;
3031 self
3032 }
3033
3034 pub fn certify(
3035 &self,
3036 manifest: &Path,
3037 artifact: &Path,
3038 ) -> Result<ControllerPlantPortfolioBatchSummary, ControllerPlantPortfolioApiError> {
3039 self.certify_observed(manifest, artifact)
3040 .map(|observed| observed.value)
3041 .map_err(|failure| *failure.error)
3042 }
3043
3044 pub fn certify_observed(
3045 &self,
3046 manifest: &Path,
3047 artifact: &Path,
3048 ) -> Result<
3049 Observed<ControllerPlantPortfolioBatchSummary>,
3050 ControllerPlantPortfolioOperationError,
3051 > {
3052 let mut command = Command::new(&self.executable);
3053 command
3054 .arg("certify-controller-plant-portfolio")
3055 .arg(manifest)
3056 .arg(artifact);
3057 let output = run_bounded(
3058 OperationKind::CertifyControllerPlantPortfolio,
3059 command,
3060 self.policy,
3061 )?;
3062 parse_controller_plant_portfolio_summary(
3063 output,
3064 "CREATED",
3065 Some(artifact),
3066 &self.capabilities,
3067 )
3068 }
3069
3070 pub fn verify(
3071 &self,
3072 manifest: &Path,
3073 artifact: &Path,
3074 ) -> Result<ControllerPlantPortfolioBatchSummary, ControllerPlantPortfolioApiError> {
3075 self.verify_observed(manifest, artifact)
3076 .map(|observed| observed.value)
3077 .map_err(|failure| *failure.error)
3078 }
3079
3080 pub fn verify_observed(
3081 &self,
3082 manifest: &Path,
3083 artifact: &Path,
3084 ) -> Result<
3085 Observed<ControllerPlantPortfolioBatchSummary>,
3086 ControllerPlantPortfolioOperationError,
3087 > {
3088 let mut command = Command::new(&self.executable);
3089 command
3090 .arg("verify-controller-plant-portfolio")
3091 .arg(manifest)
3092 .arg(artifact);
3093 let output = run_bounded(
3094 OperationKind::VerifyControllerPlantPortfolio,
3095 command,
3096 self.policy,
3097 )?;
3098 parse_controller_plant_portfolio_summary(output, "VERIFIED", None, &self.capabilities)
3099 }
3100}
3101
3102#[derive(Clone, Debug)]
3104pub struct ControllerPlantResourceTool {
3105 executable: PathBuf,
3106 capabilities: ControllerPlantResourceCapabilities,
3107 policy: ExecutionPolicy,
3108}
3109
3110impl ControllerPlantResourceTool {
3111 pub fn discover(
3112 executable: impl Into<PathBuf>,
3113 ) -> Result<Self, ControllerPlantResourceApiError> {
3114 Self::discover_with_policy(executable, ExecutionPolicy::default())
3115 }
3116
3117 pub fn discover_with_policy(
3118 executable: impl Into<PathBuf>,
3119 policy: ExecutionPolicy,
3120 ) -> Result<Self, ControllerPlantResourceApiError> {
3121 Self::discover_observed(executable, policy)
3122 .map(|observed| observed.value)
3123 .map_err(|failure| *failure.error)
3124 }
3125
3126 pub fn discover_observed(
3127 executable: impl Into<PathBuf>,
3128 policy: ExecutionPolicy,
3129 ) -> Result<Observed<Self>, ControllerPlantResourceOperationError> {
3130 let executable = executable.into();
3131 let mut command = Command::new(&executable);
3132 command.arg("controller-plant-resource-cli-version");
3133 let output = run_bounded(
3134 OperationKind::DiscoverControllerPlantResource,
3135 command,
3136 policy,
3137 )?;
3138 let (stdout, mut metrics) = successful_stdout(output)?;
3139 let capabilities =
3140 parse_controller_plant_resource_capabilities(&stdout).map_err(|error| {
3141 metrics.status = InvocationStatus::Failed(error.failure_class());
3142 PredicateOperationError {
3143 error: Box::new(error),
3144 metrics: metrics.clone(),
3145 }
3146 })?;
3147 Ok(Observed {
3148 value: Self {
3149 executable,
3150 capabilities,
3151 policy,
3152 },
3153 metrics,
3154 })
3155 }
3156
3157 pub fn capabilities(&self) -> &ControllerPlantResourceCapabilities {
3158 &self.capabilities
3159 }
3160
3161 pub fn execution_policy(&self) -> ExecutionPolicy {
3162 self.policy
3163 }
3164
3165 pub fn with_execution_policy(mut self, policy: ExecutionPolicy) -> Self {
3166 self.policy = policy;
3167 self
3168 }
3169
3170 pub fn verify(
3171 &self,
3172 manifest: &Path,
3173 resource_policy: &Path,
3174 artifact: &Path,
3175 ) -> Result<ControllerPlantResourceSummary, ControllerPlantResourceApiError> {
3176 self.verify_observed(manifest, resource_policy, artifact)
3177 .map(|observed| observed.value)
3178 .map_err(|failure| *failure.error)
3179 }
3180
3181 pub fn verify_observed(
3182 &self,
3183 manifest: &Path,
3184 resource_policy: &Path,
3185 artifact: &Path,
3186 ) -> Result<Observed<ControllerPlantResourceSummary>, ControllerPlantResourceOperationError>
3187 {
3188 let mut command = Command::new(&self.executable);
3189 command
3190 .arg("verify-controller-plant-portfolio-resources")
3191 .arg(manifest)
3192 .arg(resource_policy)
3193 .arg(artifact);
3194 let output = run_bounded(
3195 OperationKind::VerifyControllerPlantPortfolioResources,
3196 command,
3197 self.policy,
3198 )?;
3199 parse_controller_plant_resource_summary(output, &self.capabilities)
3200 .map_err(classify_controller_plant_resource_refusal)
3201 }
3202}
3203
3204#[derive(Clone, Debug)]
3206pub struct ControllerProofMtbddResourceTool {
3207 executable: PathBuf,
3208 capabilities: ControllerProofMtbddResourceCapabilities,
3209 policy: ExecutionPolicy,
3210}
3211
3212impl ControllerProofMtbddResourceTool {
3213 pub fn discover(executable: impl Into<PathBuf>) -> Result<Self, PredicateApiError> {
3214 Self::discover_with_policy(executable, ExecutionPolicy::default())
3215 }
3216
3217 pub fn discover_with_policy(
3218 executable: impl Into<PathBuf>,
3219 policy: ExecutionPolicy,
3220 ) -> Result<Self, PredicateApiError> {
3221 Self::discover_observed(executable, policy)
3222 .map(|observed| observed.value)
3223 .map_err(|failure| *failure.error)
3224 }
3225
3226 pub fn discover_observed(
3227 executable: impl Into<PathBuf>,
3228 policy: ExecutionPolicy,
3229 ) -> Result<Observed<Self>, PredicateOperationError> {
3230 let executable = executable.into();
3231 let mut command = Command::new(&executable);
3232 command.arg("controller-proof-mtbdd-resource-cli-version");
3233 let output = run_bounded(
3234 OperationKind::DiscoverControllerProofMtbddResource,
3235 command,
3236 policy,
3237 )?;
3238 let (stdout, mut metrics) = successful_stdout(output)?;
3239 let capabilities =
3240 parse_controller_proof_mtbdd_resource_capabilities(&stdout).map_err(|error| {
3241 metrics.status = InvocationStatus::Failed(error.failure_class());
3242 PredicateOperationError {
3243 error: Box::new(error),
3244 metrics: metrics.clone(),
3245 }
3246 })?;
3247 Ok(Observed {
3248 value: Self {
3249 executable,
3250 capabilities,
3251 policy,
3252 },
3253 metrics,
3254 })
3255 }
3256
3257 pub fn capabilities(&self) -> &ControllerProofMtbddResourceCapabilities {
3258 &self.capabilities
3259 }
3260
3261 pub fn execution_policy(&self) -> ExecutionPolicy {
3262 self.policy
3263 }
3264
3265 pub fn with_execution_policy(mut self, policy: ExecutionPolicy) -> Self {
3266 self.policy = policy;
3267 self
3268 }
3269
3270 pub fn verify(
3271 &self,
3272 manifest: &Path,
3273 resource_policy: &Path,
3274 artifact: &Path,
3275 ) -> Result<ControllerProofMtbddResourceSummary, PredicateApiError> {
3276 self.verify_observed(manifest, resource_policy, artifact)
3277 .map(|observed| observed.value)
3278 .map_err(|failure| *failure.error)
3279 }
3280
3281 pub fn verify_observed(
3282 &self,
3283 manifest: &Path,
3284 resource_policy: &Path,
3285 artifact: &Path,
3286 ) -> Result<Observed<ControllerProofMtbddResourceSummary>, PredicateOperationError> {
3287 let mut command = Command::new(&self.executable);
3288 command
3289 .arg("verify-controller-proof-mtbdd-plant-resources")
3290 .arg(manifest)
3291 .arg(resource_policy)
3292 .arg(artifact);
3293 let output = run_bounded(
3294 OperationKind::VerifyControllerProofMtbddPlantResources,
3295 command,
3296 self.policy,
3297 )?;
3298 parse_controller_proof_mtbdd_resource_summary(output, &self.capabilities)
3299 .map_err(classify_controller_proof_mtbdd_resource_refusal)
3300 }
3301}
3302
3303#[derive(Clone, Debug)]
3305pub struct ControllerProofMtbddPortfolioTool {
3306 executable: PathBuf,
3307 capabilities: ControllerProofMtbddPortfolioCapabilities,
3308 policy: ExecutionPolicy,
3309}
3310
3311impl ControllerProofMtbddPortfolioTool {
3312 pub fn discover(executable: impl Into<PathBuf>) -> Result<Self, PredicateApiError> {
3313 Self::discover_with_policy(executable, ExecutionPolicy::default())
3314 }
3315
3316 pub fn discover_with_policy(
3317 executable: impl Into<PathBuf>,
3318 policy: ExecutionPolicy,
3319 ) -> Result<Self, PredicateApiError> {
3320 Self::discover_observed(executable, policy)
3321 .map(|observed| observed.value)
3322 .map_err(|failure| *failure.error)
3323 }
3324
3325 pub fn discover_observed(
3326 executable: impl Into<PathBuf>,
3327 policy: ExecutionPolicy,
3328 ) -> Result<Observed<Self>, PredicateOperationError> {
3329 let executable = executable.into();
3330 let mut command = Command::new(&executable);
3331 command.arg("controller-proof-mtbdd-portfolio-cli-version");
3332 let output = run_bounded(
3333 OperationKind::DiscoverControllerProofMtbddPortfolio,
3334 command,
3335 policy,
3336 )?;
3337 let (stdout, mut metrics) = successful_stdout(output)?;
3338 let capabilities =
3339 parse_controller_proof_mtbdd_portfolio_capabilities(&stdout).map_err(|error| {
3340 metrics.status = InvocationStatus::Failed(error.failure_class());
3341 PredicateOperationError {
3342 error: Box::new(error),
3343 metrics: metrics.clone(),
3344 }
3345 })?;
3346 Ok(Observed {
3347 value: Self {
3348 executable,
3349 capabilities,
3350 policy,
3351 },
3352 metrics,
3353 })
3354 }
3355
3356 pub fn capabilities(&self) -> &ControllerProofMtbddPortfolioCapabilities {
3357 &self.capabilities
3358 }
3359
3360 pub fn execution_policy(&self) -> ExecutionPolicy {
3361 self.policy
3362 }
3363
3364 pub fn with_execution_policy(mut self, policy: ExecutionPolicy) -> Self {
3365 self.policy = policy;
3366 self
3367 }
3368
3369 pub fn verify(
3370 &self,
3371 manifest: &Path,
3372 resource_policy: &Path,
3373 artifact: &Path,
3374 ) -> Result<ControllerProofMtbddPortfolioResourceSummary, PredicateApiError> {
3375 self.verify_observed(manifest, resource_policy, artifact)
3376 .map(|observed| observed.value)
3377 .map_err(|failure| *failure.error)
3378 }
3379
3380 pub fn verify_observed(
3381 &self,
3382 manifest: &Path,
3383 resource_policy: &Path,
3384 artifact: &Path,
3385 ) -> Result<Observed<ControllerProofMtbddPortfolioResourceSummary>, PredicateOperationError>
3386 {
3387 let mut command = Command::new(&self.executable);
3388 command
3389 .arg("verify-controller-proof-mtbdd-portfolio-resources")
3390 .arg(manifest)
3391 .arg(resource_policy)
3392 .arg(artifact);
3393 let output = run_bounded(
3394 OperationKind::VerifyControllerProofMtbddPortfolioResources,
3395 command,
3396 self.policy,
3397 )?;
3398 parse_controller_proof_mtbdd_portfolio_resource_summary(output, &self.capabilities, false)
3399 .map_err(classify_controller_proof_mtbdd_resource_refusal)
3400 }
3401
3402 pub fn verify_attested(
3403 &self,
3404 manifest: &Path,
3405 resource_policy: &Path,
3406 artifact: &Path,
3407 provenance_manifest: &Path,
3408 attestation: &Path,
3409 ) -> Result<ControllerProofMtbddPortfolioResourceSummary, PredicateApiError> {
3410 self.verify_attested_observed(
3411 manifest,
3412 resource_policy,
3413 artifact,
3414 provenance_manifest,
3415 attestation,
3416 )
3417 .map(|observed| observed.value)
3418 .map_err(|failure| *failure.error)
3419 }
3420
3421 pub fn verify_attested_observed(
3422 &self,
3423 manifest: &Path,
3424 resource_policy: &Path,
3425 artifact: &Path,
3426 provenance_manifest: &Path,
3427 attestation: &Path,
3428 ) -> Result<Observed<ControllerProofMtbddPortfolioResourceSummary>, PredicateOperationError>
3429 {
3430 let mut command = Command::new(&self.executable);
3431 command
3432 .arg("verify-controller-proof-mtbdd-portfolio-resources-attested")
3433 .arg(manifest)
3434 .arg(resource_policy)
3435 .arg(artifact)
3436 .arg(provenance_manifest)
3437 .arg(attestation);
3438 let output = run_bounded(
3439 OperationKind::VerifyControllerProofMtbddPortfolioResourcesAttested,
3440 command,
3441 self.policy,
3442 )?;
3443 parse_controller_proof_mtbdd_portfolio_resource_summary(output, &self.capabilities, true)
3444 .map_err(classify_controller_proof_mtbdd_resource_refusal)
3445 }
3446}
3447
3448#[derive(Clone, Debug)]
3450pub struct EventContractTool {
3451 executable: PathBuf,
3452 capabilities: EventContractCapabilities,
3453 policy: ExecutionPolicy,
3454}
3455
3456impl EventContractTool {
3457 pub fn discover(executable: impl Into<PathBuf>) -> Result<Self, EventContractApiError> {
3458 Self::discover_with_policy(executable, ExecutionPolicy::default())
3459 }
3460
3461 pub fn discover_with_policy(
3462 executable: impl Into<PathBuf>,
3463 policy: ExecutionPolicy,
3464 ) -> Result<Self, EventContractApiError> {
3465 Self::discover_observed(executable, policy)
3466 .map(|observed| observed.value)
3467 .map_err(|failure| *failure.error)
3468 }
3469
3470 pub fn discover_observed(
3471 executable: impl Into<PathBuf>,
3472 policy: ExecutionPolicy,
3473 ) -> Result<Observed<Self>, EventContractOperationError> {
3474 let executable = executable.into();
3475 let mut command = Command::new(&executable);
3476 command.arg("event-contract-cli-version");
3477 let output = run_bounded(OperationKind::DiscoverEventContract, command, policy)?;
3478 let (stdout, mut metrics) = successful_stdout(output)?;
3479 let capabilities = parse_event_contract_capabilities(&stdout).map_err(|error| {
3480 metrics.status = InvocationStatus::Failed(error.failure_class());
3481 PredicateOperationError {
3482 error: Box::new(error),
3483 metrics: metrics.clone(),
3484 }
3485 })?;
3486 Ok(Observed {
3487 value: Self {
3488 executable,
3489 capabilities,
3490 policy,
3491 },
3492 metrics,
3493 })
3494 }
3495
3496 pub fn executable(&self) -> &Path {
3497 &self.executable
3498 }
3499
3500 pub fn capabilities(&self) -> &EventContractCapabilities {
3501 &self.capabilities
3502 }
3503
3504 pub fn execution_policy(&self) -> ExecutionPolicy {
3505 self.policy
3506 }
3507
3508 pub fn with_execution_policy(mut self, policy: ExecutionPolicy) -> Self {
3509 self.policy = policy;
3510 self
3511 }
3512
3513 pub fn certify_v3(
3514 &self,
3515 model: &Path,
3516 bad_output: usize,
3517 contract: &Path,
3518 certificate: &Path,
3519 ) -> Result<EventContractResult, EventContractApiError> {
3520 self.certify_v3_observed(model, bad_output, contract, certificate)
3521 .map(|observed| observed.value)
3522 .map_err(|failure| *failure.error)
3523 }
3524
3525 pub fn certify_v3_observed(
3526 &self,
3527 model: &Path,
3528 bad_output: usize,
3529 contract: &Path,
3530 certificate: &Path,
3531 ) -> Result<Observed<EventContractResult>, EventContractOperationError> {
3532 let operation = OperationKind::CertifyEventContractV3;
3533 self.require_contract(operation)?;
3534 let mut command = Command::new(&self.executable);
3535 command
3536 .arg("certify-aiger-event-contract-v3")
3537 .arg(model)
3538 .arg(bad_output.to_string())
3539 .arg(contract)
3540 .arg(certificate);
3541 parse_observed_result(run_bounded(operation, command, self.policy)?)
3542 }
3543
3544 pub fn verify_v3(
3545 &self,
3546 model: &Path,
3547 contract: &Path,
3548 certificate: &Path,
3549 ) -> Result<EventContractResult, EventContractApiError> {
3550 self.verify_v3_observed(model, contract, certificate)
3551 .map(|observed| observed.value)
3552 .map_err(|failure| *failure.error)
3553 }
3554
3555 pub fn verify_v3_observed(
3556 &self,
3557 model: &Path,
3558 contract: &Path,
3559 certificate: &Path,
3560 ) -> Result<Observed<EventContractResult>, EventContractOperationError> {
3561 let operation = OperationKind::VerifyEventContractV3;
3562 self.require_contract(operation)?;
3563 let mut command = Command::new(&self.executable);
3564 command
3565 .arg("verify-aiger-event-contract-certificate-v3")
3566 .arg(model)
3567 .arg(contract)
3568 .arg(certificate);
3569 parse_observed_result(run_bounded(operation, command, self.policy)?)
3570 }
3571
3572 pub fn verify_portfolio(
3573 &self,
3574 model: &Path,
3575 bad_output: usize,
3576 contract: &Path,
3577 report: &Path,
3578 certificate: &Path,
3579 ) -> Result<EventContractResult, EventContractApiError> {
3580 self.verify_portfolio_observed(model, bad_output, contract, report, certificate)
3581 .map(|observed| observed.value)
3582 .map_err(|failure| *failure.error)
3583 }
3584
3585 pub fn verify_portfolio_observed(
3586 &self,
3587 model: &Path,
3588 bad_output: usize,
3589 contract: &Path,
3590 report: &Path,
3591 certificate: &Path,
3592 ) -> Result<Observed<EventContractResult>, EventContractOperationError> {
3593 let operation = OperationKind::EventContractPortfolio;
3594 self.require_contract(operation)?;
3595 let mut command = Command::new(&self.executable);
3596 command
3597 .arg("verify-aiger-event-contract-portfolio")
3598 .arg(model)
3599 .arg(bad_output.to_string())
3600 .arg(contract)
3601 .arg(report)
3602 .arg(certificate);
3603 parse_observed_result(run_bounded(operation, command, self.policy)?)
3604 }
3605
3606 pub fn verify_portfolio_report(
3607 &self,
3608 model: &Path,
3609 bad_output: usize,
3610 contract: &Path,
3611 report: &Path,
3612 certificate: &Path,
3613 ) -> Result<EventContractResult, EventContractApiError> {
3614 self.verify_portfolio_report_observed(model, bad_output, contract, report, certificate)
3615 .map(|observed| observed.value)
3616 .map_err(|failure| *failure.error)
3617 }
3618
3619 pub fn verify_portfolio_report_observed(
3620 &self,
3621 model: &Path,
3622 bad_output: usize,
3623 contract: &Path,
3624 report: &Path,
3625 certificate: &Path,
3626 ) -> Result<Observed<EventContractResult>, EventContractOperationError> {
3627 let operation = OperationKind::VerifyEventContractPortfolioReport;
3628 self.require_contract(operation)?;
3629 let mut command = Command::new(&self.executable);
3630 command
3631 .arg("verify-aiger-event-contract-portfolio-report")
3632 .arg(model)
3633 .arg(bad_output.to_string())
3634 .arg(contract)
3635 .arg(report)
3636 .arg(certificate);
3637 parse_observed_result(run_bounded(operation, command, self.policy)?)
3638 }
3639
3640 fn require_contract(
3641 &self,
3642 operation: OperationKind,
3643 ) -> Result<(), EventContractOperationError> {
3644 if self.capabilities.certificate_version != 3 || self.capabilities.portfolio_version != 1 {
3645 let error = PredicateApiError::IncompatibleContract(
3646 "event-contract certificate v3 and portfolio v1 are required".to_string(),
3647 );
3648 return Err(PredicateOperationError {
3649 metrics: empty_metrics(
3650 operation,
3651 self.policy,
3652 InvocationStatus::Failed(error.failure_class()),
3653 ),
3654 error: Box::new(error),
3655 });
3656 }
3657 Ok(())
3658 }
3659}
3660
3661#[derive(Debug)]
3662struct ManagedOutput {
3663 status: ExitStatus,
3664 stdout: Vec<u8>,
3665 stderr: Vec<u8>,
3666 metrics: InvocationMetrics,
3667}
3668
3669fn empty_metrics(
3670 operation: OperationKind,
3671 policy: ExecutionPolicy,
3672 status: InvocationStatus,
3673) -> InvocationMetrics {
3674 InvocationMetrics {
3675 schema_version: INVOCATION_METRICS_SCHEMA_VERSION,
3676 operation,
3677 duration: Duration::ZERO,
3678 stdout_bytes: 0,
3679 stderr_bytes: 0,
3680 timeout: policy.timeout,
3681 output_limit_bytes: policy.output_limit_bytes,
3682 memory_limit_bytes: policy.memory_limit_bytes,
3683 file_limit_bytes: policy.file_limit_bytes,
3684 process_group_containment: cfg!(unix),
3685 exit_code: None,
3686 status,
3687 }
3688}
3689
3690fn operation_failure(
3691 operation: OperationKind,
3692 policy: ExecutionPolicy,
3693 started: Instant,
3694 stdout_bytes: usize,
3695 stderr_bytes: usize,
3696 exit_code: Option<i32>,
3697 error: PredicateApiError,
3698) -> PredicateOperationError {
3699 PredicateOperationError {
3700 metrics: InvocationMetrics {
3701 schema_version: INVOCATION_METRICS_SCHEMA_VERSION,
3702 operation,
3703 duration: started.elapsed(),
3704 stdout_bytes,
3705 stderr_bytes,
3706 timeout: policy.timeout,
3707 output_limit_bytes: policy.output_limit_bytes,
3708 memory_limit_bytes: policy.memory_limit_bytes,
3709 file_limit_bytes: policy.file_limit_bytes,
3710 process_group_containment: cfg!(unix),
3711 exit_code,
3712 status: InvocationStatus::Failed(error.failure_class()),
3713 },
3714 error: Box::new(error),
3715 }
3716}
3717
3718fn read_limited(
3719 reader: impl Read + Send + 'static,
3720 limit: usize,
3721) -> thread::JoinHandle<Result<Vec<u8>, std::io::Error>> {
3722 thread::spawn(move || {
3723 let mut bytes = Vec::new();
3724 reader.take(limit as u64 + 1).read_to_end(&mut bytes)?;
3725 Ok(bytes)
3726 })
3727}
3728
3729fn join_output(
3730 handle: thread::JoinHandle<Result<Vec<u8>, std::io::Error>>,
3731) -> Result<Vec<u8>, PredicateApiError> {
3732 handle
3733 .join()
3734 .map_err(|_| PredicateApiError::InvalidResponse("output reader stopped".to_string()))?
3735 .map_err(PredicateApiError::Io)
3736}
3737
3738fn configure_process(
3739 command: &mut Command,
3740 policy: ExecutionPolicy,
3741) -> Result<(), PredicateApiError> {
3742 #[cfg(unix)]
3743 {
3744 let file_limit = libc::rlim_t::try_from(policy.file_limit_bytes).map_err(|_| {
3745 PredicateApiError::InvalidPolicy(
3746 "file limit is not representable on this platform".to_string(),
3747 )
3748 })?;
3749 #[cfg(not(target_os = "macos"))]
3750 let memory_limit = policy
3751 .memory_limit_bytes
3752 .map(libc::rlim_t::try_from)
3753 .transpose()
3754 .map_err(|_| {
3755 PredicateApiError::InvalidPolicy(
3756 "memory limit is not representable on this platform".to_string(),
3757 )
3758 })?;
3759 unsafe {
3762 command.pre_exec(move || {
3763 if libc::setsid() == -1 {
3764 return Err(std::io::Error::last_os_error());
3765 }
3766 #[cfg(not(target_os = "macos"))]
3767 if let Some(memory_limit) = memory_limit {
3768 let memory = libc::rlimit {
3769 rlim_cur: memory_limit,
3770 rlim_max: memory_limit,
3771 };
3772 if libc::setrlimit(libc::RLIMIT_AS, &memory) == -1 {
3773 return Err(std::io::Error::last_os_error());
3774 }
3775 }
3776 let file = libc::rlimit {
3777 rlim_cur: file_limit,
3778 rlim_max: file_limit,
3779 };
3780 if libc::setrlimit(libc::RLIMIT_FSIZE, &file) == -1 {
3781 return Err(std::io::Error::last_os_error());
3782 }
3783 Ok(())
3784 });
3785 }
3786 }
3787 #[cfg(not(unix))]
3788 let _ = (command, policy);
3789 Ok(())
3790}
3791
3792fn stop_process(child: &mut std::process::Child) {
3793 #[cfg(unix)]
3794 {
3795 if let Ok(group) = i32::try_from(child.id()) {
3796 let _ = unsafe { libc::kill(-group, libc::SIGKILL) };
3799 }
3800 }
3801 #[cfg(not(unix))]
3802 let _ = child.kill();
3803 let _ = child.wait();
3804}
3805
3806fn run_bounded(
3807 operation: OperationKind,
3808 mut command: Command,
3809 policy: ExecutionPolicy,
3810) -> Result<ManagedOutput, PredicateOperationError> {
3811 let started = Instant::now();
3812 command.stdout(Stdio::piped()).stderr(Stdio::piped());
3813 configure_process(&mut command, policy)
3814 .map_err(|error| operation_failure(operation, policy, started, 0, 0, None, error))?;
3815 let mut child = command
3816 .spawn()
3817 .map_err(|error| operation_failure(operation, policy, started, 0, 0, None, error.into()))?;
3818 let stdout = child.stdout.take().ok_or_else(|| {
3819 operation_failure(
3820 operation,
3821 policy,
3822 started,
3823 0,
3824 0,
3825 None,
3826 PredicateApiError::InvalidResponse("stdout pipe is missing".to_string()),
3827 )
3828 })?;
3829 let stderr = child.stderr.take().ok_or_else(|| {
3830 operation_failure(
3831 operation,
3832 policy,
3833 started,
3834 0,
3835 0,
3836 None,
3837 PredicateApiError::InvalidResponse("stderr pipe is missing".to_string()),
3838 )
3839 })?;
3840 let stdout_reader = read_limited(stdout, policy.output_limit_bytes);
3841 let stderr_reader = read_limited(stderr, policy.output_limit_bytes);
3842 let deadline = Instant::now() + policy.timeout;
3843 let status = loop {
3844 if let Some(status) = child.try_wait().map_err(|error| {
3845 operation_failure(operation, policy, started, 0, 0, None, error.into())
3846 })? {
3847 break status;
3848 }
3849 if Instant::now() >= deadline {
3850 stop_process(&mut child);
3851 let stdout_bytes = join_output(stdout_reader)
3852 .map(|bytes| bytes.len())
3853 .unwrap_or(0);
3854 let stderr_bytes = join_output(stderr_reader)
3855 .map(|bytes| bytes.len())
3856 .unwrap_or(0);
3857 return Err(operation_failure(
3858 operation,
3859 policy,
3860 started,
3861 stdout_bytes,
3862 stderr_bytes,
3863 None,
3864 PredicateApiError::TimedOut {
3865 timeout: policy.timeout,
3866 },
3867 ));
3868 }
3869 thread::sleep(Duration::from_millis(5));
3870 };
3871 let stdout = join_output(stdout_reader).map_err(|error| {
3872 operation_failure(operation, policy, started, 0, 0, status.code(), error)
3873 })?;
3874 let stderr = join_output(stderr_reader).map_err(|error| {
3875 operation_failure(
3876 operation,
3877 policy,
3878 started,
3879 stdout.len(),
3880 0,
3881 status.code(),
3882 error,
3883 )
3884 })?;
3885 if stdout.len() > policy.output_limit_bytes {
3886 return Err(operation_failure(
3887 operation,
3888 policy,
3889 started,
3890 stdout.len(),
3891 stderr.len(),
3892 status.code(),
3893 PredicateApiError::OutputLimitExceeded {
3894 stream: "stdout",
3895 limit_bytes: policy.output_limit_bytes,
3896 },
3897 ));
3898 }
3899 if stderr.len() > policy.output_limit_bytes {
3900 return Err(operation_failure(
3901 operation,
3902 policy,
3903 started,
3904 stdout.len(),
3905 stderr.len(),
3906 status.code(),
3907 PredicateApiError::OutputLimitExceeded {
3908 stream: "stderr",
3909 limit_bytes: policy.output_limit_bytes,
3910 },
3911 ));
3912 }
3913 let metrics = InvocationMetrics {
3914 schema_version: INVOCATION_METRICS_SCHEMA_VERSION,
3915 operation,
3916 duration: started.elapsed(),
3917 stdout_bytes: stdout.len(),
3918 stderr_bytes: stderr.len(),
3919 timeout: policy.timeout,
3920 output_limit_bytes: policy.output_limit_bytes,
3921 memory_limit_bytes: policy.memory_limit_bytes,
3922 file_limit_bytes: policy.file_limit_bytes,
3923 process_group_containment: cfg!(unix),
3924 exit_code: status.code(),
3925 status: InvocationStatus::Success,
3926 };
3927 Ok(ManagedOutput {
3928 status,
3929 stdout,
3930 stderr,
3931 metrics,
3932 })
3933}
3934
3935fn successful_stdout(
3936 mut output: ManagedOutput,
3937) -> Result<(String, InvocationMetrics), PredicateOperationError> {
3938 if !output.status.success() {
3939 let error = PredicateApiError::CommandFailed {
3940 exit_code: output.status.code(),
3941 stderr: String::from_utf8_lossy(&output.stderr).into_owned(),
3942 };
3943 output.metrics.status = InvocationStatus::Failed(error.failure_class());
3944 return Err(PredicateOperationError {
3945 error: Box::new(error),
3946 metrics: output.metrics,
3947 });
3948 }
3949 let stdout = String::from_utf8(output.stdout).map_err(|_| {
3950 let error = PredicateApiError::InvalidResponse("stdout is not UTF-8".to_string());
3951 output.metrics.status = InvocationStatus::Failed(error.failure_class());
3952 PredicateOperationError {
3953 error: Box::new(error),
3954 metrics: output.metrics.clone(),
3955 }
3956 })?;
3957 Ok((stdout, output.metrics))
3958}
3959
3960fn parse_observed_result(
3961 output: ManagedOutput,
3962) -> Result<Observed<PredicateResult>, PredicateOperationError> {
3963 let (stdout, mut metrics) = successful_stdout(output)?;
3964 let value = parse_result(&stdout).map_err(|error| {
3965 metrics.status = InvocationStatus::Failed(error.failure_class());
3966 PredicateOperationError {
3967 error: Box::new(error),
3968 metrics: metrics.clone(),
3969 }
3970 })?;
3971 Ok(Observed { value, metrics })
3972}
3973
3974fn parse_result(stdout: &str) -> Result<PredicateResult, PredicateApiError> {
3975 let results = stdout
3976 .split_whitespace()
3977 .filter_map(|field| field.strip_prefix("result="))
3978 .collect::<Vec<_>>();
3979 let Some(&result) = results.first() else {
3980 return Err(PredicateApiError::InvalidResponse(
3981 "result field is missing".to_string(),
3982 ));
3983 };
3984 if results.iter().any(|candidate| *candidate != result) {
3985 return Err(PredicateApiError::InvalidResponse(
3986 "subprocess result fields disagree".to_string(),
3987 ));
3988 }
3989 match result {
3990 "avoidable" => Ok(PredicateResult::Avoidable),
3991 "unavoidable" => Ok(PredicateResult::Unavoidable),
3992 _ => Err(PredicateApiError::InvalidResponse(format!(
3993 "unsupported result `{result}`"
3994 ))),
3995 }
3996}
3997
3998fn canonical_usize(value: &str, field: &str) -> Result<usize, PredicateApiError> {
3999 let parsed = value.parse::<usize>().map_err(|_| {
4000 PredicateApiError::InvalidResponse(format!("{field} is not an unsigned integer"))
4001 })?;
4002 if parsed.to_string() != value {
4003 return Err(PredicateApiError::InvalidResponse(format!(
4004 "{field} is noncanonical"
4005 )));
4006 }
4007 Ok(parsed)
4008}
4009
4010fn canonical_u32(value: &str, field: &str) -> Result<u32, PredicateApiError> {
4011 let parsed = canonical_usize(value, field)?;
4012 u32::try_from(parsed).map_err(|_| {
4013 PredicateApiError::InvalidResponse(format!("{field} exceeds canonical u32 range"))
4014 })
4015}
4016
4017fn canonical_u128(value: &str, field: &str) -> Result<u128, PredicateApiError> {
4018 let parsed = value.parse::<u128>().map_err(|_| {
4019 PredicateApiError::InvalidResponse(format!("{field} is not an unsigned integer"))
4020 })?;
4021 if parsed.to_string() != value {
4022 return Err(PredicateApiError::InvalidResponse(format!(
4023 "{field} is noncanonical"
4024 )));
4025 }
4026 Ok(parsed)
4027}
4028
4029fn canonical_u64(value: &str, field: &str) -> Result<u64, PredicateApiError> {
4030 let parsed = value.parse::<u64>().map_err(|_| {
4031 PredicateApiError::InvalidResponse(format!("{field} is not an unsigned integer"))
4032 })?;
4033 if parsed.to_string() != value {
4034 return Err(PredicateApiError::InvalidResponse(format!(
4035 "{field} is noncanonical"
4036 )));
4037 }
4038 Ok(parsed)
4039}
4040
4041fn token_value<'a>(token: &'a str, key: &str) -> Result<&'a str, PredicateApiError> {
4042 token
4043 .strip_prefix(&format!("{key}="))
4044 .ok_or_else(|| PredicateApiError::InvalidResponse(format!("expected response field {key}")))
4045}
4046
4047fn parse_controller_proof_mtbdd_resource_capabilities(
4048 line: &str,
4049) -> Result<ControllerProofMtbddResourceCapabilities, PredicateApiError> {
4050 if line.contains('\r') || !line.ends_with('\n') || line.lines().count() != 1 {
4051 return Err(PredicateApiError::InvalidResponse(
4052 "controller proof MTBDD resource capability line is not canonical".to_string(),
4053 ));
4054 }
4055 let fields = line.trim_end_matches('\n').split(' ').collect::<Vec<_>>();
4056 let keys = [
4057 "controller_proof_mtbdd_resource_cli_version",
4058 "policy_version",
4059 "envelope_version",
4060 "manifest_version",
4061 "artifact_version",
4062 "max_policy_bytes",
4063 "max_artifact_bytes",
4064 "max_equivalence_artifact_bytes",
4065 "max_unsat_proof_bytes",
4066 "max_members",
4067 "max_horizon",
4068 "max_product_states",
4069 "refusal_exit",
4070 "verification",
4071 "exhaustive_replay",
4072 "accounting",
4073 "timing_calibration",
4074 "result_on_refusal",
4075 "refusal_schema",
4076 "unsupported",
4077 ];
4078 if fields.len() != keys.len() {
4079 return Err(PredicateApiError::InvalidResponse(
4080 "controller proof MTBDD resource capability field count is invalid".to_string(),
4081 ));
4082 }
4083 let values = fields
4084 .iter()
4085 .zip(keys)
4086 .map(|(field, key)| token_value(field, key))
4087 .collect::<Result<Vec<_>, _>>()?;
4088 if values[13..]
4089 != [
4090 "unsat-miter",
4091 "no",
4092 "conservative-static",
4093 "none",
4094 "none",
4095 "proof-reason-v1",
4096 "fail-closed",
4097 ]
4098 {
4099 return Err(PredicateApiError::IncompatibleContract(
4100 "controller proof MTBDD resource contract is unsupported".to_string(),
4101 ));
4102 }
4103 let versions = values[..5]
4104 .iter()
4105 .enumerate()
4106 .map(|(index, value)| canonical_u32(value, keys[index]))
4107 .collect::<Result<Vec<_>, _>>()?;
4108 let limits = values[5..12]
4109 .iter()
4110 .enumerate()
4111 .map(|(index, value)| canonical_usize(value, keys[index + 5]))
4112 .collect::<Result<Vec<_>, _>>()?;
4113 let refusal_exit = canonical_usize(values[12], keys[12])?;
4114 if versions != [1, 1, 1, 1, 1] {
4115 return Err(PredicateApiError::IncompatibleContract(
4116 "controller proof MTBDD resource version tuple is unsupported".to_string(),
4117 ));
4118 }
4119 if limits.contains(&0) || refusal_exit != 3 {
4120 return Err(PredicateApiError::InvalidResponse(
4121 "controller proof MTBDD resource discovered limit must be positive".to_string(),
4122 ));
4123 }
4124 Ok(ControllerProofMtbddResourceCapabilities {
4125 cli_version: versions[0],
4126 policy_version: versions[1],
4127 envelope_version: versions[2],
4128 manifest_version: versions[3],
4129 artifact_version: versions[4],
4130 max_policy_bytes: limits[0],
4131 max_artifact_bytes: limits[1],
4132 max_equivalence_artifact_bytes: limits[2],
4133 max_unsat_proof_bytes: limits[3],
4134 max_members: limits[4],
4135 max_horizon: limits[5],
4136 max_product_states: limits[6],
4137 refusal_exit_code: 3,
4138 })
4139}
4140
4141fn parse_controller_proof_mtbdd_portfolio_capabilities(
4142 line: &str,
4143) -> Result<ControllerProofMtbddPortfolioCapabilities, PredicateApiError> {
4144 if line.contains('\r') || !line.ends_with('\n') || line.lines().count() != 1 {
4145 return Err(PredicateApiError::InvalidResponse(
4146 "controller proof MTBDD portfolio capability line is not canonical".to_string(),
4147 ));
4148 }
4149 let fields = line.trim_end_matches('\n').split(' ').collect::<Vec<_>>();
4150 let keys = [
4151 "controller_proof_mtbdd_portfolio_cli_version",
4152 "policy_version",
4153 "envelope_version",
4154 "artifact_version",
4155 "proof_artifact_version",
4156 "direct_artifact_version",
4157 "manifest_version",
4158 "source_model_attestation_version",
4159 "max_policy_bytes",
4160 "max_artifact_bytes",
4161 "max_equivalence_artifact_bytes",
4162 "max_unsat_proof_bytes",
4163 "max_members",
4164 "max_horizon",
4165 "max_product_states",
4166 "max_attestation_bytes",
4167 "refusal_exit",
4168 "backends",
4169 "routing",
4170 "fallback",
4171 "proof_failure",
4172 "attested_verification",
4173 "accounting",
4174 "timing_calibration",
4175 "result_on_refusal",
4176 "refusal_schema",
4177 "unsupported",
4178 ];
4179 if fields.len() != keys.len() {
4180 return Err(PredicateApiError::InvalidResponse(
4181 "controller proof MTBDD portfolio capability field count is invalid".to_string(),
4182 ));
4183 }
4184 let values = fields
4185 .iter()
4186 .zip(keys)
4187 .map(|(field, key)| token_value(field, key))
4188 .collect::<Result<Vec<_>, _>>()?;
4189 if values[17..]
4190 != [
4191 "proof-mtbdd,direct-exact",
4192 "static",
4193 "exact",
4194 "fail-closed",
4195 "required",
4196 "conservative-static",
4197 "none",
4198 "none",
4199 "proof-reason-v1",
4200 "fail-closed",
4201 ]
4202 {
4203 return Err(PredicateApiError::IncompatibleContract(
4204 "controller proof MTBDD portfolio contract is unsupported".to_string(),
4205 ));
4206 }
4207 let versions = values[..8]
4208 .iter()
4209 .enumerate()
4210 .map(|(index, value)| canonical_u32(value, keys[index]))
4211 .collect::<Result<Vec<_>, _>>()?;
4212 let limits = values[8..16]
4213 .iter()
4214 .enumerate()
4215 .map(|(index, value)| canonical_usize(value, keys[index + 8]))
4216 .collect::<Result<Vec<_>, _>>()?;
4217 let refusal_exit = canonical_usize(values[16], keys[16])?;
4218 if versions != [1, 1, 1, 1, 1, 1, 1, 1] {
4219 return Err(PredicateApiError::IncompatibleContract(
4220 "controller proof MTBDD portfolio version tuple is unsupported".to_string(),
4221 ));
4222 }
4223 if limits.contains(&0) || refusal_exit != 3 {
4224 return Err(PredicateApiError::InvalidResponse(
4225 "controller proof MTBDD portfolio discovered limit must be positive".to_string(),
4226 ));
4227 }
4228 Ok(ControllerProofMtbddPortfolioCapabilities {
4229 cli_version: versions[0],
4230 policy_version: versions[1],
4231 envelope_version: versions[2],
4232 artifact_version: versions[3],
4233 proof_artifact_version: versions[4],
4234 direct_artifact_version: versions[5],
4235 manifest_version: versions[6],
4236 source_model_attestation_version: versions[7],
4237 max_policy_bytes: limits[0],
4238 max_artifact_bytes: limits[1],
4239 max_equivalence_artifact_bytes: limits[2],
4240 max_unsat_proof_bytes: limits[3],
4241 max_members: limits[4],
4242 max_horizon: limits[5],
4243 max_product_states: limits[6],
4244 max_attestation_bytes: limits[7],
4245 refusal_exit_code: 3,
4246 })
4247}
4248
4249fn classify_controller_proof_mtbdd_resource_refusal(
4250 mut failure: PredicateOperationError,
4251) -> PredicateOperationError {
4252 let reason = match failure.error.as_ref() {
4253 PredicateApiError::CommandFailed {
4254 exit_code: Some(3),
4255 stderr,
4256 } => stderr
4257 .trim_end_matches(['\r', '\n'])
4258 .strip_prefix("error: controller-proof-mtbdd-resource refusal=")
4259 .and_then(|value| value.strip_suffix(" result=none"))
4260 .and_then(|value| match value {
4261 "artifact-bytes" => Some(ControllerPlantResourceRefusalReason::ArtifactBytes),
4262 "equivalence-artifact-bytes" => {
4263 Some(ControllerPlantResourceRefusalReason::EquivalenceArtifactBytes)
4264 }
4265 "unsat-proof-bytes" => Some(ControllerPlantResourceRefusalReason::UnsatProofBytes),
4266 "members" => Some(ControllerPlantResourceRefusalReason::Members),
4267 "horizon" => Some(ControllerPlantResourceRefusalReason::Horizon),
4268 "product-states" => Some(ControllerPlantResourceRefusalReason::ProductStates),
4269 "transition-evaluations" => {
4270 Some(ControllerPlantResourceRefusalReason::TransitionEvaluations)
4271 }
4272 _ => None,
4273 }),
4274 _ => None,
4275 };
4276 if let Some(reason) = reason {
4277 let error = PredicateApiError::ResourceRefused { reason };
4278 failure.metrics.status = InvocationStatus::Failed(error.failure_class());
4279 failure.error = Box::new(error);
4280 }
4281 failure
4282}
4283
4284fn parse_controller_plant_resource_capabilities(
4285 line: &str,
4286) -> Result<ControllerPlantResourceCapabilities, PredicateApiError> {
4287 if line.contains('\r') || !line.ends_with('\n') || line.lines().count() != 1 {
4288 return Err(PredicateApiError::InvalidResponse(
4289 "controller plant resource capability line is not canonical".to_string(),
4290 ));
4291 }
4292 let fields = line.trim_end_matches('\n').split(' ').collect::<Vec<_>>();
4293 let keys = [
4294 "controller_plant_resource_cli_version",
4295 "policy_version",
4296 "envelope_version",
4297 "manifest_version",
4298 "portfolio_artifact_version",
4299 "max_policy_bytes",
4300 "max_artifact_bytes",
4301 "max_members",
4302 "max_horizon",
4303 "max_product_states",
4304 "refusal_exit",
4305 "accounting",
4306 "timing_calibration",
4307 "result_on_refusal",
4308 "refusal_schema",
4309 "unsupported",
4310 ];
4311 if fields.len() != keys.len() {
4312 return Err(PredicateApiError::InvalidResponse(
4313 "controller plant resource capability field count is invalid".to_string(),
4314 ));
4315 }
4316 let values = fields
4317 .iter()
4318 .zip(keys)
4319 .map(|(field, key)| token_value(field, key))
4320 .collect::<Result<Vec<_>, _>>()?;
4321 if values[11..]
4322 != [
4323 "conservative-static",
4324 "none",
4325 "none",
4326 "reason-v1",
4327 "fail-closed",
4328 ]
4329 {
4330 return Err(PredicateApiError::IncompatibleContract(
4331 "controller plant resource accounting contract is unsupported".to_string(),
4332 ));
4333 }
4334 let versions = values[..5]
4335 .iter()
4336 .enumerate()
4337 .map(|(index, value)| canonical_u32(value, keys[index]))
4338 .collect::<Result<Vec<_>, _>>()?;
4339 let limits = values[5..10]
4340 .iter()
4341 .enumerate()
4342 .map(|(index, value)| canonical_usize(value, keys[index + 5]))
4343 .collect::<Result<Vec<_>, _>>()?;
4344 let refusal_exit = canonical_usize(values[10], keys[10])?;
4345 if versions != [1, 1, 1, 1, 1] {
4346 return Err(PredicateApiError::IncompatibleContract(
4347 "controller plant resource version tuple is unsupported".to_string(),
4348 ));
4349 }
4350 if limits.contains(&0) || refusal_exit != 3 {
4351 return Err(PredicateApiError::InvalidResponse(
4352 "controller plant resource discovered limit must be positive".to_string(),
4353 ));
4354 }
4355 Ok(ControllerPlantResourceCapabilities {
4356 cli_version: versions[0],
4357 policy_version: versions[1],
4358 envelope_version: versions[2],
4359 manifest_version: versions[3],
4360 portfolio_artifact_version: versions[4],
4361 max_policy_bytes: limits[0],
4362 max_artifact_bytes: limits[1],
4363 max_members: limits[2],
4364 max_horizon: limits[3],
4365 max_product_states: limits[4],
4366 refusal_exit_code: 3,
4367 })
4368}
4369
4370fn classify_controller_plant_resource_refusal(
4371 mut failure: PredicateOperationError,
4372) -> PredicateOperationError {
4373 let reason = match failure.error.as_ref() {
4374 PredicateApiError::CommandFailed {
4375 exit_code: Some(3),
4376 stderr,
4377 } => stderr
4378 .trim_end_matches(['\r', '\n'])
4379 .strip_prefix("error: controller-plant-resource refusal=")
4380 .and_then(|value| value.strip_suffix(" result=none"))
4381 .and_then(|value| match value {
4382 "artifact-bytes" => Some(ControllerPlantResourceRefusalReason::ArtifactBytes),
4383 "members" => Some(ControllerPlantResourceRefusalReason::Members),
4384 "horizon" => Some(ControllerPlantResourceRefusalReason::Horizon),
4385 "product-states" => Some(ControllerPlantResourceRefusalReason::ProductStates),
4386 "transition-evaluations" => {
4387 Some(ControllerPlantResourceRefusalReason::TransitionEvaluations)
4388 }
4389 _ => None,
4390 }),
4391 _ => None,
4392 };
4393 if let Some(reason) = reason {
4394 let error = PredicateApiError::ResourceRefused { reason };
4395 failure.metrics.status = InvocationStatus::Failed(error.failure_class());
4396 failure.error = Box::new(error);
4397 }
4398 failure
4399}
4400
4401fn parse_controller_plant_resource_summary(
4402 output: ManagedOutput,
4403 capabilities: &ControllerPlantResourceCapabilities,
4404) -> Result<Observed<ControllerPlantResourceSummary>, PredicateOperationError> {
4405 let (stdout, mut metrics) = successful_stdout(output)?;
4406 let parsed = (|| -> Result<ControllerPlantResourceSummary, PredicateApiError> {
4407 if stdout.contains('\r') || !stdout.ends_with('\n') {
4408 return Err(PredicateApiError::InvalidResponse(
4409 "controller plant resource response is not canonical LF text".to_string(),
4410 ));
4411 }
4412 let mut lines = stdout.lines();
4413 let first = lines.next().ok_or_else(|| {
4414 PredicateApiError::InvalidResponse(
4415 "controller plant resource summary line is missing".to_string(),
4416 )
4417 })?;
4418 let fields = first.split(' ').collect::<Vec<_>>();
4419 let keys = [
4420 "controller-plant-resource",
4421 "status",
4422 "cli_version",
4423 "policy_version",
4424 "envelope_version",
4425 "artifact_version",
4426 "backend",
4427 "members",
4428 "maximum_member_horizon",
4429 "maximum_product_states",
4430 "transition_evaluation_bound",
4431 "safe",
4432 "unsafe",
4433 "reachable_product_states",
4434 "explored_transitions",
4435 "artifact_bytes",
4436 "load_micros",
4437 "artifact_micros",
4438 "verification_micros",
4439 "elapsed_micros",
4440 ];
4441 if fields.len() != keys.len()
4442 || fields[0] != keys[0]
4443 || token_value(fields[1], "status")? != "VERIFIED"
4444 {
4445 return Err(PredicateApiError::InvalidResponse(
4446 "controller plant resource summary shape is invalid".to_string(),
4447 ));
4448 }
4449 let cli_version = canonical_u32(token_value(fields[2], keys[2])?, keys[2])?;
4450 let policy_version = canonical_u32(token_value(fields[3], keys[3])?, keys[3])?;
4451 let envelope_version = canonical_u32(token_value(fields[4], keys[4])?, keys[4])?;
4452 let artifact_version = canonical_u32(token_value(fields[5], keys[5])?, keys[5])?;
4453 if cli_version != capabilities.cli_version
4454 || policy_version != capabilities.policy_version
4455 || envelope_version != capabilities.envelope_version
4456 || artifact_version != capabilities.portfolio_artifact_version
4457 {
4458 return Err(PredicateApiError::IncompatibleContract(
4459 "controller plant resource response version changed".to_string(),
4460 ));
4461 }
4462 let backend = match token_value(fields[6], "backend")? {
4463 "MTBDD" => ControllerPlantPortfolioBackend::Mtbdd,
4464 "DIRECT_EXACT" => ControllerPlantPortfolioBackend::DirectExact,
4465 _ => {
4466 return Err(PredicateApiError::InvalidResponse(
4467 "controller plant resource backend is invalid".to_string(),
4468 ));
4469 }
4470 };
4471 let numeric = fields[7..]
4472 .iter()
4473 .zip(&keys[7..])
4474 .map(|(field, key)| canonical_usize(token_value(field, key)?, key))
4475 .collect::<Result<Vec<_>, _>>()?;
4476 let member_count = numeric[0];
4477 if member_count == 0
4478 || member_count > capabilities.max_members
4479 || numeric[1] > capabilities.max_horizon
4480 || numeric[2] > capabilities.max_product_states
4481 || numeric[8] > capabilities.max_artifact_bytes
4482 || numeric[4].checked_add(numeric[5]) != Some(member_count)
4483 {
4484 return Err(PredicateApiError::InvalidResponse(
4485 "controller plant resource response exceeds discovered limits".to_string(),
4486 ));
4487 }
4488 let mut member_results = Vec::with_capacity(member_count);
4489 for expected_index in 0..member_count {
4490 let line = lines.next().ok_or_else(|| {
4491 PredicateApiError::InvalidResponse(
4492 "controller plant resource member line is missing".to_string(),
4493 )
4494 })?;
4495 let fields = line.split(' ').collect::<Vec<_>>();
4496 let keys = [
4497 "controller-plant-resource-member",
4498 "index",
4499 "answer",
4500 "horizon",
4501 "bad_frame",
4502 "trace_steps",
4503 "reachable_product_states",
4504 "explored_transitions",
4505 ];
4506 if fields.len() != keys.len() || fields[0] != keys[0] {
4507 return Err(PredicateApiError::InvalidResponse(
4508 "controller plant resource member shape is invalid".to_string(),
4509 ));
4510 }
4511 let index = canonical_usize(token_value(fields[1], "index")?, "index")?;
4512 if index != expected_index {
4513 return Err(PredicateApiError::InvalidResponse(
4514 "controller plant resource member ordering changed".to_string(),
4515 ));
4516 }
4517 let answer = match token_value(fields[2], "answer")? {
4518 "SAFE" => ControllerMtbddAnswer::Safe,
4519 "UNSAFE" => ControllerMtbddAnswer::Unsafe,
4520 _ => {
4521 return Err(PredicateApiError::InvalidResponse(
4522 "controller plant resource member answer is invalid".to_string(),
4523 ));
4524 }
4525 };
4526 let horizon = canonical_usize(token_value(fields[3], "horizon")?, "horizon")?;
4527 let bad_frame = match token_value(fields[4], "bad_frame")? {
4528 "none" => None,
4529 value => Some(canonical_usize(value, "bad_frame")?),
4530 };
4531 let values = fields[5..]
4532 .iter()
4533 .zip(&keys[5..])
4534 .map(|(field, key)| canonical_usize(token_value(field, key)?, key))
4535 .collect::<Result<Vec<_>, _>>()?;
4536 if horizon > capabilities.max_horizon
4537 || match (answer, bad_frame) {
4538 (ControllerMtbddAnswer::Safe, None) => values[0] != 0,
4539 (ControllerMtbddAnswer::Unsafe, Some(frame)) => {
4540 frame > horizon || values[0] != frame.saturating_add(1)
4541 }
4542 _ => true,
4543 }
4544 {
4545 return Err(PredicateApiError::InvalidResponse(
4546 "controller plant resource member result is inconsistent".to_string(),
4547 ));
4548 }
4549 member_results.push(ControllerMtbddMemberResult {
4550 index,
4551 answer,
4552 horizon,
4553 bad_frame,
4554 trace_steps: values[0],
4555 reachable_product_states: values[1],
4556 explored_transitions: values[2],
4557 });
4558 }
4559 let reachable_total = member_results.iter().try_fold(0usize, |total, member| {
4560 total.checked_add(member.reachable_product_states)
4561 });
4562 let transition_total = member_results.iter().try_fold(0usize, |total, member| {
4563 total.checked_add(member.explored_transitions)
4564 });
4565 if lines.next().is_some()
4566 || member_results
4567 .iter()
4568 .filter(|member| matches!(member.answer, ControllerMtbddAnswer::Safe))
4569 .count()
4570 != numeric[4]
4571 || member_results
4572 .iter()
4573 .filter(|member| matches!(member.answer, ControllerMtbddAnswer::Unsafe))
4574 .count()
4575 != numeric[5]
4576 || reachable_total != Some(numeric[6])
4577 || transition_total != Some(numeric[7])
4578 {
4579 return Err(PredicateApiError::InvalidResponse(
4580 "controller plant resource member totals disagree".to_string(),
4581 ));
4582 }
4583 Ok(ControllerPlantResourceSummary {
4584 policy_version,
4585 envelope_version,
4586 artifact_version,
4587 backend,
4588 members: member_count,
4589 maximum_member_horizon: numeric[1],
4590 maximum_product_states: numeric[2],
4591 transition_evaluation_bound: numeric[3],
4592 safe: numeric[4],
4593 unsafe_count: numeric[5],
4594 reachable_product_states: numeric[6],
4595 explored_transitions: numeric[7],
4596 artifact_bytes: numeric[8],
4597 load_micros: numeric[9],
4598 artifact_micros: numeric[10],
4599 verification_micros: numeric[11],
4600 elapsed_micros: numeric[12],
4601 member_results,
4602 })
4603 })();
4604 match parsed {
4605 Ok(value) => Ok(Observed { value, metrics }),
4606 Err(error) => {
4607 metrics.status = InvocationStatus::Failed(error.failure_class());
4608 Err(PredicateOperationError {
4609 error: Box::new(error),
4610 metrics,
4611 })
4612 }
4613 }
4614}
4615
4616fn parse_controller_proof_mtbdd_resource_summary(
4617 output: ManagedOutput,
4618 capabilities: &ControllerProofMtbddResourceCapabilities,
4619) -> Result<Observed<ControllerProofMtbddResourceSummary>, PredicateOperationError> {
4620 let (stdout, mut metrics) = successful_stdout(output)?;
4621 let parsed = (|| -> Result<ControllerProofMtbddResourceSummary, PredicateApiError> {
4622 if stdout.contains('\r') || !stdout.ends_with('\n') {
4623 return Err(PredicateApiError::InvalidResponse(
4624 "controller proof MTBDD resource response is not canonical LF text".to_string(),
4625 ));
4626 }
4627 let mut lines = stdout.lines();
4628 let first = lines.next().ok_or_else(|| {
4629 PredicateApiError::InvalidResponse(
4630 "controller proof MTBDD resource summary line is missing".to_string(),
4631 )
4632 })?;
4633 let fields = first.split(' ').collect::<Vec<_>>();
4634 let keys = [
4635 "controller-proof-mtbdd-resource",
4636 "status",
4637 "cli_version",
4638 "policy_version",
4639 "envelope_version",
4640 "artifact_version",
4641 "members",
4642 "maximum_member_horizon",
4643 "maximum_product_states",
4644 "transition_evaluation_bound",
4645 "equivalence_artifact_bytes",
4646 "unsat_proof_bytes",
4647 "safe",
4648 "unsafe",
4649 "reachable_product_states",
4650 "explored_transitions",
4651 "artifact_bytes",
4652 "assignments_checked",
4653 "load_micros",
4654 "artifact_micros",
4655 "verification_micros",
4656 "elapsed_micros",
4657 ];
4658 if fields.len() != keys.len()
4659 || fields[0] != keys[0]
4660 || token_value(fields[1], "status")? != "VERIFIED"
4661 {
4662 return Err(PredicateApiError::InvalidResponse(
4663 "controller proof MTBDD resource summary shape is invalid".to_string(),
4664 ));
4665 }
4666 let cli_version = canonical_u32(token_value(fields[2], keys[2])?, keys[2])?;
4667 let policy_version = canonical_u32(token_value(fields[3], keys[3])?, keys[3])?;
4668 let envelope_version = canonical_u32(token_value(fields[4], keys[4])?, keys[4])?;
4669 let artifact_version = canonical_u32(token_value(fields[5], keys[5])?, keys[5])?;
4670 if cli_version != capabilities.cli_version
4671 || policy_version != capabilities.policy_version
4672 || envelope_version != capabilities.envelope_version
4673 || artifact_version != capabilities.artifact_version
4674 {
4675 return Err(PredicateApiError::IncompatibleContract(
4676 "controller proof MTBDD resource response version changed".to_string(),
4677 ));
4678 }
4679 let numeric = fields[6..]
4680 .iter()
4681 .zip(&keys[6..])
4682 .map(|(field, key)| canonical_usize(token_value(field, key)?, key))
4683 .collect::<Result<Vec<_>, _>>()?;
4684 let member_count = numeric[0];
4685 if member_count == 0
4686 || member_count > capabilities.max_members
4687 || numeric[1] > capabilities.max_horizon
4688 || numeric[2] > capabilities.max_product_states
4689 || numeric[4] > capabilities.max_equivalence_artifact_bytes
4690 || numeric[5] > capabilities.max_unsat_proof_bytes
4691 || numeric[10] > capabilities.max_artifact_bytes
4692 || numeric[11] != 0
4693 || numeric[6].checked_add(numeric[7]) != Some(member_count)
4694 {
4695 return Err(PredicateApiError::InvalidResponse(
4696 "controller proof MTBDD resource response exceeds discovered limits".to_string(),
4697 ));
4698 }
4699 let mut member_results = Vec::with_capacity(member_count);
4700 for expected_index in 0..member_count {
4701 let line = lines.next().ok_or_else(|| {
4702 PredicateApiError::InvalidResponse(
4703 "controller proof MTBDD resource member line is missing".to_string(),
4704 )
4705 })?;
4706 let fields = line.split(' ').collect::<Vec<_>>();
4707 let keys = [
4708 "controller-proof-mtbdd-resource-member",
4709 "index",
4710 "answer",
4711 "horizon",
4712 "bad_frame",
4713 "trace_steps",
4714 "reachable_product_states",
4715 "explored_transitions",
4716 ];
4717 if fields.len() != keys.len() || fields[0] != keys[0] {
4718 return Err(PredicateApiError::InvalidResponse(
4719 "controller proof MTBDD resource member shape is invalid".to_string(),
4720 ));
4721 }
4722 let index = canonical_usize(token_value(fields[1], "index")?, "index")?;
4723 if index != expected_index {
4724 return Err(PredicateApiError::InvalidResponse(
4725 "controller proof MTBDD resource member ordering changed".to_string(),
4726 ));
4727 }
4728 let answer = match token_value(fields[2], "answer")? {
4729 "SAFE" => ControllerMtbddAnswer::Safe,
4730 "UNSAFE" => ControllerMtbddAnswer::Unsafe,
4731 _ => {
4732 return Err(PredicateApiError::InvalidResponse(
4733 "controller proof MTBDD resource member answer is invalid".to_string(),
4734 ));
4735 }
4736 };
4737 let horizon = canonical_usize(token_value(fields[3], "horizon")?, "horizon")?;
4738 let bad_frame = match token_value(fields[4], "bad_frame")? {
4739 "none" => None,
4740 value => Some(canonical_usize(value, "bad_frame")?),
4741 };
4742 let values = fields[5..]
4743 .iter()
4744 .zip(&keys[5..])
4745 .map(|(field, key)| canonical_usize(token_value(field, key)?, key))
4746 .collect::<Result<Vec<_>, _>>()?;
4747 if horizon > capabilities.max_horizon
4748 || match (answer, bad_frame) {
4749 (ControllerMtbddAnswer::Safe, None) => values[0] != 0,
4750 (ControllerMtbddAnswer::Unsafe, Some(frame)) => {
4751 frame > horizon || values[0] != frame.saturating_add(1)
4752 }
4753 _ => true,
4754 }
4755 {
4756 return Err(PredicateApiError::InvalidResponse(
4757 "controller proof MTBDD resource member result is inconsistent".to_string(),
4758 ));
4759 }
4760 member_results.push(ControllerMtbddMemberResult {
4761 index,
4762 answer,
4763 horizon,
4764 bad_frame,
4765 trace_steps: values[0],
4766 reachable_product_states: values[1],
4767 explored_transitions: values[2],
4768 });
4769 }
4770 let safe = member_results
4771 .iter()
4772 .filter(|member| matches!(member.answer, ControllerMtbddAnswer::Safe))
4773 .count();
4774 let unsafe_count = member_results.len() - safe;
4775 let reachable = member_results.iter().try_fold(0usize, |total, member| {
4776 total.checked_add(member.reachable_product_states)
4777 });
4778 let explored = member_results.iter().try_fold(0usize, |total, member| {
4779 total.checked_add(member.explored_transitions)
4780 });
4781 if lines.next().is_some()
4782 || safe != numeric[6]
4783 || unsafe_count != numeric[7]
4784 || reachable != Some(numeric[8])
4785 || explored != Some(numeric[9])
4786 {
4787 return Err(PredicateApiError::InvalidResponse(
4788 "controller proof MTBDD resource member totals disagree".to_string(),
4789 ));
4790 }
4791 Ok(ControllerProofMtbddResourceSummary {
4792 policy_version,
4793 envelope_version,
4794 artifact_version,
4795 members: member_count,
4796 maximum_member_horizon: numeric[1],
4797 maximum_product_states: numeric[2],
4798 transition_evaluation_bound: numeric[3],
4799 equivalence_artifact_bytes: numeric[4],
4800 unsat_proof_bytes: numeric[5],
4801 safe: numeric[6],
4802 unsafe_count: numeric[7],
4803 reachable_product_states: numeric[8],
4804 explored_transitions: numeric[9],
4805 artifact_bytes: numeric[10],
4806 assignments_checked: numeric[11],
4807 load_micros: numeric[12],
4808 artifact_micros: numeric[13],
4809 verification_micros: numeric[14],
4810 elapsed_micros: numeric[15],
4811 member_results,
4812 })
4813 })();
4814 match parsed {
4815 Ok(value) => Ok(Observed { value, metrics }),
4816 Err(error) => {
4817 metrics.status = InvocationStatus::Failed(error.failure_class());
4818 Err(PredicateOperationError {
4819 error: Box::new(error),
4820 metrics,
4821 })
4822 }
4823 }
4824}
4825
4826fn parse_controller_proof_mtbdd_portfolio_resource_summary(
4827 output: ManagedOutput,
4828 capabilities: &ControllerProofMtbddPortfolioCapabilities,
4829 require_attestation: bool,
4830) -> Result<Observed<ControllerProofMtbddPortfolioResourceSummary>, PredicateOperationError> {
4831 let (stdout, mut metrics) = successful_stdout(output)?;
4832 let parsed = (|| -> Result<ControllerProofMtbddPortfolioResourceSummary, PredicateApiError> {
4833 if stdout.contains('\r') || !stdout.ends_with('\n') {
4834 return Err(PredicateApiError::InvalidResponse(
4835 "controller proof MTBDD portfolio response is not canonical LF text".to_string(),
4836 ));
4837 }
4838 let mut lines = stdout.lines();
4839 let first = lines.next().ok_or_else(|| {
4840 PredicateApiError::InvalidResponse(
4841 "controller proof MTBDD portfolio summary line is missing".to_string(),
4842 )
4843 })?;
4844 let fields = first.split(' ').collect::<Vec<_>>();
4845 let keys = [
4846 "controller-proof-mtbdd-portfolio-resource",
4847 "status",
4848 "cli_version",
4849 "policy_version",
4850 "envelope_version",
4851 "artifact_version",
4852 "backend",
4853 "reason",
4854 "members",
4855 "maximum_member_horizon",
4856 "maximum_product_states",
4857 "transition_evaluation_bound",
4858 "equivalence_artifact_bytes",
4859 "unsat_proof_bytes",
4860 "safe",
4861 "unsafe",
4862 "reachable_product_states",
4863 "explored_transitions",
4864 "artifact_bytes",
4865 "assignments_checked",
4866 "load_micros",
4867 "artifact_micros",
4868 "verification_micros",
4869 "elapsed_micros",
4870 ];
4871 let attested = fields.len() == keys.len() + 5;
4872 if attested != require_attestation
4873 || (!attested && fields.len() != keys.len())
4874 || fields[0] != keys[0]
4875 || token_value(fields[1], "status")? != "VERIFIED"
4876 {
4877 return Err(PredicateApiError::InvalidResponse(
4878 "controller proof MTBDD portfolio summary shape is invalid".to_string(),
4879 ));
4880 }
4881 if attested {
4882 let attestation_keys = [
4883 "source_model_attestation_version",
4884 "source_model_members",
4885 "source_model_tool",
4886 "source_model_tool_revision",
4887 "provenance",
4888 ];
4889 let attestation_fields = &fields[keys.len()..];
4890 let version = canonical_u32(
4891 token_value(attestation_fields[0], attestation_keys[0])?,
4892 attestation_keys[0],
4893 )?;
4894 let members = canonical_usize(
4895 token_value(attestation_fields[1], attestation_keys[1])?,
4896 attestation_keys[1],
4897 )?;
4898 let tool = token_value(attestation_fields[2], attestation_keys[2])?;
4899 let revision = token_value(attestation_fields[3], attestation_keys[3])?;
4900 let provenance = token_value(attestation_fields[4], attestation_keys[4])?;
4901 if version != capabilities.source_model_attestation_version
4902 || members == 0
4903 || members > capabilities.max_members.saturating_add(1)
4904 || tool != "yosys"
4905 || revision.len() != 40
4906 || !revision
4907 .bytes()
4908 .all(|byte| byte.is_ascii_digit() || (b'a'..=b'f').contains(&byte))
4909 || provenance != "BOUND"
4910 {
4911 return Err(PredicateApiError::InvalidResponse(
4912 "controller proof MTBDD portfolio attestation response is invalid".to_string(),
4913 ));
4914 }
4915 }
4916 let cli_version = canonical_u32(token_value(fields[2], keys[2])?, keys[2])?;
4917 let policy_version = canonical_u32(token_value(fields[3], keys[3])?, keys[3])?;
4918 let envelope_version = canonical_u32(token_value(fields[4], keys[4])?, keys[4])?;
4919 let artifact_version = canonical_u32(token_value(fields[5], keys[5])?, keys[5])?;
4920 if cli_version != capabilities.cli_version
4921 || policy_version != capabilities.policy_version
4922 || envelope_version != capabilities.envelope_version
4923 || artifact_version != capabilities.artifact_version
4924 {
4925 return Err(PredicateApiError::IncompatibleContract(
4926 "controller proof MTBDD portfolio response version changed".to_string(),
4927 ));
4928 }
4929 let backend = match token_value(fields[6], keys[6])? {
4930 "PROOF_MTBDD" => ControllerPlantPortfolioBackend::Mtbdd,
4931 "DIRECT_EXACT" => ControllerPlantPortfolioBackend::DirectExact,
4932 _ => {
4933 return Err(PredicateApiError::InvalidResponse(
4934 "controller proof MTBDD portfolio backend is invalid".to_string(),
4935 ));
4936 }
4937 };
4938 let reason = match token_value(fields[7], keys[7])? {
4939 "MTBDD_ADMITTED" => ControllerPlantPortfolioReason::MtbddAdmitted,
4940 "BOUNDARY_LIMIT" => ControllerPlantPortfolioReason::BoundaryLimit,
4941 "TERMINAL_LIMIT" => ControllerPlantPortfolioReason::TerminalLimit,
4942 "NODE_LIMIT" => ControllerPlantPortfolioReason::NodeLimit,
4943 _ => {
4944 return Err(PredicateApiError::InvalidResponse(
4945 "controller proof MTBDD portfolio reason is invalid".to_string(),
4946 ));
4947 }
4948 };
4949 if matches!(backend, ControllerPlantPortfolioBackend::Mtbdd)
4950 != matches!(reason, ControllerPlantPortfolioReason::MtbddAdmitted)
4951 {
4952 return Err(PredicateApiError::InvalidResponse(
4953 "controller proof MTBDD portfolio route is inconsistent".to_string(),
4954 ));
4955 }
4956 let numeric = fields[8..keys.len()]
4957 .iter()
4958 .zip(&keys[8..])
4959 .map(|(field, key)| canonical_usize(token_value(field, key)?, key))
4960 .collect::<Result<Vec<_>, _>>()?;
4961 let member_count = numeric[0];
4962 if member_count == 0
4963 || member_count > capabilities.max_members
4964 || numeric[1] > capabilities.max_horizon
4965 || numeric[2] > capabilities.max_product_states
4966 || numeric[4] > capabilities.max_equivalence_artifact_bytes
4967 || numeric[5] > capabilities.max_unsat_proof_bytes
4968 || numeric[10] > capabilities.max_artifact_bytes
4969 || numeric[11] != 0
4970 || numeric[6].checked_add(numeric[7]) != Some(member_count)
4971 || (matches!(backend, ControllerPlantPortfolioBackend::DirectExact)
4972 && (numeric[4] != 0 || numeric[5] != 0))
4973 {
4974 return Err(PredicateApiError::InvalidResponse(
4975 "controller proof MTBDD portfolio response exceeds discovered limits".to_string(),
4976 ));
4977 }
4978 let mut member_results = Vec::with_capacity(member_count);
4979 for expected_index in 0..member_count {
4980 let line = lines.next().ok_or_else(|| {
4981 PredicateApiError::InvalidResponse(
4982 "controller proof MTBDD portfolio member line is missing".to_string(),
4983 )
4984 })?;
4985 let fields = line.split(' ').collect::<Vec<_>>();
4986 let keys = [
4987 "controller-proof-mtbdd-portfolio-resource-member",
4988 "index",
4989 "answer",
4990 "horizon",
4991 "bad_frame",
4992 "trace_steps",
4993 "reachable_product_states",
4994 "explored_transitions",
4995 ];
4996 if fields.len() != keys.len() || fields[0] != keys[0] {
4997 return Err(PredicateApiError::InvalidResponse(
4998 "controller proof MTBDD portfolio member shape is invalid".to_string(),
4999 ));
5000 }
5001 let index = canonical_usize(token_value(fields[1], keys[1])?, keys[1])?;
5002 if index != expected_index {
5003 return Err(PredicateApiError::InvalidResponse(
5004 "controller proof MTBDD portfolio member ordering changed".to_string(),
5005 ));
5006 }
5007 let answer = match token_value(fields[2], keys[2])? {
5008 "SAFE" => ControllerMtbddAnswer::Safe,
5009 "UNSAFE" => ControllerMtbddAnswer::Unsafe,
5010 _ => {
5011 return Err(PredicateApiError::InvalidResponse(
5012 "controller proof MTBDD portfolio member answer is invalid".to_string(),
5013 ));
5014 }
5015 };
5016 let horizon = canonical_usize(token_value(fields[3], keys[3])?, keys[3])?;
5017 let bad_frame = match token_value(fields[4], keys[4])? {
5018 "none" => None,
5019 value => Some(canonical_usize(value, keys[4])?),
5020 };
5021 let values = fields[5..]
5022 .iter()
5023 .zip(&keys[5..])
5024 .map(|(field, key)| canonical_usize(token_value(field, key)?, key))
5025 .collect::<Result<Vec<_>, _>>()?;
5026 if horizon > capabilities.max_horizon
5027 || match (answer, bad_frame) {
5028 (ControllerMtbddAnswer::Safe, None) => values[0] != 0,
5029 (ControllerMtbddAnswer::Unsafe, Some(frame)) => {
5030 frame > horizon || values[0] != frame.saturating_add(1)
5031 }
5032 _ => true,
5033 }
5034 {
5035 return Err(PredicateApiError::InvalidResponse(
5036 "controller proof MTBDD portfolio member result is inconsistent".to_string(),
5037 ));
5038 }
5039 member_results.push(ControllerMtbddMemberResult {
5040 index,
5041 answer,
5042 horizon,
5043 bad_frame,
5044 trace_steps: values[0],
5045 reachable_product_states: values[1],
5046 explored_transitions: values[2],
5047 });
5048 }
5049 let safe = member_results
5050 .iter()
5051 .filter(|member| matches!(member.answer, ControllerMtbddAnswer::Safe))
5052 .count();
5053 let unsafe_count = member_results.len() - safe;
5054 let reachable = member_results.iter().try_fold(0usize, |total, member| {
5055 total.checked_add(member.reachable_product_states)
5056 });
5057 let explored = member_results.iter().try_fold(0usize, |total, member| {
5058 total.checked_add(member.explored_transitions)
5059 });
5060 if lines.next().is_some()
5061 || safe != numeric[6]
5062 || unsafe_count != numeric[7]
5063 || reachable != Some(numeric[8])
5064 || explored != Some(numeric[9])
5065 {
5066 return Err(PredicateApiError::InvalidResponse(
5067 "controller proof MTBDD portfolio member totals disagree".to_string(),
5068 ));
5069 }
5070 Ok(ControllerProofMtbddPortfolioResourceSummary {
5071 policy_version,
5072 envelope_version,
5073 artifact_version,
5074 backend,
5075 reason,
5076 members: member_count,
5077 maximum_member_horizon: numeric[1],
5078 maximum_product_states: numeric[2],
5079 transition_evaluation_bound: numeric[3],
5080 equivalence_artifact_bytes: numeric[4],
5081 unsat_proof_bytes: numeric[5],
5082 safe: numeric[6],
5083 unsafe_count: numeric[7],
5084 reachable_product_states: numeric[8],
5085 explored_transitions: numeric[9],
5086 artifact_bytes: numeric[10],
5087 assignments_checked: numeric[11],
5088 load_micros: numeric[12],
5089 artifact_micros: numeric[13],
5090 verification_micros: numeric[14],
5091 elapsed_micros: numeric[15],
5092 member_results,
5093 })
5094 })();
5095 match parsed {
5096 Ok(value) => Ok(Observed { value, metrics }),
5097 Err(error) => {
5098 metrics.status = InvocationStatus::Failed(error.failure_class());
5099 Err(PredicateOperationError {
5100 error: Box::new(error),
5101 metrics,
5102 })
5103 }
5104 }
5105}
5106
5107fn canonical_node_list(nodes: &[u64]) -> Result<String, PredicateApiError> {
5108 if nodes.is_empty()
5109 || nodes
5110 .windows(2)
5111 .any(|pair| pair[0] == 0 || pair[0] >= pair[1])
5112 || nodes.last() == Some(&0)
5113 {
5114 return Err(PredicateApiError::InvalidPolicy(
5115 "revision impact output nodes must be nonempty, nonzero, unique, and strictly increasing"
5116 .to_string(),
5117 ));
5118 }
5119 Ok(nodes
5120 .iter()
5121 .map(u64::to_string)
5122 .collect::<Vec<_>>()
5123 .join(","))
5124}
5125
5126fn parse_revision_impact_capabilities(
5127 line: &str,
5128) -> Result<RevisionImpactCapabilities, PredicateApiError> {
5129 if line.contains('\r') || !line.ends_with('\n') || line.lines().count() != 1 {
5130 return Err(PredicateApiError::InvalidResponse(
5131 "revision impact capability line is not canonical".to_string(),
5132 ));
5133 }
5134 let fields = line.trim_end_matches('\n').split(' ').collect::<Vec<_>>();
5135 let keys = [
5136 "revision_impact_cli_version",
5137 "impact_version",
5138 "query_manifest_version",
5139 "max_query_manifest_bytes",
5140 "max_input_bytes",
5141 "max_evidence_bytes",
5142 "max_bundle_bytes",
5143 "max_atoms",
5144 "max_combinations",
5145 "max_queries",
5146 "semantics",
5147 "work_schema",
5148 "query_schema",
5149 "routing",
5150 "fallback",
5151 "unsupported",
5152 ];
5153 if fields.len() != keys.len() {
5154 return Err(PredicateApiError::InvalidResponse(
5155 "revision impact capability field count is invalid".to_string(),
5156 ));
5157 }
5158 let values = fields
5159 .iter()
5160 .zip(keys)
5161 .map(|(field, key)| token_value(field, key))
5162 .collect::<Result<Vec<_>, _>>()?;
5163 if values[10..]
5164 != [
5165 "exact-counterfactual-v1",
5166 "verification-v1",
5167 "transition-semantic-set-v1",
5168 "none",
5169 "none",
5170 "fail-closed",
5171 ]
5172 {
5173 return Err(PredicateApiError::IncompatibleContract(
5174 "revision impact semantic or fallback contract is unsupported".to_string(),
5175 ));
5176 }
5177 let versions = values[..3]
5178 .iter()
5179 .enumerate()
5180 .map(|(index, value)| canonical_u32(value, keys[index]))
5181 .collect::<Result<Vec<_>, _>>()?;
5182 let limits = values[3..10]
5183 .iter()
5184 .enumerate()
5185 .map(|(index, value)| canonical_usize(value, keys[index + 3]))
5186 .collect::<Result<Vec<_>, _>>()?;
5187 if versions != [2, 1, 1] {
5188 return Err(PredicateApiError::IncompatibleContract(
5189 "revision impact version tuple is unsupported".to_string(),
5190 ));
5191 }
5192 let expected = [
5193 16 * 1024,
5194 64 * 1024 * 1024,
5195 16 * 1024 * 1024,
5196 64 * 1024 * 1024,
5197 revision_impact::MAX_IMPACT_ATOMS,
5198 revision_impact::MAX_IMPACT_COMBINATIONS,
5199 revision_impact::MAX_IMPACT_QUERIES,
5200 ];
5201 if limits != expected {
5202 return Err(PredicateApiError::IncompatibleContract(
5203 "revision impact limit tuple is unsupported".to_string(),
5204 ));
5205 }
5206 Ok(RevisionImpactCapabilities {
5207 cli_version: versions[0],
5208 impact_version: versions[1],
5209 query_manifest_version: versions[2],
5210 max_query_manifest_bytes: limits[0],
5211 max_input_bytes: limits[1],
5212 max_evidence_bytes: limits[2],
5213 max_bundle_bytes: limits[3],
5214 max_atoms: limits[4],
5215 max_combinations: limits[5],
5216 max_queries: limits[6],
5217 })
5218}
5219
5220fn parse_btor2_channel_property_capabilities(
5221 line: &str,
5222) -> Result<Btor2ChannelPropertyCapabilities, PredicateApiError> {
5223 if line.contains('\r') || !line.ends_with('\n') || line.lines().count() != 1 {
5224 return Err(PredicateApiError::InvalidResponse(
5225 "BTOR2 channel property capability line is not canonical".to_string(),
5226 ));
5227 }
5228 let fields = line.trim_end_matches('\n').split(' ').collect::<Vec<_>>();
5229 let keys = [
5230 "btor2_channel_property_cli_version",
5231 "artifact_version",
5232 "query_manifest_version",
5233 "policy_version",
5234 "max_query_manifest_bytes",
5235 "max_policy_bytes",
5236 "max_model_bytes",
5237 "max_channels",
5238 "max_queries",
5239 "max_evidence_bytes",
5240 "max_artifact_bytes",
5241 "max_projected_work",
5242 "refusal_exit",
5243 "routing",
5244 "fallback",
5245 "result_on_refusal",
5246 "refusal_schema",
5247 "unsupported",
5248 "verification",
5249 ];
5250 if fields.len() != keys.len() {
5251 return Err(PredicateApiError::InvalidResponse(
5252 "BTOR2 channel property capability field count is invalid".to_string(),
5253 ));
5254 }
5255 let values = fields
5256 .iter()
5257 .zip(keys)
5258 .map(|(field, key)| token_value(field, key))
5259 .collect::<Result<Vec<_>, _>>()?;
5260 if values[13..]
5261 != [
5262 "static-explicit-or-bitblast",
5263 "exact",
5264 "none",
5265 "reason-v1",
5266 "fail-closed",
5267 "source-replay",
5268 ]
5269 {
5270 return Err(PredicateApiError::IncompatibleContract(
5271 "BTOR2 channel property route or refusal contract is unsupported".to_string(),
5272 ));
5273 }
5274 let versions = values[..4]
5275 .iter()
5276 .enumerate()
5277 .map(|(index, value)| canonical_u32(value, keys[index]))
5278 .collect::<Result<Vec<_>, _>>()?;
5279 if versions != [1, 1, 1, 1] {
5280 return Err(PredicateApiError::IncompatibleContract(
5281 "BTOR2 channel property version tuple is unsupported".to_string(),
5282 ));
5283 }
5284 let limits = values[4..11]
5285 .iter()
5286 .enumerate()
5287 .map(|(index, value)| canonical_usize(value, keys[index + 4]))
5288 .collect::<Result<Vec<_>, _>>()?;
5289 let max_projected_work = canonical_u64(values[11], keys[11])?;
5290 let refusal_exit = canonical_usize(values[12], keys[12])?;
5291 let expected = [
5292 256 * 1024,
5293 4096,
5294 btor2::MAX_BTOR2_BYTES,
5295 btor2_region_extract::MAX_REGION_CHANNELS,
5296 btor2_region_property::MAX_CHANNEL_PROPERTY_QUERIES,
5297 btor2_region_property::MAX_CHANNEL_PROPERTY_EVIDENCE_BYTES,
5298 btor2_region_property::MAX_CHANNEL_PROPERTY_ARTIFACT_BYTES,
5299 ];
5300 if limits != expected
5301 || max_projected_work != btor2_region_property::MAX_CHANNEL_PROPERTY_PROJECTED_WORK
5302 || refusal_exit != 3
5303 {
5304 return Err(PredicateApiError::IncompatibleContract(
5305 "BTOR2 channel property limit tuple is unsupported".to_string(),
5306 ));
5307 }
5308 Ok(Btor2ChannelPropertyCapabilities {
5309 cli_version: versions[0],
5310 artifact_version: versions[1],
5311 query_manifest_version: versions[2],
5312 policy_version: versions[3],
5313 max_query_manifest_bytes: limits[0],
5314 max_policy_bytes: limits[1],
5315 max_model_bytes: limits[2],
5316 max_channels: limits[3],
5317 max_queries: limits[4],
5318 max_evidence_bytes: limits[5],
5319 max_artifact_bytes: limits[6],
5320 max_projected_work,
5321 refusal_exit_code: 3,
5322 })
5323}
5324
5325fn parse_btor2_channel_property_observability_capabilities(
5326 line: &str,
5327 base: &Btor2ChannelPropertyCapabilities,
5328) -> Result<Btor2ChannelPropertyObservabilityCapabilities, PredicateApiError> {
5329 if line.contains('\r') || !line.ends_with('\n') || line.lines().count() != 1 {
5330 return Err(PredicateApiError::InvalidResponse(
5331 "BTOR2 channel property observability capability line is not canonical".to_string(),
5332 ));
5333 }
5334 let fields = line.trim_end_matches('\n').split(' ').collect::<Vec<_>>();
5335 let keys = [
5336 "btor2_channel_property_observability_cli_version",
5337 "base_cli_version",
5338 "phase_metrics_version",
5339 "phases",
5340 "timing_calibration",
5341 "correctness_dependency",
5342 "partial_metrics_on_failure",
5343 "unsupported",
5344 ];
5345 if fields.len() != keys.len() {
5346 return Err(PredicateApiError::InvalidResponse(
5347 "BTOR2 channel property observability capability field count is invalid".to_string(),
5348 ));
5349 }
5350 let values = fields
5351 .iter()
5352 .zip(keys)
5353 .map(|(field, key)| token_value(field, key))
5354 .collect::<Result<Vec<_>, _>>()?;
5355 let cli_version = canonical_u32(values[0], keys[0])?;
5356 let base_cli_version = canonical_u32(values[1], keys[1])?;
5357 let phase_metrics_version = canonical_u32(values[2], keys[2])?;
5358 if cli_version != 1
5359 || base_cli_version != base.cli_version
5360 || phase_metrics_version != 1
5361 || values[3]
5362 != "input,structural-admission,preflight,proof-construction,encoding,artifact-decode,source-replay,publication"
5363 || values[4..] != ["none", "none", "none", "fail-closed"]
5364 {
5365 return Err(PredicateApiError::IncompatibleContract(
5366 "BTOR2 channel property observability contract is unsupported".to_string(),
5367 ));
5368 }
5369 Ok(Btor2ChannelPropertyObservabilityCapabilities {
5370 cli_version,
5371 phase_metrics_version,
5372 })
5373}
5374
5375fn parse_btor2_channel_property_phase_line(
5376 line: &str,
5377 expected_operation: Btor2ChannelPropertyObservedOperation,
5378 capabilities: &Btor2ChannelPropertyObservabilityCapabilities,
5379) -> Result<Btor2ChannelPropertyPhaseMetrics, PredicateApiError> {
5380 if line.contains('\r') || !line.ends_with('\n') || line.lines().count() != 1 {
5381 return Err(PredicateApiError::InvalidResponse(
5382 "BTOR2 channel property phase row is not canonical".to_string(),
5383 ));
5384 }
5385 let fields = line.trim_end_matches('\n').split(' ').collect::<Vec<_>>();
5386 let keys = [
5387 "status",
5388 "observability_cli_version",
5389 "phase_metrics_version",
5390 "operation",
5391 "input_micros",
5392 "structural_admission_micros",
5393 "preflight_micros",
5394 "proof_construction_micros",
5395 "encoding_micros",
5396 "artifact_decode_micros",
5397 "source_replay_micros",
5398 "publication_micros",
5399 "total_micros",
5400 "timing_calibration",
5401 "correctness_dependency",
5402 ];
5403 if fields.len() != keys.len() + 1
5404 || fields.first().copied() != Some("btor2-channel-property-phases")
5405 {
5406 return Err(PredicateApiError::InvalidResponse(
5407 "BTOR2 channel property phase row shape is invalid".to_string(),
5408 ));
5409 }
5410 let values = fields[1..]
5411 .iter()
5412 .zip(keys)
5413 .map(|(field, key)| token_value(field, key))
5414 .collect::<Result<Vec<_>, _>>()?;
5415 let cli_version = canonical_u32(values[1], keys[1])?;
5416 let phase_metrics_version = canonical_u32(values[2], keys[2])?;
5417 let operation = match values[3] {
5418 "certify" => Btor2ChannelPropertyObservedOperation::Certify,
5419 "verify" => Btor2ChannelPropertyObservedOperation::Verify,
5420 _ => {
5421 return Err(PredicateApiError::InvalidResponse(
5422 "unknown BTOR2 channel property observed operation".to_string(),
5423 ));
5424 }
5425 };
5426 let timings = values[4..13]
5427 .iter()
5428 .enumerate()
5429 .map(|(index, value)| canonical_u128(value, keys[index + 4]))
5430 .collect::<Result<Vec<_>, _>>()?;
5431 let phase_sum = timings[..8]
5432 .iter()
5433 .try_fold(0u128, |total, value| total.checked_add(*value))
5434 .ok_or_else(|| {
5435 PredicateApiError::InvalidResponse(
5436 "BTOR2 channel property phase total overflows".to_string(),
5437 )
5438 })?;
5439 if values[0] != "MEASURED"
5440 || cli_version != capabilities.cli_version
5441 || phase_metrics_version != capabilities.phase_metrics_version
5442 || operation != expected_operation
5443 || phase_sum > timings[8]
5444 || values[13..] != ["none", "none"]
5445 || (operation == Btor2ChannelPropertyObservedOperation::Verify
5446 && [timings[1], timings[2], timings[3], timings[4], timings[7]] != [0; 5])
5447 {
5448 return Err(PredicateApiError::InvalidResponse(
5449 "BTOR2 channel property phase row violates the observed contract".to_string(),
5450 ));
5451 }
5452 Ok(Btor2ChannelPropertyPhaseMetrics {
5453 version: phase_metrics_version,
5454 operation,
5455 input_micros: timings[0],
5456 structural_admission_micros: timings[1],
5457 preflight_micros: timings[2],
5458 proof_construction_micros: timings[3],
5459 encoding_micros: timings[4],
5460 artifact_decode_micros: timings[5],
5461 source_replay_micros: timings[6],
5462 publication_micros: timings[7],
5463 total_micros: timings[8],
5464 })
5465}
5466
5467fn parse_btor2_channel_property_phase_observed_summary(
5468 mut output: ManagedOutput,
5469 expected_status: &str,
5470 expected_operation: Btor2ChannelPropertyObservedOperation,
5471 base_capabilities: &Btor2ChannelPropertyCapabilities,
5472 capabilities: &Btor2ChannelPropertyObservabilityCapabilities,
5473) -> Result<Observed<Btor2ChannelPropertyPhaseObservedSummary>, PredicateOperationError> {
5474 if !output.status.success() {
5475 return match parse_btor2_channel_property_summary(
5476 output,
5477 expected_status,
5478 base_capabilities,
5479 ) {
5480 Err(error) => Err(error),
5481 Ok(observed) => {
5482 let error = PredicateApiError::InvalidResponse(
5483 "non-successful observed process produced a successful summary".to_string(),
5484 );
5485 let mut metrics = observed.metrics;
5486 metrics.status = InvocationStatus::Failed(error.failure_class());
5487 Err(PredicateOperationError {
5488 error: Box::new(error),
5489 metrics,
5490 })
5491 }
5492 };
5493 }
5494 let phase_start = output.stdout[..output.stdout.len().saturating_sub(1)]
5495 .iter()
5496 .rposition(|byte| *byte == b'\n')
5497 .map_or(0, |index| index + 1);
5498 let phase_bytes = output.stdout.split_off(phase_start);
5499 let phase_line = String::from_utf8(phase_bytes).map_err(|_| {
5500 let error = PredicateApiError::InvalidResponse(
5501 "BTOR2 channel property phase row is not UTF-8".to_string(),
5502 );
5503 output.metrics.status = InvocationStatus::Failed(error.failure_class());
5504 PredicateOperationError {
5505 error: Box::new(error),
5506 metrics: output.metrics.clone(),
5507 }
5508 })?;
5509 let phases =
5510 parse_btor2_channel_property_phase_line(&phase_line, expected_operation, capabilities)
5511 .map_err(|error| {
5512 output.metrics.status = InvocationStatus::Failed(error.failure_class());
5513 PredicateOperationError {
5514 error: Box::new(error),
5515 metrics: output.metrics.clone(),
5516 }
5517 })?;
5518 let verification =
5519 parse_btor2_channel_property_summary(output, expected_status, base_capabilities)?;
5520 Ok(Observed {
5521 value: Btor2ChannelPropertyPhaseObservedSummary {
5522 verification: verification.value,
5523 phases,
5524 },
5525 metrics: verification.metrics,
5526 })
5527}
5528
5529fn parse_btor2_channel_property_summary(
5530 mut output: ManagedOutput,
5531 expected_status: &str,
5532 capabilities: &Btor2ChannelPropertyCapabilities,
5533) -> Result<Observed<Btor2ChannelPropertyProcessSummary>, PredicateOperationError> {
5534 if output.status.code() == Some(capabilities.refusal_exit_code) {
5535 let stderr = String::from_utf8_lossy(&output.stderr);
5536 let reason = stderr
5537 .strip_prefix("error: btor2-channel-property-resource refusal=")
5538 .and_then(|value| value.strip_suffix(" result=none\n"))
5539 .and_then(|value| match value {
5540 "query-count" => Some(Btor2ChannelPropertyResourceRefusalReason::QueryCount),
5541 "member-count" => Some(Btor2ChannelPropertyResourceRefusalReason::MemberCount),
5542 "projected-work" => Some(Btor2ChannelPropertyResourceRefusalReason::ProjectedWork),
5543 "evidence-bytes" => Some(Btor2ChannelPropertyResourceRefusalReason::EvidenceBytes),
5544 "artifact-bytes" => Some(Btor2ChannelPropertyResourceRefusalReason::ArtifactBytes),
5545 _ => None,
5546 });
5547 if !output.stdout.is_empty() {
5548 let error = PredicateApiError::InvalidResponse(
5549 "BTOR2 channel property refusal emitted stdout".to_string(),
5550 );
5551 output.metrics.status = InvocationStatus::Failed(error.failure_class());
5552 return Err(PredicateOperationError {
5553 error: Box::new(error),
5554 metrics: output.metrics,
5555 });
5556 }
5557 let Some(reason) = reason else {
5558 let error = PredicateApiError::InvalidResponse(
5559 "BTOR2 channel property refusal is malformed".to_string(),
5560 );
5561 output.metrics.status = InvocationStatus::Failed(error.failure_class());
5562 return Err(PredicateOperationError {
5563 error: Box::new(error),
5564 metrics: output.metrics,
5565 });
5566 };
5567 let error = PredicateApiError::Btor2ChannelPropertyResourceRefused { reason };
5568 output.metrics.status = InvocationStatus::Failed(error.failure_class());
5569 return Err(PredicateOperationError {
5570 error: Box::new(error),
5571 metrics: output.metrics,
5572 });
5573 }
5574 let (stdout, mut metrics) = successful_stdout(output)?;
5575 let parsed = (|| -> Result<Btor2ChannelPropertyProcessSummary, PredicateApiError> {
5576 if stdout.contains('\r') || !stdout.ends_with('\n') {
5577 return Err(PredicateApiError::InvalidResponse(
5578 "BTOR2 channel property response is not canonical LF text".to_string(),
5579 ));
5580 }
5581 let lines = stdout.lines().collect::<Vec<_>>();
5582 let fields = lines
5583 .first()
5584 .ok_or_else(|| {
5585 PredicateApiError::InvalidResponse(
5586 "BTOR2 channel property response is empty".to_string(),
5587 )
5588 })?
5589 .split(' ')
5590 .collect::<Vec<_>>();
5591 let keys = [
5592 "status",
5593 "cli_version",
5594 "artifact_version",
5595 "channels",
5596 "logical_queries",
5597 "proof_members",
5598 "reused_queries",
5599 "explicit_members",
5600 "bitblast_members",
5601 "evidence_bytes",
5602 "artifact_bytes",
5603 "projected_work",
5604 "elapsed_micros",
5605 ];
5606 if fields.len() != keys.len() + 1
5607 || fields.first().copied() != Some("btor2-channel-properties")
5608 {
5609 return Err(PredicateApiError::InvalidResponse(
5610 "BTOR2 channel property summary shape is invalid".to_string(),
5611 ));
5612 }
5613 let values = fields[1..]
5614 .iter()
5615 .zip(keys)
5616 .map(|(field, key)| token_value(field, key))
5617 .collect::<Result<Vec<_>, _>>()?;
5618 if values[0] != expected_status {
5619 return Err(PredicateApiError::InvalidResponse(
5620 "BTOR2 channel property status differs from requested operation".to_string(),
5621 ));
5622 }
5623 let cli_version = canonical_u32(values[1], keys[1])?;
5624 let artifact_version = canonical_u32(values[2], keys[2])?;
5625 let numeric = values[3..11]
5626 .iter()
5627 .enumerate()
5628 .map(|(index, value)| canonical_usize(value, keys[index + 3]))
5629 .collect::<Result<Vec<_>, _>>()?;
5630 let projected_work = match values[11] {
5631 "not-applied" if expected_status == "VERIFIED" => None,
5632 value if expected_status == "CREATED" => Some(canonical_u64(value, keys[11])?),
5633 _ => {
5634 return Err(PredicateApiError::InvalidResponse(
5635 "BTOR2 channel property projected work differs from operation".to_string(),
5636 ));
5637 }
5638 };
5639 let elapsed_micros = canonical_usize(values[12], keys[12])?;
5640 if cli_version != capabilities.cli_version
5641 || artifact_version != capabilities.artifact_version
5642 || numeric[0] == 0
5643 || numeric[0] > capabilities.max_channels
5644 || numeric[1] == 0
5645 || numeric[1] > capabilities.max_queries
5646 || numeric[2] == 0
5647 || numeric[2] > numeric[1]
5648 || numeric[3].checked_add(numeric[2]) != Some(numeric[1])
5649 || numeric[4].checked_add(numeric[5]) != Some(numeric[2])
5650 || numeric[6] == 0
5651 || numeric[6] > capabilities.max_evidence_bytes
5652 || numeric[7] == 0
5653 || numeric[7] > capabilities.max_artifact_bytes
5654 || projected_work
5655 .is_some_and(|work| work == 0 || work > capabilities.max_projected_work)
5656 {
5657 return Err(PredicateApiError::InvalidResponse(
5658 "BTOR2 channel property summary violates advertised dimensions".to_string(),
5659 ));
5660 }
5661 if lines.len() != numeric[1] + 1 {
5662 return Err(PredicateApiError::InvalidResponse(
5663 "BTOR2 channel property result count is inconsistent".to_string(),
5664 ));
5665 }
5666 let mut results = Vec::with_capacity(numeric[1]);
5667 for (expected_index, line) in lines[1..].iter().enumerate() {
5668 let fields = line.split(' ').collect::<Vec<_>>();
5669 let result_keys = [
5670 "index",
5671 "query_id",
5672 "channel",
5673 "property",
5674 "horizon",
5675 "answer",
5676 "bad_frame",
5677 "backend",
5678 "solver",
5679 "representative_channel",
5680 "witness_valuations",
5681 ];
5682 if fields.len() != result_keys.len() + 1
5683 || fields.first().copied() != Some("btor2-channel-property")
5684 {
5685 return Err(PredicateApiError::InvalidResponse(
5686 "BTOR2 channel property result shape is invalid".to_string(),
5687 ));
5688 }
5689 let values = fields[1..]
5690 .iter()
5691 .zip(result_keys)
5692 .map(|(field, key)| token_value(field, key))
5693 .collect::<Result<Vec<_>, _>>()?;
5694 let index = canonical_usize(values[0], result_keys[0])?;
5695 let query_id = canonical_u32(values[1], result_keys[1])?;
5696 let channel = canonical_usize(values[2], result_keys[2])?;
5697 let property = match values[3] {
5698 "output-high" => Btor2ChannelPropertyKind::OutputHigh,
5699 "output-low" => Btor2ChannelPropertyKind::OutputLow,
5700 _ => {
5701 return Err(PredicateApiError::InvalidResponse(
5702 "unknown BTOR2 channel property kind".to_string(),
5703 ));
5704 }
5705 };
5706 let horizon = canonical_u32(values[4], result_keys[4])?;
5707 let answer = match values[5] {
5708 "SAFE" => Btor2ChannelPropertyAnswer::Safe,
5709 "UNSAFE" => Btor2ChannelPropertyAnswer::Unsafe,
5710 _ => {
5711 return Err(PredicateApiError::InvalidResponse(
5712 "unknown BTOR2 channel property answer".to_string(),
5713 ));
5714 }
5715 };
5716 let bad_frame = if values[6] == "none" {
5717 None
5718 } else {
5719 Some(canonical_u32(values[6], result_keys[6])?)
5720 };
5721 let backend = match values[7] {
5722 "representative-class" => Btor2ChannelPropertyProcessBackend::RepresentativeClass,
5723 "direct-exact" => Btor2ChannelPropertyProcessBackend::DirectExact,
5724 _ => {
5725 return Err(PredicateApiError::InvalidResponse(
5726 "unknown BTOR2 channel property backend".to_string(),
5727 ));
5728 }
5729 };
5730 let solver = match values[8] {
5731 "explicit-state" => Btor2ChannelPropertyProcessSolver::ExplicitState,
5732 "bitblast-cnf" => Btor2ChannelPropertyProcessSolver::BitblastCnf,
5733 _ => {
5734 return Err(PredicateApiError::InvalidResponse(
5735 "unknown BTOR2 channel property solver".to_string(),
5736 ));
5737 }
5738 };
5739 let representative_channel = canonical_usize(values[9], result_keys[9])?;
5740 let witness_valuations = canonical_usize(values[10], result_keys[10])?;
5741 if index != expected_index
5742 || channel >= numeric[0]
5743 || representative_channel >= numeric[0]
5744 || results
5745 .last()
5746 .is_some_and(|previous: &Btor2ChannelPropertyProcessResult| {
5747 previous.query_id >= query_id
5748 })
5749 || (answer == Btor2ChannelPropertyAnswer::Safe
5750 && (bad_frame.is_some() || witness_valuations != 0))
5751 || (answer == Btor2ChannelPropertyAnswer::Unsafe
5752 && (bad_frame.is_none()
5753 || bad_frame > Some(horizon)
5754 || witness_valuations == 0))
5755 {
5756 return Err(PredicateApiError::InvalidResponse(
5757 "BTOR2 channel property result violates semantic invariants".to_string(),
5758 ));
5759 }
5760 results.push(Btor2ChannelPropertyProcessResult {
5761 index,
5762 query_id,
5763 channel,
5764 property,
5765 horizon,
5766 answer,
5767 bad_frame,
5768 backend,
5769 solver,
5770 representative_channel,
5771 witness_valuations,
5772 });
5773 }
5774 Ok(Btor2ChannelPropertyProcessSummary {
5775 artifact_version,
5776 channels: numeric[0],
5777 logical_queries: numeric[1],
5778 proof_members: numeric[2],
5779 reused_queries: numeric[3],
5780 explicit_members: numeric[4],
5781 bitblast_members: numeric[5],
5782 evidence_bytes: numeric[6],
5783 artifact_bytes: numeric[7],
5784 projected_work,
5785 elapsed_micros,
5786 results,
5787 })
5788 })();
5789 match parsed {
5790 Ok(value) => Ok(Observed { value, metrics }),
5791 Err(error) => {
5792 metrics.status = InvocationStatus::Failed(error.failure_class());
5793 Err(PredicateOperationError {
5794 error: Box::new(error),
5795 metrics,
5796 })
5797 }
5798 }
5799}
5800
5801fn parse_revision_impact_summary(
5802 output: ManagedOutput,
5803 expected_status: &str,
5804 capabilities: &RevisionImpactCapabilities,
5805) -> Result<Observed<RevisionImpactProcessSummary>, PredicateOperationError> {
5806 let (stdout, mut metrics) = successful_stdout(output)?;
5807 let parsed = (|| -> Result<RevisionImpactProcessSummary, PredicateApiError> {
5808 if stdout.contains('\r') || !stdout.ends_with('\n') {
5809 return Err(PredicateApiError::InvalidResponse(
5810 "revision impact response is not canonical LF text".to_string(),
5811 ));
5812 }
5813 let lines = stdout.lines().collect::<Vec<_>>();
5814 let fields = lines
5815 .first()
5816 .ok_or_else(|| {
5817 PredicateApiError::InvalidResponse("revision impact response is empty".to_string())
5818 })?
5819 .split(' ')
5820 .collect::<Vec<_>>();
5821 if fields.len() != 19 || fields.first().copied() != Some("btor2-revision-impact") {
5822 return Err(PredicateApiError::InvalidResponse(
5823 "revision impact summary shape is invalid".to_string(),
5824 ));
5825 }
5826 let keys = [
5827 "status",
5828 "impact_version",
5829 "atoms",
5830 "queries",
5831 "combinations",
5832 "reusable_observations",
5833 "invalidated_observations",
5834 "minimal_invalidating_sets",
5835 "minimal_semantic_change_sets",
5836 "evidence_members",
5837 "certificate_bytes",
5838 "parsed_evidence_bytes",
5839 "semantic_replays",
5840 "component_validations",
5841 "composed_pair_checks",
5842 "final_transition_checks",
5843 "result_comparisons",
5844 "elapsed_micros",
5845 ];
5846 let values = fields[1..19]
5847 .iter()
5848 .zip(keys)
5849 .map(|(field, key)| token_value(field, key))
5850 .collect::<Result<Vec<_>, _>>()?;
5851 if values[0] != expected_status {
5852 return Err(PredicateApiError::InvalidResponse(
5853 "revision impact status differs from requested operation".to_string(),
5854 ));
5855 }
5856 let impact_version = canonical_u32(values[1], keys[1])?;
5857 let numeric = values[2..]
5858 .iter()
5859 .enumerate()
5860 .map(|(index, value)| canonical_usize(value, keys[index + 2]))
5861 .collect::<Result<Vec<_>, _>>()?;
5862 if impact_version != capabilities.impact_version
5863 || numeric[0] == 0
5864 || numeric[0] > capabilities.max_atoms
5865 || numeric[1] == 0
5866 || numeric[1] > capabilities.max_queries
5867 || numeric[2] != 1usize << numeric[0]
5868 || numeric[2] > capabilities.max_combinations
5869 || numeric[3].checked_add(numeric[4]) != Some(numeric[2] * numeric[1])
5870 || numeric[7] != numeric[2] * numeric[1]
5871 || numeric[8] == 0
5872 || numeric[8] > capabilities.max_bundle_bytes
5873 || numeric[9] == 0
5874 || numeric[9] > capabilities.max_bundle_bytes
5875 || numeric[10] != numeric[7]
5876 || numeric[11] != numeric[7] * 2
5877 || numeric[14] != numeric[7]
5878 {
5879 return Err(PredicateApiError::InvalidResponse(
5880 "revision impact summary violates advertised dimensions".to_string(),
5881 ));
5882 }
5883 if lines.len() != numeric[1] + numeric[6] + 1 {
5884 return Err(PredicateApiError::InvalidResponse(
5885 "revision impact query transition count is incomplete".to_string(),
5886 ));
5887 }
5888 let query_keys = [
5889 "index",
5890 "horizon",
5891 "bad_side",
5892 "bad_output",
5893 "old_result",
5894 "new_result",
5895 ];
5896 let mut transitions = Vec::with_capacity(numeric[1]);
5897 let mut previous_key = None;
5898 for (index, line) in lines[1..=numeric[1]].iter().enumerate() {
5899 let fields = line.split(' ').collect::<Vec<_>>();
5900 if fields.len() != 7 || fields.first().copied() != Some("btor2-revision-impact-query") {
5901 return Err(PredicateApiError::InvalidResponse(
5902 "revision impact query transition shape is invalid".to_string(),
5903 ));
5904 }
5905 let values = fields[1..]
5906 .iter()
5907 .zip(query_keys)
5908 .map(|(field, key)| token_value(field, key))
5909 .collect::<Result<Vec<_>, _>>()?;
5910 if canonical_usize(values[0], query_keys[0])? != index {
5911 return Err(PredicateApiError::InvalidResponse(
5912 "revision impact query transition index is noncanonical".to_string(),
5913 ));
5914 }
5915 let horizon = canonical_u32(values[1], query_keys[1])?;
5916 let (bad_side, side_key) = match values[2] {
5917 "left" => (revision_local::ComponentSide::Left, 0_u8),
5918 "right" => (revision_local::ComponentSide::Right, 1_u8),
5919 _ => {
5920 return Err(PredicateApiError::InvalidResponse(
5921 "revision impact query side is invalid".to_string(),
5922 ));
5923 }
5924 };
5925 let bad_output = canonical_u64(values[3], query_keys[3])?;
5926 if bad_output == 0 {
5927 return Err(PredicateApiError::InvalidResponse(
5928 "revision impact query output is zero".to_string(),
5929 ));
5930 }
5931 let key = (horizon, side_key, bad_output);
5932 if previous_key.is_some_and(|previous| previous >= key) {
5933 return Err(PredicateApiError::InvalidResponse(
5934 "revision impact query transitions are not strictly ordered".to_string(),
5935 ));
5936 }
5937 previous_key = Some(key);
5938 let parse_result = |value| match value {
5939 "SAFE" => Ok(revision_local::BoundedResult::Safe),
5940 "UNSAFE" => Ok(revision_local::BoundedResult::Unsafe),
5941 _ => Err(PredicateApiError::InvalidResponse(
5942 "revision impact query result is invalid".to_string(),
5943 )),
5944 };
5945 transitions.push(RevisionImpactQueryTransition {
5946 index,
5947 horizon,
5948 bad_side,
5949 bad_output,
5950 old_result: parse_result(values[4])?,
5951 new_result: parse_result(values[5])?,
5952 });
5953 }
5954 let semantic_keys = [
5955 "query_index",
5956 "changed_mask",
5957 "baseline_result",
5958 "changed_result",
5959 ];
5960 let parse_result = |value| match value {
5961 "SAFE" => Ok(revision_local::BoundedResult::Safe),
5962 "UNSAFE" => Ok(revision_local::BoundedResult::Unsafe),
5963 _ => Err(PredicateApiError::InvalidResponse(
5964 "revision impact semantic-set result is invalid".to_string(),
5965 )),
5966 };
5967 let mut semantic_change_sets = Vec::with_capacity(numeric[6]);
5968 let mut previous_semantic_key = None;
5969 for line in &lines[numeric[1] + 1..] {
5970 let fields = line.split(' ').collect::<Vec<_>>();
5971 if fields.len() != 5
5972 || fields.first().copied() != Some("btor2-revision-impact-semantic-set")
5973 {
5974 return Err(PredicateApiError::InvalidResponse(
5975 "revision impact semantic-set shape is invalid".to_string(),
5976 ));
5977 }
5978 let values = fields[1..]
5979 .iter()
5980 .zip(semantic_keys)
5981 .map(|(field, key)| token_value(field, key))
5982 .collect::<Result<Vec<_>, _>>()?;
5983 let query_index = canonical_usize(values[0], semantic_keys[0])?;
5984 let changed_mask_usize = canonical_usize(values[1], semantic_keys[1])?;
5985 if query_index >= numeric[1]
5986 || changed_mask_usize == 0
5987 || changed_mask_usize >= numeric[2]
5988 {
5989 return Err(PredicateApiError::InvalidResponse(
5990 "revision impact semantic-set index or mask is out of range".to_string(),
5991 ));
5992 }
5993 let key = (query_index, changed_mask_usize);
5994 if previous_semantic_key.is_some_and(|previous| previous >= key) {
5995 return Err(PredicateApiError::InvalidResponse(
5996 "revision impact semantic sets are not strictly ordered".to_string(),
5997 ));
5998 }
5999 previous_semantic_key = Some(key);
6000 let baseline_result = parse_result(values[2])?;
6001 let changed_result = parse_result(values[3])?;
6002 if baseline_result == changed_result {
6003 return Err(PredicateApiError::InvalidResponse(
6004 "revision impact semantic set does not change the result".to_string(),
6005 ));
6006 }
6007 semantic_change_sets.push(RevisionImpactSemanticChangeSet {
6008 query_index,
6009 changed_mask: u16::try_from(changed_mask_usize).map_err(|_| {
6010 PredicateApiError::InvalidResponse(
6011 "revision impact semantic-set mask exceeds u16".to_string(),
6012 )
6013 })?,
6014 baseline_result,
6015 changed_result,
6016 });
6017 }
6018 Ok(RevisionImpactProcessSummary {
6019 impact_version,
6020 atoms: numeric[0],
6021 queries: numeric[1],
6022 combinations: numeric[2],
6023 reusable_observations: numeric[3],
6024 invalidated_observations: numeric[4],
6025 minimal_invalidating_sets: numeric[5],
6026 minimal_semantic_change_sets: numeric[6],
6027 evidence_members: numeric[7],
6028 certificate_bytes: numeric[8],
6029 parsed_evidence_bytes: numeric[9],
6030 semantic_replays: numeric[10],
6031 component_validations: numeric[11],
6032 composed_pair_checks: numeric[12],
6033 final_transition_checks: numeric[13],
6034 result_comparisons: numeric[14],
6035 elapsed_micros: numeric[15],
6036 transitions,
6037 semantic_change_sets,
6038 })
6039 })();
6040 match parsed {
6041 Ok(value) => Ok(Observed { value, metrics }),
6042 Err(error) => {
6043 metrics.status = InvocationStatus::Failed(error.failure_class());
6044 Err(PredicateOperationError {
6045 error: Box::new(error),
6046 metrics,
6047 })
6048 }
6049 }
6050}
6051
6052fn parse_controller_plant_portfolio_capabilities(
6053 line: &str,
6054) -> Result<ControllerPlantPortfolioCapabilities, PredicateApiError> {
6055 if line.contains('\r') || !line.ends_with('\n') || line.lines().count() != 1 {
6056 return Err(PredicateApiError::InvalidResponse(
6057 "controller plant portfolio capability line is not canonical".to_string(),
6058 ));
6059 }
6060 let fields = line.trim_end_matches('\n').split(' ').collect::<Vec<_>>();
6061 let keys = [
6062 "controller_plant_portfolio_cli_version",
6063 "artifact_version",
6064 "manifest_version",
6065 "max_manifest_bytes",
6066 "max_artifact_bytes",
6067 "max_members",
6068 "backends",
6069 "routing",
6070 "fallback",
6071 "unsupported",
6072 ];
6073 if fields.len() != keys.len() {
6074 return Err(PredicateApiError::InvalidResponse(
6075 "controller plant portfolio capability field count is invalid".to_string(),
6076 ));
6077 }
6078 let values = fields
6079 .iter()
6080 .zip(keys)
6081 .map(|(field, key)| token_value(field, key))
6082 .collect::<Result<Vec<_>, _>>()?;
6083 if values[6..] != ["mtbdd,direct-exact", "static", "exact", "fail-closed"] {
6084 return Err(PredicateApiError::IncompatibleContract(
6085 "controller plant portfolio routing contract is unsupported".to_string(),
6086 ));
6087 }
6088 let versions = values[..3]
6089 .iter()
6090 .enumerate()
6091 .map(|(index, value)| canonical_u32(value, keys[index]))
6092 .collect::<Result<Vec<_>, _>>()?;
6093 let limits = values[3..6]
6094 .iter()
6095 .enumerate()
6096 .map(|(index, value)| canonical_usize(value, keys[index + 3]))
6097 .collect::<Result<Vec<_>, _>>()?;
6098 if versions != [1, 1, 1] {
6099 return Err(PredicateApiError::IncompatibleContract(
6100 "controller plant portfolio version tuple is unsupported".to_string(),
6101 ));
6102 }
6103 if limits.contains(&0) {
6104 return Err(PredicateApiError::InvalidResponse(
6105 "controller plant portfolio discovered limit must be positive".to_string(),
6106 ));
6107 }
6108 Ok(ControllerPlantPortfolioCapabilities {
6109 cli_version: versions[0],
6110 artifact_version: versions[1],
6111 manifest_version: versions[2],
6112 max_manifest_bytes: limits[0],
6113 max_artifact_bytes: limits[1],
6114 max_members: limits[2],
6115 })
6116}
6117
6118fn parse_controller_plant_portfolio_summary(
6119 output: ManagedOutput,
6120 expected_action: &str,
6121 expected_output: Option<&Path>,
6122 capabilities: &ControllerPlantPortfolioCapabilities,
6123) -> Result<Observed<ControllerPlantPortfolioBatchSummary>, PredicateOperationError> {
6124 let (stdout, mut metrics) = successful_stdout(output)?;
6125 let parsed = (|| -> Result<ControllerPlantPortfolioBatchSummary, PredicateApiError> {
6126 if stdout.contains('\r') || !stdout.ends_with('\n') {
6127 return Err(PredicateApiError::InvalidResponse(
6128 "controller plant portfolio response is not canonical LF text".to_string(),
6129 ));
6130 }
6131 let mut lines = stdout.lines();
6132 let first = lines.next().ok_or_else(|| {
6133 PredicateApiError::InvalidResponse(
6134 "controller plant portfolio summary line is missing".to_string(),
6135 )
6136 })?;
6137 let summary_text = if let Some(expected_output) = expected_output {
6138 first
6139 .split_once(" output=")
6140 .and_then(|(summary, output)| {
6141 (output == expected_output.to_string_lossy()).then_some(summary)
6142 })
6143 .ok_or_else(|| {
6144 PredicateApiError::InvalidResponse(
6145 "controller plant portfolio output path disagrees".to_string(),
6146 )
6147 })?
6148 } else if first.contains(" output=") {
6149 return Err(PredicateApiError::InvalidResponse(
6150 "verified controller plant portfolio unexpectedly names output".to_string(),
6151 ));
6152 } else {
6153 first
6154 };
6155 let fields = summary_text.split(' ').collect::<Vec<_>>();
6156 let keys = [
6157 "controller-plant-portfolio",
6158 "status",
6159 "cli_version",
6160 "artifact_version",
6161 "backend",
6162 "reason",
6163 "members",
6164 "safe",
6165 "unsafe",
6166 "reachable_product_states",
6167 "explored_transitions",
6168 "artifact_bytes",
6169 "load_micros",
6170 "artifact_micros",
6171 "verification_micros",
6172 "write_micros",
6173 "elapsed_micros",
6174 ];
6175 if fields.len() != keys.len() || fields[0] != keys[0] {
6176 return Err(PredicateApiError::InvalidResponse(
6177 "controller plant portfolio summary shape is invalid".to_string(),
6178 ));
6179 }
6180 if token_value(fields[1], "status")? != expected_action {
6181 return Err(PredicateApiError::InvalidResponse(
6182 "controller plant portfolio action disagrees".to_string(),
6183 ));
6184 }
6185 let cli_version = canonical_u32(token_value(fields[2], "cli_version")?, "cli_version")?;
6186 let artifact_version = canonical_u32(
6187 token_value(fields[3], "artifact_version")?,
6188 "artifact_version",
6189 )?;
6190 if cli_version != capabilities.cli_version
6191 || artifact_version != capabilities.artifact_version
6192 {
6193 return Err(PredicateApiError::IncompatibleContract(
6194 "controller plant portfolio response version changed".to_string(),
6195 ));
6196 }
6197 let backend = match token_value(fields[4], "backend")? {
6198 "MTBDD" => ControllerPlantPortfolioBackend::Mtbdd,
6199 "DIRECT_EXACT" => ControllerPlantPortfolioBackend::DirectExact,
6200 _ => {
6201 return Err(PredicateApiError::InvalidResponse(
6202 "controller plant portfolio backend is invalid".to_string(),
6203 ));
6204 }
6205 };
6206 let reason = match token_value(fields[5], "reason")? {
6207 "mtbdd-admitted" => ControllerPlantPortfolioReason::MtbddAdmitted,
6208 "boundary-limit" => ControllerPlantPortfolioReason::BoundaryLimit,
6209 "terminal-limit" => ControllerPlantPortfolioReason::TerminalLimit,
6210 "node-limit" => ControllerPlantPortfolioReason::NodeLimit,
6211 _ => {
6212 return Err(PredicateApiError::InvalidResponse(
6213 "controller plant portfolio reason is invalid".to_string(),
6214 ));
6215 }
6216 };
6217 if !matches!(
6218 (backend, reason),
6219 (
6220 ControllerPlantPortfolioBackend::Mtbdd,
6221 ControllerPlantPortfolioReason::MtbddAdmitted
6222 ) | (
6223 ControllerPlantPortfolioBackend::DirectExact,
6224 ControllerPlantPortfolioReason::BoundaryLimit
6225 | ControllerPlantPortfolioReason::TerminalLimit
6226 | ControllerPlantPortfolioReason::NodeLimit
6227 )
6228 ) {
6229 return Err(PredicateApiError::InvalidResponse(
6230 "controller plant portfolio route and reason disagree".to_string(),
6231 ));
6232 }
6233 let numeric = fields[6..]
6234 .iter()
6235 .zip(&keys[6..])
6236 .map(|(field, key)| canonical_usize(token_value(field, key)?, key))
6237 .collect::<Result<Vec<_>, _>>()?;
6238 let member_count = numeric[0];
6239 if member_count == 0
6240 || member_count > capabilities.max_members
6241 || numeric[5] > capabilities.max_artifact_bytes
6242 {
6243 return Err(PredicateApiError::InvalidResponse(
6244 "controller plant portfolio response exceeds discovered limits".to_string(),
6245 ));
6246 }
6247 let mut members = Vec::with_capacity(member_count);
6248 for expected_index in 0..member_count {
6249 let line = lines.next().ok_or_else(|| {
6250 PredicateApiError::InvalidResponse(
6251 "controller plant portfolio member line is missing".to_string(),
6252 )
6253 })?;
6254 let fields = line.split(' ').collect::<Vec<_>>();
6255 let keys = [
6256 "controller-plant-portfolio-member",
6257 "index",
6258 "answer",
6259 "horizon",
6260 "bad_frame",
6261 "trace_steps",
6262 "reachable_product_states",
6263 "explored_transitions",
6264 ];
6265 if fields.len() != keys.len() || fields[0] != keys[0] {
6266 return Err(PredicateApiError::InvalidResponse(
6267 "controller plant portfolio member shape is invalid".to_string(),
6268 ));
6269 }
6270 let index = canonical_usize(token_value(fields[1], "index")?, "index")?;
6271 if index != expected_index {
6272 return Err(PredicateApiError::InvalidResponse(
6273 "controller plant portfolio member order is invalid".to_string(),
6274 ));
6275 }
6276 let answer = match token_value(fields[2], "answer")? {
6277 "SAFE" => ControllerMtbddAnswer::Safe,
6278 "UNSAFE" => ControllerMtbddAnswer::Unsafe,
6279 _ => {
6280 return Err(PredicateApiError::InvalidResponse(
6281 "controller plant portfolio member answer is invalid".to_string(),
6282 ));
6283 }
6284 };
6285 let horizon = canonical_usize(token_value(fields[3], "horizon")?, "horizon")?;
6286 let bad = token_value(fields[4], "bad_frame")?;
6287 let bad_frame = if bad == "none" {
6288 None
6289 } else {
6290 Some(canonical_usize(bad, "bad_frame")?)
6291 };
6292 let trace_steps =
6293 canonical_usize(token_value(fields[5], "trace_steps")?, "trace_steps")?;
6294 if horizon > crate::controller_plant::MAX_COMPOSITION_HORIZON
6295 || match (answer, bad_frame) {
6296 (ControllerMtbddAnswer::Safe, None) => trace_steps != 0,
6297 (ControllerMtbddAnswer::Unsafe, Some(frame)) => {
6298 frame > horizon || trace_steps != frame.saturating_add(1)
6299 }
6300 _ => true,
6301 }
6302 {
6303 return Err(PredicateApiError::InvalidResponse(
6304 "controller plant portfolio member trace boundary is invalid".to_string(),
6305 ));
6306 }
6307 members.push(ControllerMtbddMemberResult {
6308 index,
6309 answer,
6310 horizon,
6311 bad_frame,
6312 trace_steps,
6313 reachable_product_states: canonical_usize(
6314 token_value(fields[6], "reachable_product_states")?,
6315 "reachable_product_states",
6316 )?,
6317 explored_transitions: canonical_usize(
6318 token_value(fields[7], "explored_transitions")?,
6319 "explored_transitions",
6320 )?,
6321 });
6322 }
6323 let reachable_total = members.iter().try_fold(0usize, |total, member| {
6324 total.checked_add(member.reachable_product_states)
6325 });
6326 let transition_total = members.iter().try_fold(0usize, |total, member| {
6327 total.checked_add(member.explored_transitions)
6328 });
6329 if lines.next().is_some()
6330 || members
6331 .iter()
6332 .filter(|member| matches!(member.answer, ControllerMtbddAnswer::Safe))
6333 .count()
6334 != numeric[1]
6335 || members
6336 .iter()
6337 .filter(|member| matches!(member.answer, ControllerMtbddAnswer::Unsafe))
6338 .count()
6339 != numeric[2]
6340 || reachable_total != Some(numeric[3])
6341 || transition_total != Some(numeric[4])
6342 {
6343 return Err(PredicateApiError::InvalidResponse(
6344 "controller plant portfolio member totals disagree".to_string(),
6345 ));
6346 }
6347 Ok(ControllerPlantPortfolioBatchSummary {
6348 artifact_version,
6349 backend,
6350 reason,
6351 safe: numeric[1],
6352 unsafe_count: numeric[2],
6353 reachable_product_states: numeric[3],
6354 explored_transitions: numeric[4],
6355 artifact_bytes: numeric[5],
6356 load_micros: numeric[6],
6357 artifact_micros: numeric[7],
6358 verification_micros: numeric[8],
6359 write_micros: numeric[9],
6360 elapsed_micros: numeric[10],
6361 members,
6362 })
6363 })();
6364 match parsed {
6365 Ok(value) => Ok(Observed { value, metrics }),
6366 Err(error) => {
6367 metrics.status = InvocationStatus::Failed(error.failure_class());
6368 Err(PredicateOperationError {
6369 error: Box::new(error),
6370 metrics,
6371 })
6372 }
6373 }
6374}
6375
6376fn parse_controller_split_evidence_capabilities(
6377 line: &str,
6378) -> Result<ControllerSplitEvidenceCapabilities, PredicateApiError> {
6379 if line.contains('\r') || !line.ends_with('\n') || line.lines().count() != 1 {
6380 return Err(PredicateApiError::InvalidResponse(
6381 "controller split-evidence capability line is not canonical".to_string(),
6382 ));
6383 }
6384 let fields = line.trim_end_matches('\n').split(' ').collect::<Vec<_>>();
6385 let keys = [
6386 "controller_split_evidence_cli_version",
6387 "controller_artifact_version",
6388 "plant_artifact_version",
6389 "manifest_version",
6390 "max_manifest_bytes",
6391 "max_artifact_bytes",
6392 "max_batches",
6393 "admission",
6394 "verification",
6395 "exhaustive_replay",
6396 "source_binding",
6397 "obligation_binding",
6398 "unsupported",
6399 ];
6400 if fields.len() != keys.len() {
6401 return Err(PredicateApiError::InvalidResponse(
6402 "controller split-evidence capability field count is invalid".to_string(),
6403 ));
6404 }
6405 let values = fields
6406 .iter()
6407 .zip(keys)
6408 .map(|(field, key)| token_value(field, key))
6409 .collect::<Result<Vec<_>, _>>()?;
6410 if values[7..]
6411 != [
6412 "once",
6413 "unsat-miter",
6414 "no",
6415 "sha256",
6416 "complete-ordered",
6417 "fail-closed",
6418 ]
6419 {
6420 return Err(PredicateApiError::IncompatibleContract(
6421 "controller split-evidence trust contract is unsupported".to_string(),
6422 ));
6423 }
6424 let versions = values[..4]
6425 .iter()
6426 .enumerate()
6427 .map(|(index, value)| canonical_u32(value, keys[index]))
6428 .collect::<Result<Vec<_>, _>>()?;
6429 let limits = values[4..7]
6430 .iter()
6431 .enumerate()
6432 .map(|(index, value)| canonical_usize(value, keys[index + 4]))
6433 .collect::<Result<Vec<_>, _>>()?;
6434 if versions != [1, 1, 1, 1] {
6435 return Err(PredicateApiError::IncompatibleContract(
6436 "controller split-evidence version tuple is unsupported".to_string(),
6437 ));
6438 }
6439 if limits.contains(&0) {
6440 return Err(PredicateApiError::InvalidResponse(
6441 "controller split-evidence discovered limit must be positive".to_string(),
6442 ));
6443 }
6444 if limits[0] > 64 * 1024
6445 || limits[1] > controller_plant_artifact::MAX_CONTROLLER_PLANT_ARTIFACT_BYTES
6446 || limits[2] > controller_plant_artifact::MAX_CONTROLLER_PLANT_ARTIFACT_MEMBERS
6447 {
6448 return Err(PredicateApiError::IncompatibleContract(
6449 "controller split-evidence limits exceed client safety ceilings".to_string(),
6450 ));
6451 }
6452 Ok(ControllerSplitEvidenceCapabilities {
6453 cli_version: versions[0],
6454 controller_artifact_version: versions[1],
6455 plant_artifact_version: versions[2],
6456 manifest_version: versions[3],
6457 max_manifest_bytes: limits[0],
6458 max_artifact_bytes: limits[1],
6459 max_batches: limits[2],
6460 })
6461}
6462
6463fn parse_controller_split_resource_capabilities(
6464 line: &str,
6465) -> Result<ControllerSplitResourceCapabilities, PredicateApiError> {
6466 if line.contains('\r') || !line.ends_with('\n') || line.lines().count() != 1 {
6467 return Err(PredicateApiError::InvalidResponse(
6468 "controller split resource capability line is not canonical".to_string(),
6469 ));
6470 }
6471 let fields = line.trim_end_matches('\n').split(' ').collect::<Vec<_>>();
6472 let keys = [
6473 "controller_split_resource_cli_version",
6474 "policy_version",
6475 "controller_envelope_version",
6476 "plant_envelope_version",
6477 "controller_artifact_version",
6478 "plant_artifact_version",
6479 "manifest_version",
6480 "max_policy_bytes",
6481 "max_controller_artifact_bytes",
6482 "max_unsat_proof_bytes",
6483 "max_plant_artifact_bytes",
6484 "max_batches",
6485 "max_members_per_batch",
6486 "max_horizon",
6487 "max_product_states",
6488 "refusal_exit",
6489 "admission",
6490 "verification",
6491 "exhaustive_replay",
6492 "accounting",
6493 "timing_calibration",
6494 "result_on_refusal",
6495 "refusal_schema",
6496 "unsupported",
6497 ];
6498 if fields.len() != keys.len() {
6499 return Err(PredicateApiError::InvalidResponse(
6500 "controller split resource capability field count is invalid".to_string(),
6501 ));
6502 }
6503 let values = fields
6504 .iter()
6505 .zip(keys)
6506 .map(|(field, key)| token_value(field, key))
6507 .collect::<Result<Vec<_>, _>>()?;
6508 if values[16..]
6509 != [
6510 "once",
6511 "unsat-miter",
6512 "no",
6513 "conservative-static-per-batch-and-total",
6514 "none",
6515 "none",
6516 "split-reason-v1",
6517 "fail-closed",
6518 ]
6519 {
6520 return Err(PredicateApiError::IncompatibleContract(
6521 "controller split resource trust contract is unsupported".to_string(),
6522 ));
6523 }
6524 let versions = values[..7]
6525 .iter()
6526 .enumerate()
6527 .map(|(index, value)| canonical_u32(value, keys[index]))
6528 .collect::<Result<Vec<_>, _>>()?;
6529 let limits = values[7..15]
6530 .iter()
6531 .enumerate()
6532 .map(|(index, value)| canonical_usize(value, keys[index + 7]))
6533 .collect::<Result<Vec<_>, _>>()?;
6534 let refusal_exit = canonical_usize(values[15], keys[15])?;
6535 if versions != [1, 1, 1, 1, 1, 1, 1] {
6536 return Err(PredicateApiError::IncompatibleContract(
6537 "controller split resource version tuple is unsupported".to_string(),
6538 ));
6539 }
6540 if limits.contains(&0) || refusal_exit != 3 {
6541 return Err(PredicateApiError::InvalidResponse(
6542 "controller split resource discovered limits are invalid".to_string(),
6543 ));
6544 }
6545 if limits[0] > 4096
6546 || limits[1] > controller_plant_artifact::MAX_CONTROLLER_PLANT_ARTIFACT_BYTES
6547 || limits[2] > unsat_proof::MAX_UNSAT_PROOF_BYTES
6548 || limits[3] > controller_plant_artifact::MAX_CONTROLLER_PLANT_ARTIFACT_BYTES
6549 || limits[4] > controller_plant_artifact::MAX_CONTROLLER_PLANT_ARTIFACT_MEMBERS
6550 || limits[5] > controller_plant_artifact::MAX_CONTROLLER_PLANT_ARTIFACT_MEMBERS
6551 || limits[6] > controller_plant::MAX_COMPOSITION_HORIZON
6552 || limits[7] > controller_plant::MAX_PRODUCT_STATES
6553 {
6554 return Err(PredicateApiError::IncompatibleContract(
6555 "controller split resource limits exceed client safety ceilings".to_string(),
6556 ));
6557 }
6558 Ok(ControllerSplitResourceCapabilities {
6559 cli_version: versions[0],
6560 policy_version: versions[1],
6561 controller_envelope_version: versions[2],
6562 plant_envelope_version: versions[3],
6563 controller_artifact_version: versions[4],
6564 plant_artifact_version: versions[5],
6565 manifest_version: versions[6],
6566 max_policy_bytes: limits[0],
6567 max_controller_artifact_bytes: limits[1],
6568 max_unsat_proof_bytes: limits[2],
6569 max_plant_artifact_bytes: limits[3],
6570 max_batches: limits[4],
6571 max_members_per_batch: limits[5],
6572 max_horizon: limits[6],
6573 max_product_states: limits[7],
6574 refusal_exit_code: 3,
6575 })
6576}
6577
6578fn parse_controller_split_observability_capabilities(
6579 text: &str,
6580) -> Result<ControllerSplitObservabilityCapabilities, PredicateApiError> {
6581 if text.contains('\r') || !text.ends_with('\n') || text.lines().count() != 2 {
6582 return Err(PredicateApiError::InvalidResponse(
6583 "controller split observability capability response is not canonical".to_string(),
6584 ));
6585 }
6586 let lines = text.lines().collect::<Vec<_>>();
6587 let resource = parse_controller_split_resource_capabilities(&format!("{}\n", lines[0]))?;
6588 let fields = lines[1].split(' ').collect::<Vec<_>>();
6589 let keys = [
6590 "controller_split_observability_cli_version",
6591 "base_cli_version",
6592 "phase_metrics_version",
6593 "phases",
6594 "counters",
6595 "timing_calibration",
6596 "partial_metrics_on_failure",
6597 "result_on_refusal",
6598 "unsupported",
6599 ];
6600 if fields.len() != keys.len() {
6601 return Err(PredicateApiError::InvalidResponse(
6602 "controller split observability capability field count is invalid".to_string(),
6603 ));
6604 }
6605 let values = fields
6606 .iter()
6607 .zip(keys)
6608 .map(|(field, key)| token_value(field, key))
6609 .collect::<Result<Vec<_>, _>>()?;
6610 let cli_version = canonical_u32(values[0], keys[0])?;
6611 let base_cli_version = canonical_u32(values[1], keys[1])?;
6612 let phase_metrics_version = canonical_u32(values[2], keys[2])?;
6613 if cli_version != 1
6614 || base_cli_version != resource.cli_version
6615 || phase_metrics_version != 1
6616 || values[3]
6617 != "policy-and-input,controller-admission,complete-set-preflight,semantic-replay"
6618 || values[4]
6619 != "controller-admissions,manifest-loads,plant-artifact-reads,resource-assessments,batch-verifications,buffered-result-rows,prepared-batches,prepared-members,controller-evidence-bytes,total-plant-artifact-bytes,total-transition-evaluation-bound"
6620 || values[5..] != ["none", "none", "none", "fail-closed"]
6621 {
6622 return Err(PredicateApiError::IncompatibleContract(
6623 "controller split observability contract is unsupported".to_string(),
6624 ));
6625 }
6626 Ok(ControllerSplitObservabilityCapabilities {
6627 cli_version,
6628 phase_metrics_version,
6629 resource,
6630 })
6631}
6632
6633fn parse_controller_split_allocation_observability_capabilities(
6634 text: &str,
6635) -> Result<ControllerSplitAllocationObservabilityCapabilities, PredicateApiError> {
6636 if text.contains('\r') || !text.ends_with('\n') || text.lines().count() != 3 {
6637 return Err(PredicateApiError::InvalidResponse(
6638 "controller split allocation capability response is not canonical".to_string(),
6639 ));
6640 }
6641 let lines = text.lines().collect::<Vec<_>>();
6642 let observability = parse_controller_split_observability_capabilities(&format!(
6643 "{}\n{}\n",
6644 lines[0], lines[1]
6645 ))?;
6646 let fields = lines[2].split(' ').collect::<Vec<_>>();
6647 let keys = [
6648 "controller_split_allocation_observability_cli_version",
6649 "base_observability_cli_version",
6650 "allocator",
6651 "scope",
6652 "counters",
6653 "overflow",
6654 "timing_calibration",
6655 "partial_metrics_on_failure",
6656 "result_on_refusal",
6657 "unsupported",
6658 ];
6659 if fields.len() != keys.len() {
6660 return Err(PredicateApiError::InvalidResponse(
6661 "controller split allocation capability field count is invalid".to_string(),
6662 ));
6663 }
6664 let values = fields
6665 .iter()
6666 .zip(keys)
6667 .map(|(field, key)| token_value(field, key))
6668 .collect::<Result<Vec<_>, _>>()?;
6669 let cli_version = canonical_u32(values[0], keys[0])?;
6670 let base_version = canonical_u32(values[1], keys[1])?;
6671 if cli_version != 1
6672 || base_version != observability.cli_version
6673 || values[2] != "system"
6674 || values[3] != "policy-through-replay"
6675 || values[4]
6676 != "allocation-calls,allocated-bytes,deallocation-calls,deallocated-bytes,reallocation-calls,reallocated-bytes"
6677 || values[5..] != ["fail-closed", "none", "none", "none", "fail-closed"]
6678 {
6679 return Err(PredicateApiError::IncompatibleContract(
6680 "controller split allocation observability contract is unsupported".to_string(),
6681 ));
6682 }
6683 Ok(ControllerSplitAllocationObservabilityCapabilities {
6684 cli_version,
6685 observability,
6686 })
6687}
6688
6689fn parse_controller_split_cache_observability_capabilities(
6690 text: &str,
6691) -> Result<ControllerSplitCacheObservabilityCapabilities, PredicateApiError> {
6692 if text.contains('\r') || !text.ends_with('\n') || text.lines().count() != 4 {
6693 return Err(PredicateApiError::InvalidResponse(
6694 "controller split cache capability response is not canonical".to_string(),
6695 ));
6696 }
6697 let lines = text.lines().collect::<Vec<_>>();
6698 let allocation_observability = parse_controller_split_allocation_observability_capabilities(
6699 &format!("{}\n{}\n{}\n", lines[0], lines[1], lines[2]),
6700 )?;
6701 let fields = lines[3].split(' ').collect::<Vec<_>>();
6702 let keys = [
6703 "controller_split_cache_observability_cli_version",
6704 "base_allocation_observability_cli_version",
6705 "scope",
6706 "key",
6707 "counters",
6708 "integrity_preflight",
6709 "overflow",
6710 "timing_calibration",
6711 "partial_metrics_on_failure",
6712 "result_on_refusal",
6713 "unsupported",
6714 ];
6715 if fields.len() != keys.len() {
6716 return Err(PredicateApiError::InvalidResponse(
6717 "controller split cache capability field count is invalid".to_string(),
6718 ));
6719 }
6720 let values = fields
6721 .iter()
6722 .zip(keys)
6723 .map(|(field, key)| token_value(field, key))
6724 .collect::<Result<Vec<_>, _>>()?;
6725 let cli_version = canonical_u32(values[0], keys[0])?;
6726 let base_version = canonical_u32(values[1], keys[1])?;
6727 if cli_version != 1
6728 || base_version != allocation_observability.cli_version
6729 || values[2] != "semantic-replay"
6730 || values[3] != "manifest-snapshot,resource-assessment,result-sha256"
6731 || values[4] != "lookups,hits,misses,entries"
6732 || values[5..]
6733 != [
6734 "required",
6735 "fail-closed",
6736 "none",
6737 "none",
6738 "none",
6739 "fail-closed",
6740 ]
6741 {
6742 return Err(PredicateApiError::IncompatibleContract(
6743 "controller split cache observability contract is unsupported".to_string(),
6744 ));
6745 }
6746 Ok(ControllerSplitCacheObservabilityCapabilities {
6747 cli_version,
6748 allocation_observability,
6749 })
6750}
6751
6752fn parse_controller_mtbdd_capabilities(
6753 line: &str,
6754) -> Result<ControllerMtbddCapabilities, PredicateApiError> {
6755 if line.contains('\r') || !line.ends_with('\n') || line.lines().count() != 1 {
6756 return Err(PredicateApiError::InvalidResponse(
6757 "controller MTBDD capability line is not canonical".to_string(),
6758 ));
6759 }
6760 let fields = line.trim_end_matches('\n').split(' ').collect::<Vec<_>>();
6761 let keys = [
6762 "controller_mtbdd_cli_version",
6763 "mtbdd_version",
6764 "plant_artifact_version",
6765 "manifest_version",
6766 "max_manifest_bytes",
6767 "max_artifact_bytes",
6768 "max_members",
6769 "max_state_bits",
6770 "max_inputs",
6771 "max_outputs",
6772 "max_nodes",
6773 "max_terminals",
6774 "max_assignments",
6775 "max_horizon",
6776 "unsupported",
6777 ];
6778 if fields.len() != keys.len() {
6779 return Err(PredicateApiError::InvalidResponse(
6780 "controller MTBDD capability field count is invalid".to_string(),
6781 ));
6782 }
6783 let values = fields
6784 .iter()
6785 .zip(keys)
6786 .map(|(field, key)| token_value(field, key))
6787 .collect::<Result<Vec<_>, _>>()?;
6788 if values[14] != "fail-closed" {
6789 return Err(PredicateApiError::IncompatibleContract(
6790 "controller MTBDD unsupported-input policy is not fail-closed".to_string(),
6791 ));
6792 }
6793 let versions = values[..4]
6794 .iter()
6795 .enumerate()
6796 .map(|(index, value)| canonical_u32(value, keys[index]))
6797 .collect::<Result<Vec<_>, _>>()?;
6798 let limits = values[4..14]
6799 .iter()
6800 .enumerate()
6801 .map(|(index, value)| canonical_usize(value, keys[index + 4]))
6802 .collect::<Result<Vec<_>, _>>()?;
6803 if versions != [1, 1, 1, 1] {
6804 return Err(PredicateApiError::IncompatibleContract(
6805 "controller MTBDD version tuple is unsupported".to_string(),
6806 ));
6807 }
6808 if limits.contains(&0) {
6809 return Err(PredicateApiError::InvalidResponse(
6810 "controller MTBDD discovered limit must be positive".to_string(),
6811 ));
6812 }
6813 let capabilities = ControllerMtbddCapabilities {
6814 cli_version: versions[0],
6815 mtbdd_version: versions[1],
6816 plant_artifact_version: versions[2],
6817 manifest_version: versions[3],
6818 max_manifest_bytes: limits[0],
6819 max_artifact_bytes: limits[1],
6820 max_members: limits[2],
6821 max_state_bits: limits[3],
6822 max_inputs: limits[4],
6823 max_outputs: limits[5],
6824 max_nodes: limits[6],
6825 max_terminals: limits[7],
6826 max_assignments: limits[8],
6827 max_horizon: limits[9],
6828 };
6829 Ok(capabilities)
6830}
6831
6832fn parse_controller_proof_mtbdd_capabilities(
6833 line: &str,
6834) -> Result<ControllerProofMtbddCapabilities, PredicateApiError> {
6835 if line.contains('\r') || !line.ends_with('\n') || line.lines().count() != 1 {
6836 return Err(PredicateApiError::InvalidResponse(
6837 "proof-carrying controller MTBDD capability line is not canonical".to_string(),
6838 ));
6839 }
6840 let fields = line.trim_end_matches('\n').split(' ').collect::<Vec<_>>();
6841 let keys = [
6842 "controller_proof_mtbdd_cli_version",
6843 "mtbdd_version",
6844 "equivalence_proof_version",
6845 "plant_artifact_version",
6846 "manifest_version",
6847 "max_manifest_bytes",
6848 "max_artifact_bytes",
6849 "max_equivalence_artifact_bytes",
6850 "max_unsat_proof_bytes",
6851 "max_members",
6852 "max_state_bits",
6853 "max_inputs",
6854 "max_outputs",
6855 "max_nodes",
6856 "max_terminals",
6857 "max_horizon",
6858 "verification",
6859 "exhaustive_replay",
6860 "unsupported",
6861 ];
6862 if fields.len() != keys.len() {
6863 return Err(PredicateApiError::InvalidResponse(
6864 "proof-carrying controller MTBDD capability field count is invalid".to_string(),
6865 ));
6866 }
6867 let values = fields
6868 .iter()
6869 .zip(keys)
6870 .map(|(field, key)| token_value(field, key))
6871 .collect::<Result<Vec<_>, _>>()?;
6872 if values[16] != "unsat-miter" || values[17] != "no" || values[18] != "fail-closed" {
6873 return Err(PredicateApiError::IncompatibleContract(
6874 "proof-carrying controller MTBDD verification contract changed".to_string(),
6875 ));
6876 }
6877 let versions = values[..5]
6878 .iter()
6879 .enumerate()
6880 .map(|(index, value)| canonical_u32(value, keys[index]))
6881 .collect::<Result<Vec<_>, _>>()?;
6882 if versions != [1, 1, 1, 1, 1] {
6883 return Err(PredicateApiError::IncompatibleContract(
6884 "proof-carrying controller MTBDD version tuple is unsupported".to_string(),
6885 ));
6886 }
6887 let limits = values[5..16]
6888 .iter()
6889 .enumerate()
6890 .map(|(index, value)| canonical_usize(value, keys[index + 5]))
6891 .collect::<Result<Vec<_>, _>>()?;
6892 if limits.contains(&0) {
6893 return Err(PredicateApiError::InvalidResponse(
6894 "proof-carrying controller MTBDD discovered limit must be positive".to_string(),
6895 ));
6896 }
6897 Ok(ControllerProofMtbddCapabilities {
6898 cli_version: versions[0],
6899 mtbdd_version: versions[1],
6900 equivalence_proof_version: versions[2],
6901 plant_artifact_version: versions[3],
6902 manifest_version: versions[4],
6903 max_manifest_bytes: limits[0],
6904 max_artifact_bytes: limits[1],
6905 max_equivalence_artifact_bytes: limits[2],
6906 max_unsat_proof_bytes: limits[3],
6907 max_members: limits[4],
6908 max_state_bits: limits[5],
6909 max_inputs: limits[6],
6910 max_outputs: limits[7],
6911 max_nodes: limits[8],
6912 max_terminals: limits[9],
6913 max_horizon: limits[10],
6914 })
6915}
6916
6917fn parse_controller_split_artifact_summary(
6918 output: ManagedOutput,
6919 prefix: &str,
6920 has_members: bool,
6921 expected_output: &Path,
6922 capabilities: &ControllerSplitEvidenceCapabilities,
6923) -> Result<Observed<ControllerSplitArtifactSummary>, PredicateOperationError> {
6924 let (stdout, mut metrics) = successful_stdout(output)?;
6925 let parsed = (|| -> Result<ControllerSplitArtifactSummary, PredicateApiError> {
6926 if stdout.contains('\r') || !stdout.ends_with('\n') || stdout.lines().count() != 1 {
6927 return Err(PredicateApiError::InvalidResponse(
6928 "controller split artifact response is not canonical".to_string(),
6929 ));
6930 }
6931 let line = stdout.trim_end_matches('\n');
6932 let (summary, output_path) = line.rsplit_once(" output=").ok_or_else(|| {
6933 PredicateApiError::InvalidResponse(
6934 "controller split artifact output field is missing".to_string(),
6935 )
6936 })?;
6937 if output_path != expected_output.to_string_lossy() {
6938 return Err(PredicateApiError::InvalidResponse(
6939 "controller split artifact output disagrees".to_string(),
6940 ));
6941 }
6942 let fields = summary.split(' ').collect::<Vec<_>>();
6943 let keys = if has_members {
6944 vec![
6945 prefix,
6946 "status",
6947 "cli_version",
6948 "artifact_version",
6949 "members",
6950 "artifact_bytes",
6951 "elapsed_micros",
6952 ]
6953 } else {
6954 vec![
6955 prefix,
6956 "status",
6957 "cli_version",
6958 "artifact_version",
6959 "mtbdd_nodes",
6960 "mtbdd_terminals",
6961 "artifact_bytes",
6962 "elapsed_micros",
6963 ]
6964 };
6965 if fields.len() != keys.len() || fields[0] != prefix {
6966 return Err(PredicateApiError::InvalidResponse(
6967 "controller split artifact field count is invalid".to_string(),
6968 ));
6969 }
6970 let values = fields[1..]
6971 .iter()
6972 .zip(&keys[1..])
6973 .map(|(field, key)| token_value(field, key))
6974 .collect::<Result<Vec<_>, _>>()?;
6975 if values[0] != "CREATED"
6976 || canonical_u32(values[1], "cli_version")? != capabilities.cli_version
6977 {
6978 return Err(PredicateApiError::IncompatibleContract(
6979 "controller split artifact creation contract changed".to_string(),
6980 ));
6981 }
6982 let artifact_version = canonical_u32(values[2], "artifact_version")?;
6983 let expected_version = if has_members {
6984 capabilities.plant_artifact_version
6985 } else {
6986 capabilities.controller_artifact_version
6987 };
6988 if artifact_version != expected_version {
6989 return Err(PredicateApiError::IncompatibleContract(
6990 "controller split artifact version changed".to_string(),
6991 ));
6992 }
6993 let members = has_members
6994 .then(|| canonical_usize(values[3], "members"))
6995 .transpose()?;
6996 if matches!(members, Some(0))
6997 || members.is_some_and(|count| {
6998 count > controller_plant_artifact::MAX_CONTROLLER_PLANT_ARTIFACT_MEMBERS
6999 })
7000 {
7001 return Err(PredicateApiError::InvalidResponse(
7002 "controller split artifact member count is zero".to_string(),
7003 ));
7004 }
7005 let mtbdd_nodes = (!has_members)
7006 .then(|| canonical_usize(values[3], "mtbdd_nodes"))
7007 .transpose()?;
7008 let mtbdd_terminals = (!has_members)
7009 .then(|| canonical_usize(values[4], "mtbdd_terminals"))
7010 .transpose()?;
7011 if matches!(mtbdd_nodes, Some(0))
7012 || mtbdd_nodes.is_some_and(|count| count > controller_mtbdd::MAX_MTBDD_NODES)
7013 || matches!(mtbdd_terminals, Some(0))
7014 || mtbdd_terminals.is_some_and(|count| count > controller_mtbdd::MAX_MTBDD_TERMINALS)
7015 {
7016 return Err(PredicateApiError::InvalidResponse(
7017 "controller split evidence MTBDD dimensions are outside limits".to_string(),
7018 ));
7019 }
7020 let offset = if has_members { 4 } else { 5 };
7021 let artifact_bytes = canonical_usize(values[offset], "artifact_bytes")?;
7022 let elapsed_micros = canonical_usize(values[offset + 1], "elapsed_micros")?;
7023 if artifact_bytes == 0 || artifact_bytes > capabilities.max_artifact_bytes {
7024 return Err(PredicateApiError::InvalidResponse(
7025 "controller split artifact byte count is outside limits".to_string(),
7026 ));
7027 }
7028 Ok(ControllerSplitArtifactSummary {
7029 artifact_version,
7030 members,
7031 mtbdd_nodes,
7032 mtbdd_terminals,
7033 artifact_bytes,
7034 elapsed_micros,
7035 })
7036 })();
7037 match parsed {
7038 Ok(value) => Ok(Observed { value, metrics }),
7039 Err(error) => {
7040 metrics.status = InvocationStatus::Failed(error.failure_class());
7041 Err(PredicateOperationError {
7042 error: Box::new(error),
7043 metrics,
7044 })
7045 }
7046 }
7047}
7048
7049fn parse_controller_split_set_summary(
7050 output: ManagedOutput,
7051 expected_batches: usize,
7052 capabilities: &ControllerSplitEvidenceCapabilities,
7053) -> Result<Observed<ControllerSplitSetSummary>, PredicateOperationError> {
7054 let (stdout, mut metrics) = successful_stdout(output)?;
7055 let parsed = (|| -> Result<ControllerSplitSetSummary, PredicateApiError> {
7056 if stdout.contains('\r') || !stdout.ends_with('\n') {
7057 return Err(PredicateApiError::InvalidResponse(
7058 "controller split set response is not canonical LF text".to_string(),
7059 ));
7060 }
7061 let lines = stdout.lines().collect::<Vec<_>>();
7062 if lines.len() != expected_batches + 1 {
7063 return Err(PredicateApiError::InvalidResponse(
7064 "controller split set response line count is invalid".to_string(),
7065 ));
7066 }
7067 let mut batches = Vec::with_capacity(expected_batches);
7068 for (expected_index, line) in lines[..expected_batches].iter().enumerate() {
7069 let fields = line.split(' ').collect::<Vec<_>>();
7070 let keys = [
7071 "controller-split-batch",
7072 "index",
7073 "status",
7074 "members",
7075 "safe",
7076 "unsafe",
7077 "reachable_product_states",
7078 "explored_transitions",
7079 "artifact_bytes",
7080 "verification_micros",
7081 ];
7082 if fields.len() != keys.len() || fields[0] != keys[0] {
7083 return Err(PredicateApiError::InvalidResponse(
7084 "controller split batch fields are invalid".to_string(),
7085 ));
7086 }
7087 let values = fields[1..]
7088 .iter()
7089 .zip(&keys[1..])
7090 .map(|(field, key)| token_value(field, key))
7091 .collect::<Result<Vec<_>, _>>()?;
7092 let index = canonical_usize(values[0], "index")?;
7093 if index != expected_index || values[1] != "VERIFIED" {
7094 return Err(PredicateApiError::InvalidResponse(
7095 "controller split batch order or status is invalid".to_string(),
7096 ));
7097 }
7098 let numbers = values[2..]
7099 .iter()
7100 .enumerate()
7101 .map(|(index, value)| canonical_usize(value, keys[index + 3]))
7102 .collect::<Result<Vec<_>, _>>()?;
7103 if numbers[0] == 0
7104 || numbers[0] > controller_plant_artifact::MAX_CONTROLLER_PLANT_ARTIFACT_MEMBERS
7105 || numbers[1].checked_add(numbers[2]) != Some(numbers[0])
7106 || numbers[5] == 0
7107 || numbers[5] > capabilities.max_artifact_bytes
7108 {
7109 return Err(PredicateApiError::InvalidResponse(
7110 "controller split batch dimensions are invalid".to_string(),
7111 ));
7112 }
7113 batches.push(ControllerSplitBatchSummary {
7114 index,
7115 members: numbers[0],
7116 safe: numbers[1],
7117 unsafe_count: numbers[2],
7118 reachable_product_states: numbers[3],
7119 explored_transitions: numbers[4],
7120 artifact_bytes: numbers[5],
7121 verification_micros: numbers[6],
7122 });
7123 }
7124 let fields = lines[expected_batches].split(' ').collect::<Vec<_>>();
7125 let keys = [
7126 "controller-split-set",
7127 "status",
7128 "cli_version",
7129 "controller_admissions",
7130 "batches",
7131 "members",
7132 "safe",
7133 "unsafe",
7134 "reachable_product_states",
7135 "explored_transitions",
7136 "controller_evidence_bytes",
7137 "admission_micros",
7138 "elapsed_micros",
7139 ];
7140 if fields.len() != keys.len() || fields[0] != keys[0] {
7141 return Err(PredicateApiError::InvalidResponse(
7142 "controller split aggregate fields are invalid".to_string(),
7143 ));
7144 }
7145 let values = fields[1..]
7146 .iter()
7147 .zip(&keys[1..])
7148 .map(|(field, key)| token_value(field, key))
7149 .collect::<Result<Vec<_>, _>>()?;
7150 if values[0] != "VERIFIED"
7151 || canonical_u32(values[1], "cli_version")? != capabilities.cli_version
7152 {
7153 return Err(PredicateApiError::IncompatibleContract(
7154 "controller split aggregate contract changed".to_string(),
7155 ));
7156 }
7157 let numbers = values[2..]
7158 .iter()
7159 .enumerate()
7160 .map(|(index, value)| canonical_usize(value, keys[index + 3]))
7161 .collect::<Result<Vec<_>, _>>()?;
7162 let summary = ControllerSplitSetSummary {
7163 controller_admissions: numbers[0],
7164 members: numbers[2],
7165 safe: numbers[3],
7166 unsafe_count: numbers[4],
7167 reachable_product_states: numbers[5],
7168 explored_transitions: numbers[6],
7169 controller_evidence_bytes: numbers[7],
7170 admission_micros: numbers[8],
7171 elapsed_micros: numbers[9],
7172 batches,
7173 };
7174 let batch_members = summary
7175 .batches
7176 .iter()
7177 .try_fold(0usize, |total, batch| total.checked_add(batch.members));
7178 let batch_safe = summary
7179 .batches
7180 .iter()
7181 .try_fold(0usize, |total, batch| total.checked_add(batch.safe));
7182 let batch_unsafe = summary
7183 .batches
7184 .iter()
7185 .try_fold(0usize, |total, batch| total.checked_add(batch.unsafe_count));
7186 let batch_reachable = summary.batches.iter().try_fold(0usize, |total, batch| {
7187 total.checked_add(batch.reachable_product_states)
7188 });
7189 let batch_transitions = summary.batches.iter().try_fold(0usize, |total, batch| {
7190 total.checked_add(batch.explored_transitions)
7191 });
7192 if summary.controller_admissions != 1
7193 || numbers[1] != expected_batches
7194 || Some(summary.members) != batch_members
7195 || Some(summary.safe) != batch_safe
7196 || Some(summary.unsafe_count) != batch_unsafe
7197 || Some(summary.reachable_product_states) != batch_reachable
7198 || Some(summary.explored_transitions) != batch_transitions
7199 || summary.controller_evidence_bytes == 0
7200 || summary.controller_evidence_bytes > capabilities.max_artifact_bytes
7201 {
7202 return Err(PredicateApiError::InvalidResponse(
7203 "controller split aggregate does not reconcile".to_string(),
7204 ));
7205 }
7206 Ok(summary)
7207 })();
7208 match parsed {
7209 Ok(value) => Ok(Observed { value, metrics }),
7210 Err(error) => {
7211 metrics.status = InvocationStatus::Failed(error.failure_class());
7212 Err(PredicateOperationError {
7213 error: Box::new(error),
7214 metrics,
7215 })
7216 }
7217 }
7218}
7219
7220fn parse_controller_split_resource_set_summary(
7221 output: ManagedOutput,
7222 expected_batches: usize,
7223 capabilities: &ControllerSplitResourceCapabilities,
7224) -> Result<Observed<ControllerSplitResourceSetSummary>, PredicateOperationError> {
7225 let (stdout, mut metrics) = successful_stdout(output)?;
7226 let parsed = (|| -> Result<ControllerSplitResourceSetSummary, PredicateApiError> {
7227 if stdout.contains('\r') || !stdout.ends_with('\n') {
7228 return Err(PredicateApiError::InvalidResponse(
7229 "controller split resource response is not canonical LF text".to_string(),
7230 ));
7231 }
7232 let lines = stdout.lines().collect::<Vec<_>>();
7233 if lines.len() != expected_batches + 1 {
7234 return Err(PredicateApiError::InvalidResponse(
7235 "controller split resource response line count is invalid".to_string(),
7236 ));
7237 }
7238 let mut batches = Vec::with_capacity(expected_batches);
7239 for (expected_index, line) in lines[..expected_batches].iter().enumerate() {
7240 let fields = line.split(' ').collect::<Vec<_>>();
7241 let keys = [
7242 "controller-split-resource-batch",
7243 "index",
7244 "status",
7245 "policy_version",
7246 "envelope_version",
7247 "artifact_version",
7248 "members",
7249 "maximum_member_horizon",
7250 "maximum_product_states",
7251 "transition_evaluation_bound",
7252 "safe",
7253 "unsafe",
7254 "reachable_product_states",
7255 "explored_transitions",
7256 "artifact_bytes",
7257 "verification_micros",
7258 ];
7259 if fields.len() != keys.len() || fields[0] != keys[0] {
7260 return Err(PredicateApiError::InvalidResponse(
7261 "controller split resource batch fields are invalid".to_string(),
7262 ));
7263 }
7264 let values = fields[1..]
7265 .iter()
7266 .zip(&keys[1..])
7267 .map(|(field, key)| token_value(field, key))
7268 .collect::<Result<Vec<_>, _>>()?;
7269 let index = canonical_usize(values[0], "index")?;
7270 if index != expected_index || values[1] != "VERIFIED" {
7271 return Err(PredicateApiError::InvalidResponse(
7272 "controller split resource batch order or status is invalid".to_string(),
7273 ));
7274 }
7275 let policy_version = canonical_u32(values[2], "policy_version")?;
7276 let envelope_version = canonical_u32(values[3], "envelope_version")?;
7277 let artifact_version = canonical_u32(values[4], "artifact_version")?;
7278 if policy_version != capabilities.policy_version
7279 || envelope_version != capabilities.plant_envelope_version
7280 || artifact_version != capabilities.plant_artifact_version
7281 {
7282 return Err(PredicateApiError::IncompatibleContract(
7283 "controller split resource batch versions changed".to_string(),
7284 ));
7285 }
7286 let numbers = values[5..]
7287 .iter()
7288 .enumerate()
7289 .map(|(index, value)| canonical_usize(value, keys[index + 6]))
7290 .collect::<Result<Vec<_>, _>>()?;
7291 if numbers[0] == 0
7292 || numbers[0] > capabilities.max_members_per_batch
7293 || numbers[1] > capabilities.max_horizon
7294 || numbers[2] > capabilities.max_product_states
7295 || numbers[4].checked_add(numbers[5]) != Some(numbers[0])
7296 || numbers[8] == 0
7297 || numbers[8] > capabilities.max_plant_artifact_bytes
7298 {
7299 return Err(PredicateApiError::InvalidResponse(
7300 "controller split resource batch dimensions are invalid".to_string(),
7301 ));
7302 }
7303 batches.push(ControllerSplitResourceBatchSummary {
7304 index,
7305 members: numbers[0],
7306 maximum_member_horizon: numbers[1],
7307 maximum_product_states: numbers[2],
7308 transition_evaluation_bound: numbers[3],
7309 safe: numbers[4],
7310 unsafe_count: numbers[5],
7311 reachable_product_states: numbers[6],
7312 explored_transitions: numbers[7],
7313 artifact_bytes: numbers[8],
7314 verification_micros: numbers[9],
7315 });
7316 }
7317 let fields = lines[expected_batches].split(' ').collect::<Vec<_>>();
7318 let keys = [
7319 "controller-split-resource-set",
7320 "status",
7321 "cli_version",
7322 "policy_version",
7323 "controller_envelope_version",
7324 "plant_envelope_version",
7325 "controller_admissions",
7326 "batches",
7327 "members",
7328 "safe",
7329 "unsafe",
7330 "reachable_product_states",
7331 "explored_transitions",
7332 "controller_evidence_bytes",
7333 "controller_mtbdd_bytes",
7334 "equivalence_artifact_bytes",
7335 "unsat_proof_bytes",
7336 "total_plant_artifact_bytes",
7337 "total_transition_evaluation_bound",
7338 "admission_micros",
7339 "elapsed_micros",
7340 ];
7341 if fields.len() != keys.len() || fields[0] != keys[0] {
7342 return Err(PredicateApiError::InvalidResponse(
7343 "controller split resource aggregate fields are invalid".to_string(),
7344 ));
7345 }
7346 let values = fields[1..]
7347 .iter()
7348 .zip(&keys[1..])
7349 .map(|(field, key)| token_value(field, key))
7350 .collect::<Result<Vec<_>, _>>()?;
7351 if values[0] != "VERIFIED"
7352 || canonical_u32(values[1], keys[2])? != capabilities.cli_version
7353 || canonical_u32(values[2], keys[3])? != capabilities.policy_version
7354 || canonical_u32(values[3], keys[4])? != capabilities.controller_envelope_version
7355 || canonical_u32(values[4], keys[5])? != capabilities.plant_envelope_version
7356 {
7357 return Err(PredicateApiError::IncompatibleContract(
7358 "controller split resource aggregate contract changed".to_string(),
7359 ));
7360 }
7361 let numbers = values[5..]
7362 .iter()
7363 .enumerate()
7364 .map(|(index, value)| canonical_usize(value, keys[index + 6]))
7365 .collect::<Result<Vec<_>, _>>()?;
7366 let summary = ControllerSplitResourceSetSummary {
7367 controller_admissions: numbers[0],
7368 members: numbers[2],
7369 safe: numbers[3],
7370 unsafe_count: numbers[4],
7371 reachable_product_states: numbers[5],
7372 explored_transitions: numbers[6],
7373 controller_evidence_bytes: numbers[7],
7374 controller_mtbdd_bytes: numbers[8],
7375 equivalence_artifact_bytes: numbers[9],
7376 unsat_proof_bytes: numbers[10],
7377 total_plant_artifact_bytes: numbers[11],
7378 total_transition_evaluation_bound: numbers[12],
7379 admission_micros: numbers[13],
7380 elapsed_micros: numbers[14],
7381 batches,
7382 };
7383 let batch_members = summary
7384 .batches
7385 .iter()
7386 .try_fold(0usize, |total, batch| total.checked_add(batch.members));
7387 let batch_safe = summary
7388 .batches
7389 .iter()
7390 .try_fold(0usize, |total, batch| total.checked_add(batch.safe));
7391 let batch_unsafe = summary
7392 .batches
7393 .iter()
7394 .try_fold(0usize, |total, batch| total.checked_add(batch.unsafe_count));
7395 let batch_reachable = summary.batches.iter().try_fold(0usize, |total, batch| {
7396 total.checked_add(batch.reachable_product_states)
7397 });
7398 let batch_explored = summary.batches.iter().try_fold(0usize, |total, batch| {
7399 total.checked_add(batch.explored_transitions)
7400 });
7401 let batch_artifact_bytes = summary.batches.iter().try_fold(0usize, |total, batch| {
7402 total.checked_add(batch.artifact_bytes)
7403 });
7404 let batch_transition_bound = summary.batches.iter().try_fold(0usize, |total, batch| {
7405 total.checked_add(batch.transition_evaluation_bound)
7406 });
7407 if summary.controller_admissions != 1
7408 || numbers[1] != expected_batches
7409 || Some(summary.members) != batch_members
7410 || Some(summary.safe) != batch_safe
7411 || Some(summary.unsafe_count) != batch_unsafe
7412 || Some(summary.reachable_product_states) != batch_reachable
7413 || Some(summary.explored_transitions) != batch_explored
7414 || Some(summary.total_plant_artifact_bytes) != batch_artifact_bytes
7415 || Some(summary.total_transition_evaluation_bound) != batch_transition_bound
7416 || summary.controller_evidence_bytes == 0
7417 || summary.controller_evidence_bytes > capabilities.max_controller_artifact_bytes
7418 || summary.controller_mtbdd_bytes == 0
7419 || summary.equivalence_artifact_bytes == 0
7420 || summary.unsat_proof_bytes == 0
7421 || summary.unsat_proof_bytes > capabilities.max_unsat_proof_bytes
7422 || summary.controller_mtbdd_bytes >= summary.controller_evidence_bytes
7423 || summary.equivalence_artifact_bytes >= summary.controller_evidence_bytes
7424 || summary
7425 .controller_mtbdd_bytes
7426 .checked_add(summary.equivalence_artifact_bytes)
7427 .is_none_or(|payload| payload >= summary.controller_evidence_bytes)
7428 || summary.unsat_proof_bytes > summary.equivalence_artifact_bytes
7429 {
7430 return Err(PredicateApiError::InvalidResponse(
7431 "controller split resource aggregate does not reconcile".to_string(),
7432 ));
7433 }
7434 Ok(summary)
7435 })();
7436 match parsed {
7437 Ok(value) => Ok(Observed { value, metrics }),
7438 Err(error) => {
7439 metrics.status = InvocationStatus::Failed(error.failure_class());
7440 Err(PredicateOperationError {
7441 error: Box::new(error),
7442 metrics,
7443 })
7444 }
7445 }
7446}
7447
7448fn parse_controller_split_observed_summary(
7449 output: ManagedOutput,
7450 expected_batches: usize,
7451 capabilities: &ControllerSplitObservabilityCapabilities,
7452) -> Result<Observed<ControllerSplitObservedSummary>, PredicateOperationError> {
7453 let status = output.status;
7454 let (stdout, mut invocation_metrics) = successful_stdout(output)?;
7455 let parsed = (|| -> Result<ControllerSplitObservedSummary, PredicateApiError> {
7456 if stdout.contains('\r')
7457 || !stdout.ends_with('\n')
7458 || stdout.lines().count() != expected_batches + 2
7459 {
7460 return Err(PredicateApiError::InvalidResponse(
7461 "controller split observed response line count is invalid".to_string(),
7462 ));
7463 }
7464 let lines = stdout.lines().collect::<Vec<_>>();
7465 let base_stdout = format!("{}\n", lines[..expected_batches + 1].join("\n"));
7466 let base_output = ManagedOutput {
7467 status,
7468 stdout: base_stdout.into_bytes(),
7469 stderr: Vec::new(),
7470 metrics: invocation_metrics.clone(),
7471 };
7472 let verification = parse_controller_split_resource_set_summary(
7473 base_output,
7474 expected_batches,
7475 &capabilities.resource,
7476 )
7477 .map_err(|failure| *failure.error)?
7478 .value;
7479
7480 let fields = lines[expected_batches + 1].split(' ').collect::<Vec<_>>();
7481 let keys = [
7482 "controller-split-resource-observability",
7483 "status",
7484 "cli_version",
7485 "phase_metrics_version",
7486 "policy_and_input_micros",
7487 "controller_admission_micros",
7488 "complete_set_preflight_micros",
7489 "semantic_replay_micros",
7490 "total_micros",
7491 "controller_admissions",
7492 "manifest_loads",
7493 "plant_artifact_reads",
7494 "resource_assessments",
7495 "batch_verifications",
7496 "buffered_result_rows",
7497 "prepared_batches",
7498 "prepared_members",
7499 "controller_evidence_bytes",
7500 "total_plant_artifact_bytes",
7501 "total_transition_evaluation_bound",
7502 "timing_calibration",
7503 ];
7504 if fields.len() != keys.len() || fields[0] != keys[0] {
7505 return Err(PredicateApiError::InvalidResponse(
7506 "controller split observed metrics fields are invalid".to_string(),
7507 ));
7508 }
7509 let values = fields[1..]
7510 .iter()
7511 .zip(&keys[1..])
7512 .map(|(field, key)| token_value(field, key))
7513 .collect::<Result<Vec<_>, _>>()?;
7514 if values[0] != "MEASURED"
7515 || canonical_u32(values[1], keys[2])? != capabilities.cli_version
7516 || canonical_u32(values[2], keys[3])? != capabilities.phase_metrics_version
7517 || values[19] != "none"
7518 {
7519 return Err(PredicateApiError::IncompatibleContract(
7520 "controller split observed metrics contract changed".to_string(),
7521 ));
7522 }
7523 let timings = values[3..8]
7524 .iter()
7525 .enumerate()
7526 .map(|(index, value)| canonical_u128(value, keys[index + 4]))
7527 .collect::<Result<Vec<_>, _>>()?;
7528 let counts = values[8..19]
7529 .iter()
7530 .enumerate()
7531 .map(|(index, value)| canonical_usize(value, keys[index + 9]))
7532 .collect::<Result<Vec<_>, _>>()?;
7533 let expected_manifest_loads = expected_batches
7534 .checked_mul(2)
7535 .and_then(|count| count.checked_add(1));
7536 let expected_double_batches = expected_batches.checked_mul(2);
7537 let expected_rows = expected_batches.checked_add(1);
7538 let phase_sum = timings[..4]
7539 .iter()
7540 .try_fold(0u128, |total, value| total.checked_add(*value));
7541 if phase_sum.is_none_or(|sum| sum > timings[4])
7542 || u128::try_from(verification.elapsed_micros) != Ok(timings[4])
7543 || counts[0] != 1
7544 || Some(counts[1]) != expected_manifest_loads
7545 || Some(counts[2]) != expected_double_batches
7546 || Some(counts[3]) != expected_double_batches
7547 || counts[4] != expected_batches
7548 || Some(counts[5]) != expected_rows
7549 || counts[6] != expected_batches
7550 || counts[7] != verification.members
7551 || counts[8] != verification.controller_evidence_bytes
7552 || counts[9] != verification.total_plant_artifact_bytes
7553 || counts[10] != verification.total_transition_evaluation_bound
7554 {
7555 return Err(PredicateApiError::InvalidResponse(
7556 "controller split observed metrics do not reconcile".to_string(),
7557 ));
7558 }
7559 Ok(ControllerSplitObservedSummary {
7560 verification,
7561 phases: ControllerSplitPhaseMetrics {
7562 version: capabilities.phase_metrics_version,
7563 policy_and_input_micros: timings[0],
7564 controller_admission_micros: timings[1],
7565 complete_set_preflight_micros: timings[2],
7566 semantic_replay_micros: timings[3],
7567 total_micros: timings[4],
7568 controller_admissions: counts[0],
7569 manifest_loads: counts[1],
7570 plant_artifact_reads: counts[2],
7571 resource_assessments: counts[3],
7572 batch_verifications: counts[4],
7573 buffered_result_rows: counts[5],
7574 prepared_batches: counts[6],
7575 prepared_members: counts[7],
7576 controller_evidence_bytes: counts[8],
7577 total_plant_artifact_bytes: counts[9],
7578 total_transition_evaluation_bound: counts[10],
7579 },
7580 })
7581 })();
7582 match parsed {
7583 Ok(value) => Ok(Observed {
7584 value,
7585 metrics: invocation_metrics,
7586 }),
7587 Err(error) => {
7588 invocation_metrics.status = InvocationStatus::Failed(error.failure_class());
7589 Err(PredicateOperationError {
7590 error: Box::new(error),
7591 metrics: invocation_metrics,
7592 })
7593 }
7594 }
7595}
7596
7597fn parse_controller_split_allocation_observed_summary(
7598 output: ManagedOutput,
7599 expected_batches: usize,
7600 capabilities: &ControllerSplitAllocationObservabilityCapabilities,
7601) -> Result<Observed<ControllerSplitAllocationObservedSummary>, PredicateOperationError> {
7602 let status = output.status;
7603 let (stdout, mut invocation_metrics) = successful_stdout(output)?;
7604 let parsed = (|| -> Result<ControllerSplitAllocationObservedSummary, PredicateApiError> {
7605 if stdout.contains('\r')
7606 || !stdout.ends_with('\n')
7607 || stdout.lines().count() != expected_batches + 3
7608 {
7609 return Err(PredicateApiError::InvalidResponse(
7610 "controller split allocation response line count is invalid".to_string(),
7611 ));
7612 }
7613 let lines = stdout.lines().collect::<Vec<_>>();
7614 let observed_stdout = format!("{}\n", lines[..expected_batches + 2].join("\n"));
7615 let observed_output = ManagedOutput {
7616 status,
7617 stdout: observed_stdout.into_bytes(),
7618 stderr: Vec::new(),
7619 metrics: invocation_metrics.clone(),
7620 };
7621 let observed = parse_controller_split_observed_summary(
7622 observed_output,
7623 expected_batches,
7624 &capabilities.observability,
7625 )
7626 .map_err(|failure| *failure.error)?
7627 .value;
7628
7629 let fields = lines[expected_batches + 2].split(' ').collect::<Vec<_>>();
7630 let keys = [
7631 "controller-split-allocation-observability",
7632 "status",
7633 "cli_version",
7634 "allocator",
7635 "scope",
7636 "allocation_calls",
7637 "allocated_bytes",
7638 "deallocation_calls",
7639 "deallocated_bytes",
7640 "reallocation_calls",
7641 "reallocated_bytes",
7642 "overflow",
7643 "timing_calibration",
7644 ];
7645 if fields.len() != keys.len() || fields[0] != keys[0] {
7646 return Err(PredicateApiError::InvalidResponse(
7647 "controller split allocation metrics fields are invalid".to_string(),
7648 ));
7649 }
7650 let values = fields[1..]
7651 .iter()
7652 .zip(&keys[1..])
7653 .map(|(field, key)| token_value(field, key))
7654 .collect::<Result<Vec<_>, _>>()?;
7655 if values[0] != "MEASURED"
7656 || canonical_u32(values[1], keys[2])? != capabilities.cli_version
7657 || values[2] != "system"
7658 || values[3] != "policy-through-replay"
7659 || values[10] != "none"
7660 || values[11] != "none"
7661 {
7662 return Err(PredicateApiError::IncompatibleContract(
7663 "controller split allocation metrics contract changed".to_string(),
7664 ));
7665 }
7666 let counts = values[4..10]
7667 .iter()
7668 .enumerate()
7669 .map(|(index, value)| canonical_u64(value, keys[index + 5]))
7670 .collect::<Result<Vec<_>, _>>()?;
7671 if counts[0] == 0 || counts[1] == 0 {
7672 return Err(PredicateApiError::InvalidResponse(
7673 "controller split allocation metrics are empty".to_string(),
7674 ));
7675 }
7676 Ok(ControllerSplitAllocationObservedSummary {
7677 observed,
7678 allocations: ControllerSplitAllocationMetrics {
7679 version: capabilities.cli_version,
7680 allocation_calls: counts[0],
7681 allocated_bytes: counts[1],
7682 deallocation_calls: counts[2],
7683 deallocated_bytes: counts[3],
7684 reallocation_calls: counts[4],
7685 reallocated_bytes: counts[5],
7686 },
7687 })
7688 })();
7689 match parsed {
7690 Ok(value) => Ok(Observed {
7691 value,
7692 metrics: invocation_metrics,
7693 }),
7694 Err(error) => {
7695 invocation_metrics.status = InvocationStatus::Failed(error.failure_class());
7696 Err(PredicateOperationError {
7697 error: Box::new(error),
7698 metrics: invocation_metrics,
7699 })
7700 }
7701 }
7702}
7703
7704fn parse_controller_split_cache_observed_summary(
7705 output: ManagedOutput,
7706 expected_batches: usize,
7707 capabilities: &ControllerSplitCacheObservabilityCapabilities,
7708) -> Result<Observed<ControllerSplitCacheObservedSummary>, PredicateOperationError> {
7709 let status = output.status;
7710 let (stdout, mut invocation_metrics) = successful_stdout(output)?;
7711 let parsed = (|| -> Result<ControllerSplitCacheObservedSummary, PredicateApiError> {
7712 if stdout.contains('\r')
7713 || !stdout.ends_with('\n')
7714 || stdout.lines().count() != expected_batches + 4
7715 {
7716 return Err(PredicateApiError::InvalidResponse(
7717 "controller split cache response line count is invalid".to_string(),
7718 ));
7719 }
7720 let lines = stdout.lines().collect::<Vec<_>>();
7721 let allocation_stdout = format!("{}\n", lines[..expected_batches + 3].join("\n"));
7722 let allocation_output = ManagedOutput {
7723 status,
7724 stdout: allocation_stdout.into_bytes(),
7725 stderr: Vec::new(),
7726 metrics: invocation_metrics.clone(),
7727 };
7728 let observed = parse_controller_split_allocation_observed_summary(
7729 allocation_output,
7730 expected_batches,
7731 &capabilities.allocation_observability,
7732 )
7733 .map_err(|failure| *failure.error)?
7734 .value;
7735
7736 let fields = lines[expected_batches + 3].split(' ').collect::<Vec<_>>();
7737 let keys = [
7738 "controller-split-cache-observability",
7739 "status",
7740 "cli_version",
7741 "scope",
7742 "key",
7743 "lookups",
7744 "hits",
7745 "misses",
7746 "entries",
7747 "integrity_preflight",
7748 "overflow",
7749 "timing_calibration",
7750 ];
7751 if fields.len() != keys.len() || fields[0] != keys[0] {
7752 return Err(PredicateApiError::InvalidResponse(
7753 "controller split cache metrics fields are invalid".to_string(),
7754 ));
7755 }
7756 let values = fields[1..]
7757 .iter()
7758 .zip(&keys[1..])
7759 .map(|(field, key)| token_value(field, key))
7760 .collect::<Result<Vec<_>, _>>()?;
7761 if values[0] != "MEASURED"
7762 || canonical_u32(values[1], keys[2])? != capabilities.cli_version
7763 || values[2] != "semantic-replay"
7764 || values[3] != "manifest-snapshot,resource-assessment,result-sha256"
7765 || values[8] != "required"
7766 || values[9] != "none"
7767 || values[10] != "none"
7768 {
7769 return Err(PredicateApiError::IncompatibleContract(
7770 "controller split cache metrics contract changed".to_string(),
7771 ));
7772 }
7773 let counts = values[4..8]
7774 .iter()
7775 .enumerate()
7776 .map(|(index, value)| canonical_usize(value, keys[index + 5]))
7777 .collect::<Result<Vec<_>, _>>()?;
7778 let accounted = counts[1].checked_add(counts[2]);
7779 if counts[0] != expected_batches
7780 || accounted != Some(counts[0])
7781 || counts[3] != counts[2]
7782 || counts[3] > counts[0]
7783 {
7784 return Err(PredicateApiError::InvalidResponse(
7785 "controller split cache metrics do not reconcile".to_string(),
7786 ));
7787 }
7788 Ok(ControllerSplitCacheObservedSummary {
7789 observed,
7790 cache: ControllerSplitCacheMetrics {
7791 version: capabilities.cli_version,
7792 lookups: counts[0],
7793 hits: counts[1],
7794 misses: counts[2],
7795 entries: counts[3],
7796 },
7797 })
7798 })();
7799 match parsed {
7800 Ok(value) => Ok(Observed {
7801 value,
7802 metrics: invocation_metrics,
7803 }),
7804 Err(error) => {
7805 invocation_metrics.status = InvocationStatus::Failed(error.failure_class());
7806 Err(PredicateOperationError {
7807 error: Box::new(error),
7808 metrics: invocation_metrics,
7809 })
7810 }
7811 }
7812}
7813
7814fn classify_controller_split_resource_refusal(
7815 mut failure: PredicateOperationError,
7816) -> PredicateOperationError {
7817 let reason = match failure.error.as_ref() {
7818 PredicateApiError::CommandFailed {
7819 exit_code: Some(3),
7820 stderr,
7821 } => stderr
7822 .trim_end_matches(['\r', '\n'])
7823 .strip_prefix("error: controller-split-resource refusal=")
7824 .and_then(|value| value.strip_suffix(" result=none"))
7825 .and_then(|value| match value {
7826 "controller-artifact-bytes" => {
7827 Some(ControllerPlantResourceRefusalReason::ControllerArtifactBytes)
7828 }
7829 "unsat-proof-bytes" => Some(ControllerPlantResourceRefusalReason::UnsatProofBytes),
7830 "batches" => Some(ControllerPlantResourceRefusalReason::Batches),
7831 "plant-artifact-bytes" => {
7832 Some(ControllerPlantResourceRefusalReason::PlantArtifactBytes)
7833 }
7834 "members-per-batch" => Some(ControllerPlantResourceRefusalReason::MembersPerBatch),
7835 "horizon" => Some(ControllerPlantResourceRefusalReason::Horizon),
7836 "product-states" => Some(ControllerPlantResourceRefusalReason::ProductStates),
7837 "transitions-per-batch" => {
7838 Some(ControllerPlantResourceRefusalReason::TransitionsPerBatch)
7839 }
7840 "total-plant-artifact-bytes" => {
7841 Some(ControllerPlantResourceRefusalReason::TotalPlantArtifactBytes)
7842 }
7843 "total-members" => Some(ControllerPlantResourceRefusalReason::TotalMembers),
7844 "total-transition-evaluations" => {
7845 Some(ControllerPlantResourceRefusalReason::TotalTransitionEvaluations)
7846 }
7847 _ => None,
7848 }),
7849 _ => None,
7850 };
7851 if let Some(reason) = reason {
7852 let error = PredicateApiError::ResourceRefused { reason };
7853 failure.metrics.status = InvocationStatus::Failed(error.failure_class());
7854 failure.error = Box::new(error);
7855 }
7856 failure
7857}
7858
7859fn parse_controller_mtbdd_summary(
7860 output: ManagedOutput,
7861 expected_action: &str,
7862 expected_output: Option<&Path>,
7863 capabilities: &ControllerMtbddCapabilities,
7864 summary_prefix: &str,
7865 member_prefix: &str,
7866) -> Result<Observed<ControllerMtbddBatchSummary>, PredicateOperationError> {
7867 let (stdout, mut metrics) = successful_stdout(output)?;
7868 let parsed = (|| -> Result<ControllerMtbddBatchSummary, PredicateApiError> {
7869 if stdout.contains('\r') || !stdout.ends_with('\n') {
7870 return Err(PredicateApiError::InvalidResponse(
7871 "controller MTBDD response is not canonical LF text".to_string(),
7872 ));
7873 }
7874 let mut lines = stdout.lines();
7875 let first = lines.next().ok_or_else(|| {
7876 PredicateApiError::InvalidResponse(
7877 "controller MTBDD summary line is missing".to_string(),
7878 )
7879 })?;
7880 let summary_text = if let Some(expected_output) = expected_output {
7881 first
7882 .split_once(" output=")
7883 .and_then(|(summary, output)| {
7884 (output == expected_output.to_string_lossy()).then_some(summary)
7885 })
7886 .ok_or_else(|| {
7887 PredicateApiError::InvalidResponse(
7888 "controller MTBDD created response output disagrees".to_string(),
7889 )
7890 })?
7891 } else if first.contains(" output=") {
7892 return Err(PredicateApiError::InvalidResponse(
7893 "controller MTBDD verified response unexpectedly names output".to_string(),
7894 ));
7895 } else {
7896 first
7897 };
7898 let fields = summary_text.split(' ').collect::<Vec<_>>();
7899 let keys = [
7900 summary_prefix,
7901 "status",
7902 "cli_version",
7903 "artifact_version",
7904 "members",
7905 "safe",
7906 "unsafe",
7907 "mtbdd_nodes",
7908 "mtbdd_terminals",
7909 "assignments_checked",
7910 "reachable_product_states",
7911 "explored_transitions",
7912 "artifact_bytes",
7913 "elapsed_micros",
7914 ];
7915 if fields.len() != keys.len() || fields[0] != keys[0] {
7916 return Err(PredicateApiError::InvalidResponse(
7917 "controller MTBDD summary shape is invalid".to_string(),
7918 ));
7919 }
7920 if token_value(fields[1], keys[1])? != expected_action {
7921 return Err(PredicateApiError::InvalidResponse(
7922 "controller MTBDD response action disagrees".to_string(),
7923 ));
7924 }
7925 let numeric = fields[2..]
7926 .iter()
7927 .zip(&keys[2..])
7928 .map(|(field, key)| canonical_usize(token_value(field, key)?, key))
7929 .collect::<Result<Vec<_>, _>>()?;
7930 if numeric[0] != capabilities.cli_version as usize {
7931 return Err(PredicateApiError::IncompatibleContract(
7932 "controller MTBDD response CLI version changed".to_string(),
7933 ));
7934 }
7935 if numeric[1] != capabilities.plant_artifact_version as usize {
7936 return Err(PredicateApiError::IncompatibleContract(
7937 "controller MTBDD response artifact version changed".to_string(),
7938 ));
7939 }
7940 let member_count = numeric[2];
7941 if member_count == 0
7942 || member_count > capabilities.max_members
7943 || numeric[5] > capabilities.max_nodes
7944 || numeric[6] > capabilities.max_terminals
7945 || numeric[7] > capabilities.max_assignments
7946 || numeric[10] > capabilities.max_artifact_bytes
7947 {
7948 return Err(PredicateApiError::InvalidResponse(
7949 "controller MTBDD response exceeds discovered limits".to_string(),
7950 ));
7951 }
7952 let mut members = Vec::with_capacity(member_count);
7953 for expected_index in 0..member_count {
7954 let line = lines.next().ok_or_else(|| {
7955 PredicateApiError::InvalidResponse(
7956 "controller MTBDD member line is missing".to_string(),
7957 )
7958 })?;
7959 let fields = line.split(' ').collect::<Vec<_>>();
7960 let keys = [
7961 member_prefix,
7962 "index",
7963 "answer",
7964 "horizon",
7965 "bad_frame",
7966 "trace_steps",
7967 "reachable_product_states",
7968 "explored_transitions",
7969 ];
7970 if fields.len() != keys.len() || fields[0] != keys[0] {
7971 return Err(PredicateApiError::InvalidResponse(
7972 "controller MTBDD member shape is invalid".to_string(),
7973 ));
7974 }
7975 let index = canonical_usize(token_value(fields[1], "index")?, "index")?;
7976 if index != expected_index {
7977 return Err(PredicateApiError::InvalidResponse(
7978 "controller MTBDD member order is invalid".to_string(),
7979 ));
7980 }
7981 let answer = match token_value(fields[2], "answer")? {
7982 "SAFE" => ControllerMtbddAnswer::Safe,
7983 "UNSAFE" => ControllerMtbddAnswer::Unsafe,
7984 _ => {
7985 return Err(PredicateApiError::InvalidResponse(
7986 "controller MTBDD member answer is invalid".to_string(),
7987 ));
7988 }
7989 };
7990 let bad = token_value(fields[4], "bad_frame")?;
7991 let bad_frame = if bad == "none" {
7992 None
7993 } else {
7994 Some(canonical_usize(bad, "bad_frame")?)
7995 };
7996 if matches!(answer, ControllerMtbddAnswer::Safe) != bad_frame.is_none() {
7997 return Err(PredicateApiError::InvalidResponse(
7998 "controller MTBDD answer and bad frame disagree".to_string(),
7999 ));
8000 }
8001 let horizon = canonical_usize(token_value(fields[3], "horizon")?, "horizon")?;
8002 let trace_steps =
8003 canonical_usize(token_value(fields[5], "trace_steps")?, "trace_steps")?;
8004 if horizon > capabilities.max_horizon
8005 || match (answer, bad_frame) {
8006 (ControllerMtbddAnswer::Safe, None) => trace_steps != 0,
8007 (ControllerMtbddAnswer::Unsafe, Some(frame)) => {
8008 frame > horizon || trace_steps != frame.saturating_add(1)
8009 }
8010 _ => true,
8011 }
8012 {
8013 return Err(PredicateApiError::InvalidResponse(
8014 "controller MTBDD member trace boundary is invalid".to_string(),
8015 ));
8016 }
8017 members.push(ControllerMtbddMemberResult {
8018 index,
8019 answer,
8020 horizon,
8021 bad_frame,
8022 trace_steps,
8023 reachable_product_states: canonical_usize(
8024 token_value(fields[6], "reachable_product_states")?,
8025 "reachable_product_states",
8026 )?,
8027 explored_transitions: canonical_usize(
8028 token_value(fields[7], "explored_transitions")?,
8029 "explored_transitions",
8030 )?,
8031 });
8032 }
8033 let reachable_total = members
8034 .iter()
8035 .try_fold(0usize, |total, member| {
8036 total.checked_add(member.reachable_product_states)
8037 })
8038 .ok_or_else(|| {
8039 PredicateApiError::InvalidResponse(
8040 "controller MTBDD reachable-state total overflows".to_string(),
8041 )
8042 })?;
8043 let transition_total = members
8044 .iter()
8045 .try_fold(0usize, |total, member| {
8046 total.checked_add(member.explored_transitions)
8047 })
8048 .ok_or_else(|| {
8049 PredicateApiError::InvalidResponse(
8050 "controller MTBDD transition total overflows".to_string(),
8051 )
8052 })?;
8053 if lines.next().is_some()
8054 || members
8055 .iter()
8056 .filter(|member| matches!(member.answer, ControllerMtbddAnswer::Safe))
8057 .count()
8058 != numeric[3]
8059 || members
8060 .iter()
8061 .filter(|member| matches!(member.answer, ControllerMtbddAnswer::Unsafe))
8062 .count()
8063 != numeric[4]
8064 || reachable_total != numeric[8]
8065 || transition_total != numeric[9]
8066 {
8067 return Err(PredicateApiError::InvalidResponse(
8068 "controller MTBDD member totals disagree".to_string(),
8069 ));
8070 }
8071 Ok(ControllerMtbddBatchSummary {
8072 artifact_version: numeric[1] as u32,
8073 safe: numeric[3],
8074 unsafe_count: numeric[4],
8075 mtbdd_nodes: numeric[5],
8076 mtbdd_terminals: numeric[6],
8077 assignments_checked: numeric[7],
8078 reachable_product_states: numeric[8],
8079 explored_transitions: numeric[9],
8080 artifact_bytes: numeric[10],
8081 elapsed_micros: numeric[11],
8082 members,
8083 })
8084 })();
8085 match parsed {
8086 Ok(value) => Ok(Observed { value, metrics }),
8087 Err(error) => {
8088 metrics.status = InvocationStatus::Failed(error.failure_class());
8089 Err(PredicateOperationError {
8090 error: Box::new(error),
8091 metrics,
8092 })
8093 }
8094 }
8095}
8096
8097fn parse_capabilities(line: &str) -> Result<PredicateCapabilities, PredicateApiError> {
8098 let fields = line.trim_end_matches('\n').split(' ').collect::<Vec<_>>();
8099 if fields.len() != 11 || line.contains('\r') || !line.ends_with('\n') {
8100 return Err(PredicateApiError::InvalidResponse(
8101 "capability line shape is not canonical".to_string(),
8102 ));
8103 }
8104 let value = |index: usize, key: &str| -> Result<&str, PredicateApiError> {
8105 fields[index]
8106 .strip_prefix(&format!("{key}="))
8107 .ok_or_else(|| {
8108 PredicateApiError::InvalidResponse(format!("expected capability `{key}`"))
8109 })
8110 };
8111 let number = |index: usize, key: &str| -> Result<u64, PredicateApiError> {
8112 let text = value(index, key)?;
8113 if text.is_empty() || (text.len() > 1 && text.starts_with('0')) {
8114 return Err(PredicateApiError::InvalidResponse(format!(
8115 "capability `{key}` is not canonical decimal"
8116 )));
8117 }
8118 text.parse::<u64>().map_err(|_| {
8119 PredicateApiError::InvalidResponse(format!("capability `{key}` is invalid"))
8120 })
8121 };
8122 let cli_version = u32::try_from(number(0, "predicate_cli_version")?).map_err(|_| {
8123 PredicateApiError::InvalidResponse("predicate CLI version exceeds u32".to_string())
8124 })?;
8125 if cli_version != PREDICATE_API_VERSION {
8126 return Err(PredicateApiError::IncompatibleContract(format!(
8127 "expected CLI v{PREDICATE_API_VERSION}, found v{cli_version}"
8128 )));
8129 }
8130 if value(1, "certificate_versions")? != "1,2" {
8131 return Err(PredicateApiError::IncompatibleContract(
8132 "certificate versions must be 1,2".to_string(),
8133 ));
8134 }
8135 if number(2, "portfolio_certificate_version")? != 1 {
8136 return Err(PredicateApiError::IncompatibleContract(
8137 "portfolio certificate version must be 1".to_string(),
8138 ));
8139 }
8140 let proof_format = value(3, "proof_format")?.to_string();
8141 if proof_format != "varisat-native-0.2.2" {
8142 return Err(PredicateApiError::IncompatibleContract(format!(
8143 "unsupported proof format `{proof_format}`"
8144 )));
8145 }
8146 let as_usize = |index: usize, key: &str| -> Result<usize, PredicateApiError> {
8147 usize::try_from(number(index, key)?).map_err(|_| {
8148 PredicateApiError::InvalidResponse(format!("capability `{key}` exceeds usize"))
8149 })
8150 };
8151 Ok(PredicateCapabilities {
8152 cli_version,
8153 certificate_versions: vec![CertificateVersion::V1, CertificateVersion::V2],
8154 portfolio_certificate_version: CertificateVersion::V1,
8155 proof_format,
8156 min_relevant_inputs: as_usize(4, "min_relevant_inputs")?,
8157 max_relevant_inputs: as_usize(5, "max_relevant_inputs")?,
8158 max_latches: as_usize(6, "max_latches")?,
8159 max_horizon: as_usize(7, "max_horizon")?,
8160 max_certificate_v2_bytes: number(8, "max_certificate_v2_bytes")?,
8161 max_proof_bytes: as_usize(9, "max_proof_bytes")?,
8162 max_total_proof_bytes: as_usize(10, "max_total_proof_bytes")?,
8163 })
8164}
8165
8166fn parse_event_contract_capabilities(
8167 line: &str,
8168) -> Result<EventContractCapabilities, EventContractApiError> {
8169 let fields = line.trim_end_matches('\n').split(' ').collect::<Vec<_>>();
8170 if fields.len() != 13 || line.contains('\r') || !line.ends_with('\n') {
8171 return Err(PredicateApiError::InvalidResponse(
8172 "event-contract capability line shape is not canonical".to_string(),
8173 ));
8174 }
8175 let value = |index: usize, key: &str| -> Result<&str, PredicateApiError> {
8176 fields[index]
8177 .strip_prefix(&format!("{key}="))
8178 .ok_or_else(|| {
8179 PredicateApiError::InvalidResponse(format!(
8180 "expected event-contract capability `{key}`"
8181 ))
8182 })
8183 };
8184 let number = |index: usize, key: &str| -> Result<u64, PredicateApiError> {
8185 let text = value(index, key)?;
8186 if text.is_empty()
8187 || !text.bytes().all(|byte| byte.is_ascii_digit())
8188 || (text.len() > 1 && text.starts_with('0'))
8189 {
8190 return Err(PredicateApiError::InvalidResponse(format!(
8191 "event-contract capability `{key}` is not canonical decimal"
8192 )));
8193 }
8194 text.parse::<u64>().map_err(|_| {
8195 PredicateApiError::InvalidResponse(format!(
8196 "event-contract capability `{key}` is invalid"
8197 ))
8198 })
8199 };
8200 let as_usize = |index: usize, key: &str| -> Result<usize, PredicateApiError> {
8201 usize::try_from(number(index, key)?).map_err(|_| {
8202 PredicateApiError::InvalidResponse(format!(
8203 "event-contract capability `{key}` exceeds usize"
8204 ))
8205 })
8206 };
8207 let cli_version = u32::try_from(number(0, "event_contract_cli_version")?).map_err(|_| {
8208 PredicateApiError::InvalidResponse("event-contract CLI version exceeds u32".to_string())
8209 })?;
8210 if cli_version != EVENT_CONTRACT_API_VERSION {
8211 return Err(PredicateApiError::IncompatibleContract(format!(
8212 "expected event-contract CLI v{EVENT_CONTRACT_API_VERSION}, found v{cli_version}"
8213 )));
8214 }
8215 let certificate_version = u32::try_from(number(1, "certificate_version")?).map_err(|_| {
8216 PredicateApiError::InvalidResponse(
8217 "event-contract certificate version exceeds u32".to_string(),
8218 )
8219 })?;
8220 let portfolio_version = u32::try_from(number(2, "portfolio_version")?).map_err(|_| {
8221 PredicateApiError::InvalidResponse(
8222 "event-contract portfolio version exceeds u32".to_string(),
8223 )
8224 })?;
8225 if certificate_version != 3 || portfolio_version != 1 {
8226 return Err(PredicateApiError::IncompatibleContract(
8227 "event-contract certificate v3 and portfolio v1 are required".to_string(),
8228 ));
8229 }
8230 let semantics = value(3, "semantics")?.to_string();
8231 if semantics != "bounded-named-cnf-terminal-bad-avoidance" {
8232 return Err(PredicateApiError::IncompatibleContract(format!(
8233 "unsupported event-contract semantics `{semantics}`"
8234 )));
8235 }
8236 let proof_format = value(4, "proof_format")?.to_string();
8237 if proof_format != "varisat-native-0.2.2" {
8238 return Err(PredicateApiError::IncompatibleContract(format!(
8239 "unsupported proof format `{proof_format}`"
8240 )));
8241 }
8242 Ok(EventContractCapabilities {
8243 cli_version,
8244 certificate_version,
8245 portfolio_version,
8246 semantics,
8247 proof_format,
8248 min_relevant_inputs: as_usize(5, "min_relevant_inputs")?,
8249 max_relevant_inputs: as_usize(6, "max_relevant_inputs")?,
8250 max_latches: as_usize(7, "max_latches")?,
8251 max_horizon: as_usize(8, "max_horizon")?,
8252 max_contract_bytes: number(9, "max_contract_bytes")?,
8253 max_certificate_bytes: number(10, "max_certificate_bytes")?,
8254 max_proof_bytes: as_usize(11, "max_proof_bytes")?,
8255 max_total_proof_bytes: as_usize(12, "max_total_proof_bytes")?,
8256 })
8257}
8258
8259#[cfg(test)]
8260mod tests {
8261 use super::*;
8262
8263 #[test]
8264 fn capability_parser_accepts_v1_and_rejects_drift() {
8265 let canonical = "predicate_cli_version=1 certificate_versions=1,2 portfolio_certificate_version=1 proof_format=varisat-native-0.2.2 min_relevant_inputs=9 max_relevant_inputs=16 max_latches=4 max_horizon=64 max_certificate_v2_bytes=16777216 max_proof_bytes=1048576 max_total_proof_bytes=8388608\n";
8266 let parsed = parse_capabilities(canonical).unwrap();
8267 assert_eq!(parsed.cli_version, 1);
8268 assert_eq!(parsed.certificate_versions.len(), 2);
8269 assert!(matches!(
8270 parse_capabilities(&canonical.replacen(
8271 "predicate_cli_version=1",
8272 "predicate_cli_version=2",
8273 1
8274 )),
8275 Err(PredicateApiError::IncompatibleContract(_))
8276 ));
8277 assert!(parse_capabilities(&canonical.replace(" max_latches=4", "")).is_err());
8278 }
8279
8280 #[test]
8281 fn event_contract_capability_parser_accepts_v1_and_rejects_drift() {
8282 let canonical = "event_contract_cli_version=1 certificate_version=3 portfolio_version=1 semantics=bounded-named-cnf-terminal-bad-avoidance proof_format=varisat-native-0.2.2 min_relevant_inputs=9 max_relevant_inputs=16 max_latches=4 max_horizon=64 max_contract_bytes=1048576 max_certificate_bytes=33554432 max_proof_bytes=1048576 max_total_proof_bytes=8388608\n";
8283 let parsed = parse_event_contract_capabilities(canonical).unwrap();
8284 assert_eq!(parsed.cli_version, 1);
8285 assert_eq!(parsed.certificate_version, 3);
8286 assert!(matches!(
8287 parse_event_contract_capabilities(&canonical.replacen(
8288 "event_contract_cli_version=1",
8289 "event_contract_cli_version=2",
8290 1
8291 )),
8292 Err(PredicateApiError::IncompatibleContract(_))
8293 ));
8294 assert!(
8295 parse_event_contract_capabilities(&canonical.replace(" max_latches=4", "")).is_err()
8296 );
8297 }
8298
8299 #[test]
8300 fn revision_impact_capability_parser_is_strict_and_fail_closed() {
8301 let canonical = "revision_impact_cli_version=2 impact_version=1 query_manifest_version=1 max_query_manifest_bytes=16384 max_input_bytes=67108864 max_evidence_bytes=16777216 max_bundle_bytes=67108864 max_atoms=8 max_combinations=256 max_queries=32 semantics=exact-counterfactual-v1 work_schema=verification-v1 query_schema=transition-semantic-set-v1 routing=none fallback=none unsupported=fail-closed\n";
8302 let parsed = parse_revision_impact_capabilities(canonical).unwrap();
8303 assert_eq!(parsed.cli_version, 2);
8304 assert_eq!(parsed.max_bundle_bytes, 64 * 1024 * 1024);
8305 for hostile in [
8306 canonical.replace("impact_version=1", "impact_version=2"),
8307 canonical.replace("max_atoms=8", "max_atoms=08"),
8308 canonical.replace("max_queries=32", "max_queries=33"),
8309 canonical.replace("routing=none", "routing=heuristic"),
8310 canonical.replace("work_schema=verification-v1", "work_schema=timing-v1"),
8311 canonical.replace(
8312 "query_schema=transition-semantic-set-v1",
8313 "query_schema=summary-v1",
8314 ),
8315 canonical.replace("fallback=none", "fallback=exact"),
8316 canonical.replace("unsupported=fail-closed", "unsupported=ignore"),
8317 canonical.replace('\n', "\r\n"),
8318 canonical.trim_end().to_string(),
8319 format!("{canonical}unexpected=1\n"),
8320 ] {
8321 assert!(parse_revision_impact_capabilities(&hostile).is_err());
8322 }
8323 assert!(canonical_node_list(&[]).is_err());
8324 assert!(canonical_node_list(&[0]).is_err());
8325 assert!(canonical_node_list(&[7, 7]).is_err());
8326 assert!(canonical_node_list(&[10, 7]).is_err());
8327 assert_eq!(canonical_node_list(&[7, 10]).unwrap(), "7,10");
8328 }
8329
8330 #[test]
8331 fn btor2_channel_property_capability_parser_is_strict_and_fail_closed() {
8332 let canonical = "btor2_channel_property_cli_version=1 artifact_version=1 query_manifest_version=1 policy_version=1 max_query_manifest_bytes=262144 max_policy_bytes=4096 max_model_bytes=8388608 max_channels=64 max_queries=4096 max_evidence_bytes=67108864 max_artifact_bytes=69206016 max_projected_work=100000000000 refusal_exit=3 routing=static-explicit-or-bitblast fallback=exact result_on_refusal=none refusal_schema=reason-v1 unsupported=fail-closed verification=source-replay\n";
8333 let parsed = parse_btor2_channel_property_capabilities(canonical).unwrap();
8334 assert_eq!(parsed.cli_version, 1);
8335 assert_eq!(parsed.max_artifact_bytes, 66 * 1024 * 1024);
8336 for hostile in [
8337 canonical.replace("artifact_version=1", "artifact_version=2"),
8338 canonical.replace("max_channels=64", "max_channels=064"),
8339 canonical.replace("max_queries=4096", "max_queries=4095"),
8340 canonical.replace("refusal_exit=3", "refusal_exit=2"),
8341 canonical.replace("fallback=exact", "fallback=partial"),
8342 canonical.replace("result_on_refusal=none", "result_on_refusal=SAFE"),
8343 canonical.replace("verification=source-replay", "verification=trusted"),
8344 canonical.replace('\n', "\r\n"),
8345 canonical.trim_end().to_string(),
8346 format!("{canonical}unexpected=1\n"),
8347 ] {
8348 assert!(parse_btor2_channel_property_capabilities(&hostile).is_err());
8349 }
8350 }
8351
8352 #[test]
8353 fn btor2_channel_property_phase_parser_is_strict_and_fail_closed() {
8354 let base = parse_btor2_channel_property_capabilities("btor2_channel_property_cli_version=1 artifact_version=1 query_manifest_version=1 policy_version=1 max_query_manifest_bytes=262144 max_policy_bytes=4096 max_model_bytes=8388608 max_channels=64 max_queries=4096 max_evidence_bytes=67108864 max_artifact_bytes=69206016 max_projected_work=100000000000 refusal_exit=3 routing=static-explicit-or-bitblast fallback=exact result_on_refusal=none refusal_schema=reason-v1 unsupported=fail-closed verification=source-replay\n").unwrap();
8355 let capability = "btor2_channel_property_observability_cli_version=1 base_cli_version=1 phase_metrics_version=1 phases=input,structural-admission,preflight,proof-construction,encoding,artifact-decode,source-replay,publication timing_calibration=none correctness_dependency=none partial_metrics_on_failure=none unsupported=fail-closed\n";
8356 let parsed =
8357 parse_btor2_channel_property_observability_capabilities(capability, &base).unwrap();
8358 for hostile in [
8359 capability.replace("phase_metrics_version=1", "phase_metrics_version=2"),
8360 capability.replace("proof-construction", "proof-generation"),
8361 capability.replace(
8362 "correctness_dependency=none",
8363 "correctness_dependency=timing",
8364 ),
8365 capability.replace(
8366 "partial_metrics_on_failure=none",
8367 "partial_metrics_on_failure=yes",
8368 ),
8369 capability.replace('\n', "\r\n"),
8370 capability.trim_end().to_string(),
8371 format!("{capability}unexpected=1\n"),
8372 ] {
8373 assert!(
8374 parse_btor2_channel_property_observability_capabilities(&hostile, &base).is_err()
8375 );
8376 }
8377
8378 let canonical = "btor2-channel-property-phases status=MEASURED observability_cli_version=1 phase_metrics_version=1 operation=certify input_micros=1 structural_admission_micros=2 preflight_micros=3 proof_construction_micros=4 encoding_micros=5 artifact_decode_micros=6 source_replay_micros=7 publication_micros=8 total_micros=40 timing_calibration=none correctness_dependency=none\n";
8379 let phases = parse_btor2_channel_property_phase_line(
8380 canonical,
8381 Btor2ChannelPropertyObservedOperation::Certify,
8382 &parsed,
8383 )
8384 .unwrap();
8385 assert_eq!(phases.total_micros, 40);
8386 for hostile in [
8387 canonical.replace("operation=certify", "operation=verify"),
8388 canonical.replace("input_micros=1", "input_micros=01"),
8389 canonical.replace("total_micros=40", "total_micros=35"),
8390 canonical.replace("timing_calibration=none", "timing_calibration=local"),
8391 canonical.replace(
8392 "correctness_dependency=none",
8393 "correctness_dependency=timing",
8394 ),
8395 canonical.replace("status=MEASURED", "status=PARTIAL"),
8396 canonical.replace('\n', "\r\n"),
8397 canonical.trim_end().to_string(),
8398 format!("{canonical}unexpected=1\n"),
8399 ] {
8400 assert!(
8401 parse_btor2_channel_property_phase_line(
8402 &hostile,
8403 Btor2ChannelPropertyObservedOperation::Certify,
8404 &parsed,
8405 )
8406 .is_err()
8407 );
8408 }
8409
8410 let verify = "btor2-channel-property-phases status=MEASURED observability_cli_version=1 phase_metrics_version=1 operation=verify input_micros=1 structural_admission_micros=0 preflight_micros=0 proof_construction_micros=0 encoding_micros=0 artifact_decode_micros=2 source_replay_micros=3 publication_micros=0 total_micros=6 timing_calibration=none correctness_dependency=none\n";
8411 assert!(
8412 parse_btor2_channel_property_phase_line(
8413 verify,
8414 Btor2ChannelPropertyObservedOperation::Verify,
8415 &parsed,
8416 )
8417 .is_ok()
8418 );
8419 assert!(
8420 parse_btor2_channel_property_phase_line(
8421 &verify.replace("preflight_micros=0", "preflight_micros=1"),
8422 Btor2ChannelPropertyObservedOperation::Verify,
8423 &parsed,
8424 )
8425 .is_err()
8426 );
8427 }
8428
8429 #[test]
8430 fn controller_mtbdd_capability_parser_is_strict_and_fail_closed() {
8431 let canonical = "controller_mtbdd_cli_version=1 mtbdd_version=1 plant_artifact_version=1 manifest_version=1 max_manifest_bytes=65536 max_artifact_bytes=33554432 max_members=64 max_state_bits=20 max_inputs=32 max_outputs=8 max_nodes=1048576 max_terminals=1048576 max_assignments=1048576 max_horizon=4096 unsupported=fail-closed\n";
8432 let parsed = parse_controller_mtbdd_capabilities(canonical).unwrap();
8433 assert_eq!(parsed.cli_version, 1);
8434 assert_eq!(parsed.max_outputs, 8);
8435 assert!(
8436 parse_controller_mtbdd_capabilities(&canonical.replacen(
8437 "controller_mtbdd_cli_version=1",
8438 "controller_mtbdd_cli_version=4294967297",
8439 1
8440 ))
8441 .is_err()
8442 );
8443 assert!(
8444 parse_controller_mtbdd_capabilities(
8445 &canonical.replace("max_outputs=8", "max_outputs=08")
8446 )
8447 .is_err()
8448 );
8449 assert!(
8450 parse_controller_mtbdd_capabilities(
8451 &canonical.replace("max_members=64", "max_members=0")
8452 )
8453 .is_err()
8454 );
8455 assert!(
8456 parse_controller_mtbdd_capabilities(
8457 &canonical.replace("unsupported=fail-closed", "unsupported=best-effort")
8458 )
8459 .is_err()
8460 );
8461 assert!(parse_controller_mtbdd_capabilities(&canonical.replace('\n', "\r\n")).is_err());
8462 }
8463
8464 #[test]
8465 fn controller_proof_mtbdd_capability_parser_is_strict() {
8466 let canonical = "controller_proof_mtbdd_cli_version=1 mtbdd_version=1 equivalence_proof_version=1 plant_artifact_version=1 manifest_version=1 max_manifest_bytes=65536 max_artifact_bytes=16777216 max_equivalence_artifact_bytes=2097152 max_unsat_proof_bytes=1048576 max_members=64 max_state_bits=6 max_inputs=12 max_outputs=8 max_nodes=512 max_terminals=1024 max_horizon=1024 verification=unsat-miter exhaustive_replay=no unsupported=fail-closed\n";
8467 let parsed = parse_controller_proof_mtbdd_capabilities(canonical).unwrap();
8468 assert_eq!(parsed.equivalence_proof_version, 1);
8469 assert_eq!(parsed.max_equivalence_artifact_bytes, 2_097_152);
8470 assert_eq!(parsed.max_unsat_proof_bytes, 1_048_576);
8471 for hostile in [
8472 canonical.replace("verification=unsat-miter", "verification=trusted"),
8473 canonical.replace("exhaustive_replay=no", "exhaustive_replay=yes"),
8474 canonical.replace(
8475 "max_equivalence_artifact_bytes=2097152",
8476 "max_equivalence_artifact_bytes=0",
8477 ),
8478 canonical.replace("max_unsat_proof_bytes=1048576", "max_unsat_proof_bytes=0"),
8479 canonical.replace("unsupported=fail-closed", "unsupported=best-effort"),
8480 canonical.replace('\n', "\r\n"),
8481 ] {
8482 assert!(parse_controller_proof_mtbdd_capabilities(&hostile).is_err());
8483 }
8484 }
8485
8486 #[test]
8487 fn controller_split_evidence_capability_parser_is_strict() {
8488 let canonical = "controller_split_evidence_cli_version=1 controller_artifact_version=1 plant_artifact_version=1 manifest_version=1 max_manifest_bytes=65536 max_artifact_bytes=16777216 max_batches=64 admission=once verification=unsat-miter exhaustive_replay=no source_binding=sha256 obligation_binding=complete-ordered unsupported=fail-closed\n";
8489 let parsed = parse_controller_split_evidence_capabilities(canonical).unwrap();
8490 assert_eq!(parsed.cli_version, 1);
8491 assert_eq!(parsed.max_batches, 64);
8492 for hostile in [
8493 canonical.replace("admission=once", "admission=per-batch"),
8494 canonical.replace("verification=unsat-miter", "verification=trusted"),
8495 canonical.replace("exhaustive_replay=no", "exhaustive_replay=yes"),
8496 canonical.replace("source_binding=sha256", "source_binding=path"),
8497 canonical.replace(
8498 "obligation_binding=complete-ordered",
8499 "obligation_binding=partial",
8500 ),
8501 canonical.replace("max_batches=64", "max_batches=0"),
8502 canonical.replace("max_batches=64", "max_batches=064"),
8503 canonical.replace("max_batches=64", "max_batches=65"),
8504 canonical.replace("max_artifact_bytes=16777216", "max_artifact_bytes=16777217"),
8505 canonical.replace("unsupported=fail-closed", "unsupported=best-effort"),
8506 canonical.replace('\n', "\r\n"),
8507 ] {
8508 assert!(parse_controller_split_evidence_capabilities(&hostile).is_err());
8509 }
8510 }
8511
8512 #[test]
8513 fn controller_split_resource_capability_parser_is_strict() {
8514 let canonical = "controller_split_resource_cli_version=1 policy_version=1 controller_envelope_version=1 plant_envelope_version=1 controller_artifact_version=1 plant_artifact_version=1 manifest_version=1 max_policy_bytes=4096 max_controller_artifact_bytes=16777216 max_unsat_proof_bytes=1048576 max_plant_artifact_bytes=16777216 max_batches=64 max_members_per_batch=64 max_horizon=1024 max_product_states=4096 refusal_exit=3 admission=once verification=unsat-miter exhaustive_replay=no accounting=conservative-static-per-batch-and-total timing_calibration=none result_on_refusal=none refusal_schema=split-reason-v1 unsupported=fail-closed\n";
8515 let parsed = parse_controller_split_resource_capabilities(canonical).unwrap();
8516 assert_eq!(parsed.policy_version, 1);
8517 assert_eq!(parsed.max_batches, 64);
8518 for hostile in [
8519 canonical.replace("admission=once", "admission=per-batch"),
8520 canonical.replace("verification=unsat-miter", "verification=trusted"),
8521 canonical.replace(
8522 "accounting=conservative-static-per-batch-and-total",
8523 "accounting=measured",
8524 ),
8525 canonical.replace("timing_calibration=none", "timing_calibration=per-formula"),
8526 canonical.replace("result_on_refusal=none", "result_on_refusal=safe"),
8527 canonical.replace("refusal_exit=3", "refusal_exit=2"),
8528 canonical.replace("max_batches=64", "max_batches=65"),
8529 canonical.replace("max_policy_bytes=4096", "max_policy_bytes=0"),
8530 canonical.replace("refusal_schema=split-reason-v1", "refusal_schema=free-text"),
8531 canonical.replace('\n', "\r\n"),
8532 ] {
8533 assert!(parse_controller_split_resource_capabilities(&hostile).is_err());
8534 }
8535 }
8536
8537 #[test]
8538 fn controller_split_observability_capability_parser_is_strict() {
8539 let resource = "controller_split_resource_cli_version=1 policy_version=1 controller_envelope_version=1 plant_envelope_version=1 controller_artifact_version=1 plant_artifact_version=1 manifest_version=1 max_policy_bytes=4096 max_controller_artifact_bytes=16777216 max_unsat_proof_bytes=1048576 max_plant_artifact_bytes=16777216 max_batches=64 max_members_per_batch=64 max_horizon=1024 max_product_states=4096 refusal_exit=3 admission=once verification=unsat-miter exhaustive_replay=no accounting=conservative-static-per-batch-and-total timing_calibration=none result_on_refusal=none refusal_schema=split-reason-v1 unsupported=fail-closed";
8540 let observability = "controller_split_observability_cli_version=1 base_cli_version=1 phase_metrics_version=1 phases=policy-and-input,controller-admission,complete-set-preflight,semantic-replay counters=controller-admissions,manifest-loads,plant-artifact-reads,resource-assessments,batch-verifications,buffered-result-rows,prepared-batches,prepared-members,controller-evidence-bytes,total-plant-artifact-bytes,total-transition-evaluation-bound timing_calibration=none partial_metrics_on_failure=none result_on_refusal=none unsupported=fail-closed";
8541 let canonical = format!("{resource}\n{observability}\n");
8542 let parsed = parse_controller_split_observability_capabilities(&canonical).unwrap();
8543 assert_eq!(parsed.resource.cli_version, 1);
8544 assert_eq!(parsed.cli_version, 1);
8545 assert_eq!(parsed.phase_metrics_version, 1);
8546 for hostile in [
8547 canonical.replace("phase_metrics_version=1", "phase_metrics_version=2"),
8548 canonical.replace("semantic-replay", "trusted-replay"),
8549 canonical.replace("manifest-loads", "manifest-hints"),
8550 canonical.replace(
8551 "partial_metrics_on_failure=none",
8552 "partial_metrics_on_failure=yes",
8553 ),
8554 canonical.replace("timing_calibration=none", "timing_calibration=per-formula"),
8555 canonical.replace('\n', "\r\n"),
8556 format!("{resource}\n"),
8557 format!("{observability}\n{resource}\n"),
8558 ] {
8559 assert!(parse_controller_split_observability_capabilities(&hostile).is_err());
8560 }
8561 }
8562
8563 #[test]
8564 fn controller_split_allocation_capability_parser_is_strict() {
8565 let resource = "controller_split_resource_cli_version=1 policy_version=1 controller_envelope_version=1 plant_envelope_version=1 controller_artifact_version=1 plant_artifact_version=1 manifest_version=1 max_policy_bytes=4096 max_controller_artifact_bytes=16777216 max_unsat_proof_bytes=1048576 max_plant_artifact_bytes=16777216 max_batches=64 max_members_per_batch=64 max_horizon=1024 max_product_states=4096 refusal_exit=3 admission=once verification=unsat-miter exhaustive_replay=no accounting=conservative-static-per-batch-and-total timing_calibration=none result_on_refusal=none refusal_schema=split-reason-v1 unsupported=fail-closed";
8566 let observability = "controller_split_observability_cli_version=1 base_cli_version=1 phase_metrics_version=1 phases=policy-and-input,controller-admission,complete-set-preflight,semantic-replay counters=controller-admissions,manifest-loads,plant-artifact-reads,resource-assessments,batch-verifications,buffered-result-rows,prepared-batches,prepared-members,controller-evidence-bytes,total-plant-artifact-bytes,total-transition-evaluation-bound timing_calibration=none partial_metrics_on_failure=none result_on_refusal=none unsupported=fail-closed";
8567 let allocation = "controller_split_allocation_observability_cli_version=1 base_observability_cli_version=1 allocator=system scope=policy-through-replay counters=allocation-calls,allocated-bytes,deallocation-calls,deallocated-bytes,reallocation-calls,reallocated-bytes overflow=fail-closed timing_calibration=none partial_metrics_on_failure=none result_on_refusal=none unsupported=fail-closed";
8568 let canonical = format!("{resource}\n{observability}\n{allocation}\n");
8569 let parsed =
8570 parse_controller_split_allocation_observability_capabilities(&canonical).unwrap();
8571 assert_eq!(parsed.cli_version, 1);
8572 assert_eq!(parsed.observability.cli_version, 1);
8573 for hostile in [
8574 canonical.replace("allocator=system", "allocator=jemalloc"),
8575 canonical.replace("scope=policy-through-replay", "scope=whole-process"),
8576 canonical.replace("allocated-bytes", "live-bytes"),
8577 canonical.replace("overflow=fail-closed", "overflow=saturate"),
8578 canonical.replace(
8579 "base_observability_cli_version=1",
8580 "base_observability_cli_version=2",
8581 ),
8582 canonical.replace('\n', "\r\n"),
8583 format!("{resource}\n{observability}\n"),
8584 ] {
8585 assert!(
8586 parse_controller_split_allocation_observability_capabilities(&hostile).is_err()
8587 );
8588 }
8589 }
8590
8591 #[test]
8592 fn controller_split_cache_capability_parser_is_strict() {
8593 let resource = "controller_split_resource_cli_version=1 policy_version=1 controller_envelope_version=1 plant_envelope_version=1 controller_artifact_version=1 plant_artifact_version=1 manifest_version=1 max_policy_bytes=4096 max_controller_artifact_bytes=16777216 max_unsat_proof_bytes=1048576 max_plant_artifact_bytes=16777216 max_batches=64 max_members_per_batch=64 max_horizon=1024 max_product_states=4096 refusal_exit=3 admission=once verification=unsat-miter exhaustive_replay=no accounting=conservative-static-per-batch-and-total timing_calibration=none result_on_refusal=none refusal_schema=split-reason-v1 unsupported=fail-closed";
8594 let observability = "controller_split_observability_cli_version=1 base_cli_version=1 phase_metrics_version=1 phases=policy-and-input,controller-admission,complete-set-preflight,semantic-replay counters=controller-admissions,manifest-loads,plant-artifact-reads,resource-assessments,batch-verifications,buffered-result-rows,prepared-batches,prepared-members,controller-evidence-bytes,total-plant-artifact-bytes,total-transition-evaluation-bound timing_calibration=none partial_metrics_on_failure=none result_on_refusal=none unsupported=fail-closed";
8595 let allocation = "controller_split_allocation_observability_cli_version=1 base_observability_cli_version=1 allocator=system scope=policy-through-replay counters=allocation-calls,allocated-bytes,deallocation-calls,deallocated-bytes,reallocation-calls,reallocated-bytes overflow=fail-closed timing_calibration=none partial_metrics_on_failure=none result_on_refusal=none unsupported=fail-closed";
8596 let cache = "controller_split_cache_observability_cli_version=1 base_allocation_observability_cli_version=1 scope=semantic-replay key=manifest-snapshot,resource-assessment,result-sha256 counters=lookups,hits,misses,entries integrity_preflight=required overflow=fail-closed timing_calibration=none partial_metrics_on_failure=none result_on_refusal=none unsupported=fail-closed";
8597 let canonical = format!("{resource}\n{observability}\n{allocation}\n{cache}\n");
8598 let parsed = parse_controller_split_cache_observability_capabilities(&canonical).unwrap();
8599 assert_eq!(parsed.cli_version, 1);
8600 assert_eq!(parsed.allocation_observability.cli_version, 1);
8601 for hostile in [
8602 canonical.replace("scope=semantic-replay", "scope=preflight"),
8603 canonical.replace("manifest-snapshot", "manifest-path"),
8604 canonical.replace(
8605 "integrity_preflight=required",
8606 "integrity_preflight=optional",
8607 ),
8608 canonical.replace("hits,misses", "misses,hits"),
8609 canonical.replace("overflow=fail-closed", "overflow=saturate"),
8610 canonical.replace(
8611 "base_allocation_observability_cli_version=1",
8612 "base_allocation_observability_cli_version=2",
8613 ),
8614 canonical.replace('\n', "\r\n"),
8615 format!("{resource}\n{observability}\n{allocation}\n"),
8616 ] {
8617 assert!(parse_controller_split_cache_observability_capabilities(&hostile).is_err());
8618 }
8619 }
8620
8621 #[test]
8622 fn controller_plant_portfolio_capability_parser_is_strict() {
8623 let canonical = "controller_plant_portfolio_cli_version=1 artifact_version=1 manifest_version=1 max_manifest_bytes=65536 max_artifact_bytes=16777216 max_members=64 backends=mtbdd,direct-exact routing=static fallback=exact unsupported=fail-closed\n";
8624 let parsed = parse_controller_plant_portfolio_capabilities(canonical).unwrap();
8625 assert_eq!(parsed.cli_version, 1);
8626 assert_eq!(parsed.max_members, 64);
8627 assert!(
8628 parse_controller_plant_portfolio_capabilities(
8629 &canonical.replace("routing=static", "routing=timed")
8630 )
8631 .is_err()
8632 );
8633 assert!(
8634 parse_controller_plant_portfolio_capabilities(
8635 &canonical.replace("max_members=64", "max_members=064")
8636 )
8637 .is_err()
8638 );
8639 assert!(
8640 parse_controller_plant_portfolio_capabilities(&canonical.replace('\n', "\r\n"))
8641 .is_err()
8642 );
8643 }
8644
8645 #[test]
8646 fn controller_plant_resource_capability_parser_is_strict() {
8647 let canonical = "controller_plant_resource_cli_version=1 policy_version=1 envelope_version=1 manifest_version=1 portfolio_artifact_version=1 max_policy_bytes=4096 max_artifact_bytes=16777216 max_members=64 max_horizon=1024 max_product_states=4096 refusal_exit=3 accounting=conservative-static timing_calibration=none result_on_refusal=none refusal_schema=reason-v1 unsupported=fail-closed\n";
8648 let parsed = parse_controller_plant_resource_capabilities(canonical).unwrap();
8649 assert_eq!(parsed.envelope_version, 1);
8650 assert_eq!(parsed.max_product_states, 4096);
8651 for hostile in [
8652 canonical.replace("accounting=conservative-static", "accounting=measured"),
8653 canonical.replace("timing_calibration=none", "timing_calibration=per-formula"),
8654 canonical.replace("result_on_refusal=none", "result_on_refusal=safe"),
8655 canonical.replace("refusal_exit=3", "refusal_exit=2"),
8656 canonical.replace("refusal_schema=reason-v1", "refusal_schema=free-text"),
8657 canonical.replace("max_policy_bytes=4096", "max_policy_bytes=0"),
8658 canonical.replace("max_members=64", "max_members=064"),
8659 canonical.replace('\n', "\r\n"),
8660 ] {
8661 assert!(parse_controller_plant_resource_capabilities(&hostile).is_err());
8662 }
8663 }
8664
8665 #[test]
8666 fn controller_proof_mtbdd_resource_capability_parser_is_strict() {
8667 let canonical = "controller_proof_mtbdd_resource_cli_version=1 policy_version=1 envelope_version=1 manifest_version=1 artifact_version=1 max_policy_bytes=4096 max_artifact_bytes=16777216 max_equivalence_artifact_bytes=2097152 max_unsat_proof_bytes=1048576 max_members=64 max_horizon=1024 max_product_states=4096 refusal_exit=3 verification=unsat-miter exhaustive_replay=no accounting=conservative-static timing_calibration=none result_on_refusal=none refusal_schema=proof-reason-v1 unsupported=fail-closed\n";
8668 let parsed = parse_controller_proof_mtbdd_resource_capabilities(canonical).unwrap();
8669 assert_eq!(parsed.max_equivalence_artifact_bytes, 2_097_152);
8670 assert_eq!(parsed.max_unsat_proof_bytes, 1_048_576);
8671 for hostile in [
8672 canonical.replace("verification=unsat-miter", "verification=trusted"),
8673 canonical.replace("exhaustive_replay=no", "exhaustive_replay=yes"),
8674 canonical.replace("accounting=conservative-static", "accounting=measured"),
8675 canonical.replace("result_on_refusal=none", "result_on_refusal=safe"),
8676 canonical.replace("refusal_schema=proof-reason-v1", "refusal_schema=free-text"),
8677 canonical.replace("max_unsat_proof_bytes=1048576", "max_unsat_proof_bytes=0"),
8678 canonical.replace("max_members=64", "max_members=064"),
8679 canonical.replace('\n', "\r\n"),
8680 ] {
8681 assert!(parse_controller_proof_mtbdd_resource_capabilities(&hostile).is_err());
8682 }
8683 }
8684
8685 #[test]
8686 fn controller_proof_mtbdd_portfolio_capability_parser_is_strict() {
8687 let canonical = "controller_proof_mtbdd_portfolio_cli_version=1 policy_version=1 envelope_version=1 artifact_version=1 proof_artifact_version=1 direct_artifact_version=1 manifest_version=1 source_model_attestation_version=1 max_policy_bytes=4096 max_artifact_bytes=16777216 max_equivalence_artifact_bytes=2097152 max_unsat_proof_bytes=1048576 max_members=64 max_horizon=1024 max_product_states=4096 max_attestation_bytes=65536 refusal_exit=3 backends=proof-mtbdd,direct-exact routing=static fallback=exact proof_failure=fail-closed attested_verification=required accounting=conservative-static timing_calibration=none result_on_refusal=none refusal_schema=proof-reason-v1 unsupported=fail-closed\n";
8688 let parsed = parse_controller_proof_mtbdd_portfolio_capabilities(canonical).unwrap();
8689 assert_eq!(parsed.artifact_version, 1);
8690 assert_eq!(parsed.max_unsat_proof_bytes, 1_048_576);
8691 for hostile in [
8692 canonical.replace("backends=proof-mtbdd,direct-exact", "backends=direct-exact"),
8693 canonical.replace("routing=static", "routing=timed"),
8694 canonical.replace("fallback=exact", "fallback=heuristic"),
8695 canonical.replace("proof_failure=fail-closed", "proof_failure=fallback"),
8696 canonical.replace(
8697 "attested_verification=required",
8698 "attested_verification=optional",
8699 ),
8700 canonical.replace("result_on_refusal=none", "result_on_refusal=safe"),
8701 canonical.replace("max_members=64", "max_members=064"),
8702 canonical.replace('\n', "\r\n"),
8703 ] {
8704 assert!(parse_controller_proof_mtbdd_portfolio_capabilities(&hostile).is_err());
8705 }
8706 }
8707
8708 #[test]
8709 fn result_parser_rejects_missing_ambiguous_and_unknown_answers() {
8710 assert_eq!(
8711 parse_result("status=VERIFIED result=avoidable\n").unwrap(),
8712 PredicateResult::Avoidable
8713 );
8714 assert!(parse_result("status=VERIFIED\n").is_err());
8715 assert_eq!(
8716 parse_result("result=avoidable result=avoidable\n").unwrap(),
8717 PredicateResult::Avoidable
8718 );
8719 assert!(parse_result("result=avoidable result=unavoidable\n").is_err());
8720 assert!(parse_result("result=maybe\n").is_err());
8721 }
8722
8723 #[test]
8724 fn execution_policy_rejects_zero_and_excessive_bounds() {
8725 assert!(ExecutionPolicy::new(Duration::ZERO, 1).is_err());
8726 assert!(ExecutionPolicy::new(Duration::from_secs(1), 0).is_err());
8727 assert!(ExecutionPolicy::new(Duration::from_secs(1), MAX_OUTPUT_LIMIT_BYTES + 1).is_err());
8728 assert!(
8729 ExecutionPolicy::new(Duration::from_secs(1), 1024)
8730 .unwrap()
8731 .with_file_limit(0)
8732 .is_err()
8733 );
8734 assert_eq!(
8735 ExecutionPolicy::new(Duration::from_secs(2), 4096)
8736 .unwrap()
8737 .output_limit_bytes(),
8738 4096
8739 );
8740 }
8741
8742 #[test]
8743 fn invocation_metrics_csv_schema_is_stable() {
8744 let metrics = InvocationMetrics {
8745 schema_version: 1,
8746 operation: OperationKind::VerifyV2,
8747 duration: Duration::from_nanos(123),
8748 stdout_bytes: 45,
8749 stderr_bytes: 6,
8750 timeout: Duration::from_millis(700),
8751 output_limit_bytes: 8192,
8752 memory_limit_bytes: Some(1024 * 1024 * 1024),
8753 file_limit_bytes: 32 * 1024 * 1024,
8754 process_group_containment: true,
8755 exit_code: Some(2),
8756 status: InvocationStatus::Failed(FailureClass::ExitStatus),
8757 };
8758 assert_eq!(
8759 InvocationMetrics::csv_header(),
8760 "schema_version,operation,duration_ns,stdout_bytes,stderr_bytes,timeout_ms,output_limit_bytes,exit_code,status,failure_class"
8761 );
8762 assert_eq!(
8763 metrics.to_csv_row(),
8764 "1,verify_v2,123,45,6,700,8192,2,error,exit_status"
8765 );
8766
8767 let successful = InvocationMetrics {
8768 schema_version: 1,
8769 operation: OperationKind::Discover,
8770 duration: Duration::from_nanos(7),
8771 stdout_bytes: 10,
8772 stderr_bytes: 0,
8773 timeout: Duration::from_millis(700),
8774 output_limit_bytes: 8192,
8775 memory_limit_bytes: None,
8776 file_limit_bytes: 32 * 1024 * 1024,
8777 process_group_containment: true,
8778 exit_code: Some(0),
8779 status: InvocationStatus::Success,
8780 };
8781 let aggregate = aggregate_invocation_metrics([&metrics, &successful]).unwrap();
8782 assert_eq!(aggregate.jobs, 2);
8783 assert_eq!(aggregate.successes, 1);
8784 assert_eq!(aggregate.failures, 1);
8785 assert_eq!(aggregate.total_duration_ns, 130);
8786 assert_eq!(aggregate.maximum_duration_ns, 123);
8787 assert_eq!(aggregate.total_stdout_bytes, 55);
8788 assert_eq!(aggregate.total_stderr_bytes, 6);
8789 assert_eq!(aggregate.process_group_contained_jobs, 2);
8790 assert_eq!(aggregate.memory_limited_jobs, 1);
8791 assert_eq!(aggregate.operation_counts["discover"], 1);
8792 assert_eq!(aggregate.operation_counts["verify_v2"], 1);
8793 assert_eq!(aggregate.failure_counts["exit_status"], 1);
8794 assert_eq!(
8795 InvocationMetricsAggregate::csv_header(),
8796 "schema_version,jobs,successes,failures,total_duration_ns,maximum_duration_ns,total_stdout_bytes,total_stderr_bytes,process_group_contained_jobs,memory_limited_jobs,operation_counts,failure_counts"
8797 );
8798 assert_eq!(
8799 aggregate.to_csv_row(),
8800 "1,2,1,1,130,123,55,6,2,1,discover=1;verify_v2=1,exit_status=1"
8801 );
8802
8803 let mut incompatible = successful.clone();
8804 incompatible.schema_version = 2;
8805 assert_eq!(
8806 aggregate_invocation_metrics([&incompatible]),
8807 Err(MetricsAggregationError::UnsupportedSchema(2))
8808 );
8809 assert_eq!(
8810 aggregate_invocation_metrics(std::iter::empty()),
8811 Err(MetricsAggregationError::Empty)
8812 );
8813 }
8814
8815 #[cfg(unix)]
8816 #[test]
8817 fn bounded_runner_reports_deadline_and_output_classes() {
8818 let mut delayed = Command::new("sh");
8819 delayed.arg("-c").arg("sleep 5 & wait");
8820 let started = Instant::now();
8821 let failure = run_bounded(
8822 OperationKind::VerifyV2,
8823 delayed,
8824 ExecutionPolicy::new(Duration::from_millis(10), 1024).unwrap(),
8825 )
8826 .unwrap_err();
8827 assert!(matches!(*failure.error, PredicateApiError::TimedOut { .. }));
8828 assert_eq!(
8829 failure.metrics.status,
8830 InvocationStatus::Failed(FailureClass::Timeout)
8831 );
8832 assert!(started.elapsed() < Duration::from_secs(1));
8833
8834 let mut verbose = Command::new("printf");
8835 verbose.arg("0123456789");
8836 let failure = run_bounded(
8837 OperationKind::CertifyV2,
8838 verbose,
8839 ExecutionPolicy::new(Duration::from_secs(1), 4).unwrap(),
8840 )
8841 .unwrap_err();
8842 assert!(matches!(
8843 *failure.error,
8844 PredicateApiError::OutputLimitExceeded {
8845 stream: "stdout",
8846 limit_bytes: 4,
8847 }
8848 ));
8849 assert_eq!(failure.metrics.operation, OperationKind::CertifyV2);
8850 assert_eq!(failure.metrics.stdout_bytes, 5);
8851 assert_eq!(
8852 failure.metrics.status,
8853 InvocationStatus::Failed(FailureClass::OutputLimit)
8854 );
8855 }
8856
8857 #[cfg(all(unix, not(target_os = "macos")))]
8858 #[test]
8859 fn bounded_runner_applies_address_space_ceiling() {
8860 let bytes = 128 * 1024 * 1024;
8861 let policy = ExecutionPolicy::new(Duration::from_secs(1), 1024)
8862 .unwrap()
8863 .with_memory_limit(bytes)
8864 .unwrap();
8865 let mut command = Command::new("sh");
8866 command.arg("-c").arg("ulimit -v");
8867 let output = run_bounded(OperationKind::Discover, command, policy).unwrap();
8868 let reported_kib = String::from_utf8(output.stdout)
8869 .unwrap()
8870 .trim()
8871 .parse::<u64>()
8872 .unwrap();
8873 assert!(reported_kib > 0);
8874 assert!(reported_kib <= bytes / 1024);
8875 assert_eq!(output.metrics.memory_limit_bytes, Some(bytes));
8876 }
8877}