Skip to main content

cockpitctl_types/
lib.rs

1//! DTOs and stable primitives for cockpitctl.
2//!
3//! This crate is intentionally boring:
4//! - pure data structures
5//! - stable IDs and enums
6//! - deterministic ordering helpers
7//!
8//! It must not depend on filesystem, clap, or network.
9
10use std::collections::BTreeMap;
11
12use serde::{Deserialize, Serialize};
13use serde_json::Value;
14
15/// Embedded JSON Schema for sensor.report.v1.
16pub const SENSOR_REPORT_V1_SCHEMA_JSON: &str = include_str!("../schemas/sensor.report.v1.json");
17
18/// Embedded JSON Schema for cockpit.report.v1.
19pub const COCKPIT_REPORT_V1_SCHEMA_JSON: &str = include_str!("../schemas/cockpit.report.v1.json");
20
21/// Embedded JSON Schema for buildfix.plan.v1.
22pub const BUILDFIX_PLAN_V1_SCHEMA_JSON: &str = include_str!("../schemas/buildfix.plan.v1.json");
23
24/// Embedded JSON Schema for cockpit.promote.v1.
25pub const COCKPIT_PROMOTE_V1_SCHEMA_JSON: &str = include_str!("../schemas/cockpit.promote.v1.json");
26
27/// A schema identifier string, e.g. `builddiag.report.v1`.
28pub type SchemaId = String;
29
30#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
31#[serde(rename_all = "snake_case")]
32pub enum VerdictStatus {
33    Pass,
34    Warn,
35    Fail,
36    Skip,
37}
38
39fn is_zero(v: &u64) -> bool {
40    *v == 0
41}
42
43#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Default)]
44pub struct VerdictCounts {
45    pub info: u64,
46    pub warn: u64,
47    pub error: u64,
48    #[serde(default, skip_serializing_if = "is_zero")]
49    pub suppressed: u64,
50}
51
52#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
53pub struct Verdict {
54    pub status: VerdictStatus,
55    pub counts: VerdictCounts,
56    #[serde(default)]
57    pub reasons: Vec<String>,
58}
59
60#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
61pub struct ToolInfo {
62    pub name: String,
63    pub version: String,
64    #[serde(default, skip_serializing_if = "Option::is_none")]
65    pub commit: Option<String>,
66}
67
68#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
69pub struct HostInfo {
70    #[serde(default, skip_serializing_if = "Option::is_none")]
71    pub os: Option<String>,
72    #[serde(default, skip_serializing_if = "Option::is_none")]
73    pub arch: Option<String>,
74    #[serde(default, skip_serializing_if = "Option::is_none")]
75    pub hostname: Option<String>,
76}
77
78#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
79pub struct GitInfo {
80    #[serde(default, skip_serializing_if = "Option::is_none")]
81    pub repo: Option<String>,
82    #[serde(default, skip_serializing_if = "Option::is_none")]
83    pub base_ref: Option<String>,
84    #[serde(default, skip_serializing_if = "Option::is_none")]
85    pub head_ref: Option<String>,
86    #[serde(default, skip_serializing_if = "Option::is_none")]
87    pub base_sha: Option<String>,
88    #[serde(default, skip_serializing_if = "Option::is_none")]
89    pub head_sha: Option<String>,
90    #[serde(default, skip_serializing_if = "Option::is_none")]
91    pub merge_base: Option<String>,
92}
93
94#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
95pub struct CiInfo {
96    #[serde(default, skip_serializing_if = "Option::is_none")]
97    pub provider: Option<String>,
98    #[serde(default, skip_serializing_if = "Option::is_none")]
99    pub run_id: Option<String>,
100    #[serde(default, skip_serializing_if = "Option::is_none")]
101    pub run_url: Option<String>,
102    #[serde(default, skip_serializing_if = "Option::is_none")]
103    pub job: Option<String>,
104}
105
106#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
107#[serde(rename_all = "snake_case")]
108pub enum CapabilityStatus {
109    Available,
110    Unavailable,
111    Skipped,
112}
113
114#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
115pub struct Capability {
116    pub status: CapabilityStatus,
117    #[serde(default, skip_serializing_if = "Option::is_none")]
118    pub reason: Option<String>,
119}
120
121#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
122pub struct RunInfo {
123    pub started_at: String, // RFC3339
124    #[serde(default, skip_serializing_if = "Option::is_none")]
125    pub ended_at: Option<String>,
126    #[serde(default, skip_serializing_if = "Option::is_none")]
127    pub duration_ms: Option<u64>,
128    #[serde(default, skip_serializing_if = "Option::is_none")]
129    pub host: Option<HostInfo>,
130    #[serde(default, skip_serializing_if = "Option::is_none")]
131    pub git: Option<GitInfo>,
132    #[serde(default, skip_serializing_if = "Option::is_none")]
133    pub ci: Option<CiInfo>,
134    /// Declared capabilities (e.g., "git", "baseline", "lcov").
135    #[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
136    pub capabilities: BTreeMap<String, Capability>,
137}
138
139#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
140#[serde(rename_all = "snake_case")]
141pub enum Severity {
142    Info,
143    Warn,
144    Error,
145}
146
147#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
148pub struct Location {
149    #[serde(default, skip_serializing_if = "Option::is_none")]
150    pub path: Option<String>,
151    #[serde(default, skip_serializing_if = "Option::is_none")]
152    pub line: Option<u32>,
153    #[serde(default, skip_serializing_if = "Option::is_none")]
154    pub col: Option<u32>,
155}
156
157#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
158pub struct Finding {
159    pub severity: Severity,
160    #[serde(default, skip_serializing_if = "Option::is_none")]
161    pub check_id: Option<String>,
162    pub code: String,
163    pub message: String,
164
165    #[serde(default, skip_serializing_if = "Option::is_none")]
166    pub location: Option<Location>,
167
168    #[serde(default, skip_serializing_if = "Option::is_none")]
169    pub help: Option<String>,
170    #[serde(default, skip_serializing_if = "Option::is_none")]
171    pub url: Option<String>,
172    #[serde(default, skip_serializing_if = "Option::is_none")]
173    pub fingerprint: Option<String>,
174
175    #[serde(default, skip_serializing_if = "Option::is_none")]
176    pub data: Option<Value>,
177}
178
179/// The shared receipt envelope for sensors.
180#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
181pub struct SensorReport {
182    pub schema: SchemaId,
183    pub tool: ToolInfo,
184    pub run: RunInfo,
185    pub verdict: Verdict,
186    #[serde(default)]
187    pub findings: Vec<Finding>,
188    #[serde(default, skip_serializing_if = "Vec::is_empty")]
189    pub artifacts: Vec<ArtifactPointer>,
190    #[serde(default, skip_serializing_if = "Option::is_none")]
191    pub data: Option<Value>,
192}
193
194/// Missing receipt behavior (policy).
195#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, Default)]
196#[serde(rename_all = "snake_case")]
197pub enum MissingPolicy {
198    #[default]
199    Skip,
200    Warn,
201    Fail,
202}
203
204/// Presence state for a sensor in the cockpit report.
205#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
206#[serde(rename_all = "snake_case")]
207pub enum Presence {
208    Present,
209    Missing,
210    Invalid,
211}
212
213/// Policy outcome for a sensor after evaluation.
214#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
215#[serde(rename_all = "snake_case")]
216pub enum PolicyOutcome {
217    Blocked,
218    Allowed,
219    Informational,
220}
221
222/// Pointer to an artifact produced by a sensor.
223#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
224pub struct ArtifactPointer {
225    pub id: String,
226    pub path: String,
227    pub mime: String,
228    #[serde(default, skip_serializing_if = "Option::is_none")]
229    pub schema: Option<String>,
230}
231
232/// Promotion hints for cockpit (`data._cockpit`).
233#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
234pub struct CockpitPromoteHints {
235    #[serde(default, skip_serializing_if = "Option::is_none")]
236    pub schema: Option<String>,
237    #[serde(default, skip_serializing_if = "Vec::is_empty")]
238    pub cards: Vec<PromoteCard>,
239    #[serde(default, skip_serializing_if = "Vec::is_empty")]
240    pub suggested_highlights: Vec<SuggestedHighlight>,
241    #[serde(default, skip_serializing_if = "Vec::is_empty")]
242    pub suggested_artifacts: Vec<SuggestedArtifact>,
243}
244
245/// A card promoted to the cockpit summary from a sensor.
246#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
247pub struct PromoteCard {
248    pub id: String,
249    pub label: String,
250    pub value: String,
251    #[serde(default, skip_serializing_if = "Option::is_none")]
252    pub severity: Option<Severity>,
253}
254
255/// A suggested highlight from a sensor via promotion.
256#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
257pub struct SuggestedHighlight {
258    pub finding_fingerprint: String,
259}
260
261/// A suggested artifact from a sensor via promotion.
262#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
263pub struct SuggestedArtifact {
264    pub artifact_id: String,
265}
266
267/// Schema validation mode for sensor receipts.
268#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, Default)]
269#[serde(rename_all = "snake_case")]
270pub enum SchemaValidation {
271    /// Skip schema validation; only parse as JSON (default).
272    #[default]
273    Lax,
274    /// Validate receipts against the JSON schema.
275    Strict,
276}
277
278/// A per-sensor policy in cockpit.toml.
279#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Default)]
280pub struct SensorPolicy {
281    #[serde(default)]
282    pub blocking: bool,
283    #[serde(default)]
284    pub missing: MissingPolicy,
285    #[serde(default, skip_serializing_if = "Option::is_none")]
286    pub section: Option<String>,
287    #[serde(default, skip_serializing_if = "Option::is_none")]
288    pub require_label: Option<String>,
289    #[serde(default, skip_serializing_if = "Option::is_none")]
290    pub repro: Option<String>,
291}
292
293/// Buildfix auto-apply policy in cockpit.toml.
294#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
295pub struct BuildfixPolicy {
296    /// Enable automatic fix application after ingest.
297    #[serde(default)]
298    pub auto_apply: bool,
299    /// Maximum safety level allowed for auto-apply.
300    #[serde(default = "default_buildfix_max_auto_apply_safety")]
301    pub max_auto_apply_safety: SafetyLevel,
302    /// Require each selected fix to match at least one surfaced finding.
303    #[serde(default = "default_buildfix_require_matched_finding")]
304    pub require_matched_finding: bool,
305    /// Optional external actuator command used to apply selected fixes.
306    #[serde(default, skip_serializing_if = "Option::is_none")]
307    pub actuator: Option<BuildfixActuatorConfig>,
308}
309
310impl Default for BuildfixPolicy {
311    fn default() -> Self {
312        Self {
313            auto_apply: false,
314            max_auto_apply_safety: default_buildfix_max_auto_apply_safety(),
315            require_matched_finding: default_buildfix_require_matched_finding(),
316            actuator: None,
317        }
318    }
319}
320
321/// External command configuration for buildfix actuation.
322#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
323pub struct BuildfixActuatorConfig {
324    pub command: String,
325    #[serde(default = "default_buildfix_actuator_timeout_ms")]
326    pub timeout_ms: u64,
327}
328
329#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
330pub struct Policy {
331    #[serde(default)]
332    pub warn_is_fail: bool,
333    #[serde(default = "default_max_highlights")]
334    pub max_highlights: usize,
335    #[serde(default = "default_max_per_sensor_findings")]
336    pub max_per_sensor_findings: usize,
337    #[serde(default = "default_max_annotations")]
338    pub max_annotations: usize,
339    #[serde(default = "default_section_order")]
340    pub section_order: Vec<String>,
341    /// Schema validation mode: "lax" (default) skips schema validation; "strict"
342    /// validates receipts against the embedded sensor.report.v1 schema.
343    #[serde(default)]
344    pub schema_validation: SchemaValidation,
345    /// Maximum receipt file size in bytes (default 2MB). Receipts exceeding this
346    /// limit are rejected with a `cockpit.receipt_oversized` finding.
347    #[serde(default = "default_max_receipt_size_bytes")]
348    pub max_receipt_size_bytes: usize,
349}
350
351fn default_max_highlights() -> usize {
352    7
353}
354fn default_max_per_sensor_findings() -> usize {
355    20
356}
357fn default_max_annotations() -> usize {
358    25
359}
360fn default_max_receipt_size_bytes() -> usize {
361    2 * 1024 * 1024 // 2MB
362}
363fn default_buildfix_max_auto_apply_safety() -> SafetyLevel {
364    SafetyLevel::Safe
365}
366fn default_buildfix_require_matched_finding() -> bool {
367    true
368}
369fn default_buildfix_actuator_timeout_ms() -> u64 {
370    30_000 // 30 seconds
371}
372fn default_section_order() -> Vec<String> {
373    vec![
374        "Highlights".into(),
375        "Repo contract".into(),
376        "Dependencies".into(),
377        "Policy".into(),
378        "Tests".into(),
379        "Diagnostics".into(),
380        "Performance".into(),
381        "Environment".into(),
382        "Other".into(),
383    ]
384}
385
386impl Default for Policy {
387    fn default() -> Self {
388        Self {
389            warn_is_fail: false,
390            max_highlights: default_max_highlights(),
391            max_per_sensor_findings: default_max_per_sensor_findings(),
392            max_annotations: default_max_annotations(),
393            section_order: default_section_order(),
394            schema_validation: SchemaValidation::default(),
395            max_receipt_size_bytes: default_max_receipt_size_bytes(),
396        }
397    }
398}
399
400#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Default)]
401pub struct CockpitConfig {
402    #[serde(default)]
403    pub policy: Policy,
404    #[serde(default)]
405    pub buildfix: BuildfixPolicy,
406    #[serde(default)]
407    pub policy_signing: PolicySigningConfig,
408    #[serde(default)]
409    pub sensors: std::collections::BTreeMap<String, SensorPolicy>,
410    #[serde(default, skip_serializing_if = "Vec::is_empty")]
411    pub hooks: Vec<HookConfig>,
412}
413
414/// A single sensor row in the cockpit aggregate report.
415#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
416pub struct SensorSummary {
417    pub id: String,
418    pub blocking: bool,
419    pub missing: MissingPolicy,
420    pub presence: Presence,
421    pub report_path: String,
422    #[serde(default, skip_serializing_if = "Option::is_none")]
423    pub comment_path: Option<String>,
424    pub verdict: Verdict,
425    #[serde(default)]
426    pub truncated: bool,
427    #[serde(default)]
428    pub errors: Vec<String>,
429    #[serde(default, skip_serializing_if = "Option::is_none")]
430    pub missing_policy_applied: Option<MissingPolicy>,
431    #[serde(default, skip_serializing_if = "Option::is_none")]
432    pub policy_outcome: Option<PolicyOutcome>,
433}
434
435#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
436pub struct Highlight {
437    pub sensor_id: String,
438    pub finding: Finding,
439}
440
441/// The director output: cockpit.report.v1.
442#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
443pub struct CockpitReport {
444    pub schema: SchemaId,
445    pub tool: ToolInfo,
446    pub run: RunInfo,
447    pub verdict: Verdict,
448    pub sensors: Vec<SensorSummary>,
449    pub highlights: Vec<Highlight>,
450    pub policy: PolicySnapshot,
451    #[serde(default, skip_serializing_if = "Option::is_none")]
452    pub data: Option<Value>,
453}
454
455#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
456pub struct PolicySnapshot {
457    pub warn_is_fail: bool,
458    pub max_highlights: usize,
459    pub max_per_sensor_findings: usize,
460    pub max_annotations: usize,
461    pub section_order: Vec<String>,
462    pub sensors: Vec<PolicySensorSnapshot>,
463}
464
465#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
466pub struct PolicySensorSnapshot {
467    pub id: String,
468    pub blocking: bool,
469    pub missing: MissingPolicy,
470    #[serde(default, skip_serializing_if = "Option::is_none")]
471    pub section: Option<String>,
472    #[serde(default, skip_serializing_if = "Option::is_none")]
473    pub require_label: Option<String>,
474    #[serde(default, skip_serializing_if = "Option::is_none")]
475    pub repro: Option<String>,
476}
477
478/// A stable key used for sorting findings deterministically.
479#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
480pub struct FindingSortKey {
481    pub severity_rank: u8,
482    pub sensor_id: String,
483    pub path: String,
484    pub line: u32,
485    pub code: String,
486    pub message: String,
487}
488
489pub fn severity_rank(s: &Severity) -> u8 {
490    match s {
491        Severity::Error => 0,
492        Severity::Warn => 1,
493        Severity::Info => 2,
494    }
495}
496
497pub fn verdict_status_rank(s: &VerdictStatus) -> u8 {
498    match s {
499        VerdictStatus::Fail => 0,
500        VerdictStatus::Warn => 1,
501        VerdictStatus::Pass => 2,
502        VerdictStatus::Skip => 3,
503    }
504}
505
506/// Validate a sensor ID for safe path usage.
507pub fn is_valid_sensor_id(id: &str) -> bool {
508    !id.is_empty()
509        && !id.contains("..")
510        && !id.contains('/')
511        && !id.contains('\\')
512        && id
513            .bytes()
514            .all(|b| b.is_ascii_alphanumeric() || b == b'_' || b == b'-')
515}
516
517// ============================================================================
518// Buildfix types (actuator protocol)
519// ============================================================================
520
521/// Safety level for a fix.
522#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
523#[serde(rename_all = "snake_case")]
524pub enum SafetyLevel {
525    /// No side effects; safe to apply automatically.
526    Safe,
527    /// Requires confirmation before applying.
528    Guarded,
529    /// May break things; use with caution.
530    Unsafe,
531}
532
533/// Rank a safety level for deterministic ordering and gating.
534/// Lower is safer.
535pub fn safety_level_rank(s: &SafetyLevel) -> u8 {
536    match s {
537        SafetyLevel::Safe => 0,
538        SafetyLevel::Guarded => 1,
539        SafetyLevel::Unsafe => 2,
540    }
541}
542
543/// Reference to a finding that a fix addresses.
544#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
545pub struct FindingRef {
546    pub sensor_id: String,
547    #[serde(default, skip_serializing_if = "Option::is_none")]
548    pub fingerprint: Option<String>,
549    #[serde(default, skip_serializing_if = "Option::is_none")]
550    pub code: Option<String>,
551    #[serde(default, skip_serializing_if = "Option::is_none")]
552    pub tool: Option<String>,
553    #[serde(default, skip_serializing_if = "Option::is_none")]
554    pub check_id: Option<String>,
555}
556
557/// Preconditions that must hold before applying a fix.
558#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
559pub struct Preconditions {
560    pub repo_head: String,
561    #[serde(default, skip_serializing_if = "Vec::is_empty")]
562    pub receipt_digests: Vec<String>,
563}
564
565/// A single fix in a buildfix plan.
566#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
567pub struct Fix {
568    pub id: String,
569    pub safety: SafetyLevel,
570    pub description: String,
571    #[serde(default, skip_serializing_if = "Vec::is_empty")]
572    pub finding_refs: Vec<FindingRef>,
573    #[serde(default, skip_serializing_if = "Option::is_none")]
574    pub preconditions: Option<Preconditions>,
575    #[serde(default, skip_serializing_if = "Option::is_none")]
576    pub data: Option<Value>,
577}
578
579/// A buildfix plan: a set of fixes to apply.
580#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
581pub struct BuildfixPlan {
582    pub schema: SchemaId,
583    pub tool: ToolInfo,
584    pub fixes: Vec<Fix>,
585}
586
587// ============================================================================
588// Trend tracking types
589// ============================================================================
590
591/// Change in the overall verdict between baseline and current.
592#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
593pub struct VerdictChange {
594    pub before: VerdictStatus,
595    pub after: VerdictStatus,
596}
597
598/// Delta counts for findings.
599#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq)]
600pub struct CountDeltas {
601    pub info_delta: i64,
602    pub warn_delta: i64,
603    pub error_delta: i64,
604}
605
606/// A finding that changed between baseline and current.
607#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
608pub struct TrendFinding {
609    pub sensor_id: String,
610    pub code: String,
611    pub message: String,
612    #[serde(default, skip_serializing_if = "Option::is_none")]
613    pub path: Option<String>,
614    #[serde(default, skip_serializing_if = "Option::is_none")]
615    pub line: Option<u32>,
616    #[serde(default, skip_serializing_if = "Option::is_none")]
617    pub fingerprint: Option<String>,
618    pub severity: Severity,
619}
620
621/// Category of change for a trending finding.
622#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
623#[serde(rename_all = "snake_case")]
624pub enum TrendChange {
625    New,
626    Fixed,
627}
628
629/// Full trend delta between a baseline and current report.
630#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
631pub struct TrendDelta {
632    pub verdict_change: Option<VerdictChange>,
633    pub count_deltas: CountDeltas,
634    pub new_findings: Vec<TrendFinding>,
635    pub fixed_findings: Vec<TrendFinding>,
636    pub sensors_added: Vec<String>,
637    pub sensors_removed: Vec<String>,
638}
639
640// ============================================================================
641// Buildfix summary types (for cockpit report surfacing)
642// ============================================================================
643
644/// A matched finding for a fix.
645#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
646pub struct MatchedFinding {
647    pub sensor_id: String,
648    pub code: String,
649    #[serde(default, skip_serializing_if = "Option::is_none")]
650    pub fingerprint: Option<String>,
651}
652
653/// Summary of a single fix for cockpit surfacing.
654#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
655pub struct FixSummary {
656    pub fix_id: String,
657    pub sensor_id: String,
658    pub safety: SafetyLevel,
659    pub description: String,
660    pub matched_findings: Vec<MatchedFinding>,
661    pub unmatched: bool,
662}
663
664/// Summary of buildfix plans for cockpit surfacing.
665#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq)]
666pub struct BuildfixSummary {
667    pub fixes: Vec<FixSummary>,
668    pub total_fixes: usize,
669    pub matched_count: usize,
670    pub unmatched_count: usize,
671}
672
673/// Schema identifier for buildfix apply requests sent to actuator commands.
674pub const BUILDFIX_APPLY_REQUEST_SCHEMA_ID: &str = "buildfix.apply.request.v1";
675
676/// Structured request sent to a buildfix actuator command on stdin.
677#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
678pub struct BuildfixApplyRequest {
679    pub schema: SchemaId,
680    pub max_auto_apply_safety: SafetyLevel,
681    pub require_matched_finding: bool,
682    pub fixes: Vec<FixSummary>,
683}
684
685/// Structured response returned by a buildfix actuator command on stdout.
686#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq)]
687pub struct BuildfixActuatorResult {
688    #[serde(default)]
689    pub applied_fix_ids: Vec<String>,
690    #[serde(default)]
691    pub skipped_fix_ids: Vec<String>,
692    #[serde(default)]
693    pub errors: Vec<String>,
694}
695
696/// Result status of buildfix auto-apply.
697#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
698#[serde(rename_all = "snake_case")]
699pub enum BuildfixApplyStatus {
700    Skipped,
701    Applied,
702    Failed,
703}
704
705/// Buildfix auto-apply evidence surfaced in `cockpit.report.v1` data.
706#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
707pub struct BuildfixApplySummary {
708    pub status: BuildfixApplyStatus,
709    pub auto_apply_enabled: bool,
710    pub max_auto_apply_safety: SafetyLevel,
711    pub require_matched_finding: bool,
712    #[serde(default, skip_serializing_if = "Vec::is_empty")]
713    pub candidate_fix_ids: Vec<String>,
714    #[serde(default, skip_serializing_if = "Vec::is_empty")]
715    pub selected_fix_ids: Vec<String>,
716    #[serde(default, skip_serializing_if = "Vec::is_empty")]
717    pub applied_fix_ids: Vec<String>,
718    #[serde(default, skip_serializing_if = "Vec::is_empty")]
719    pub skipped_fix_ids: Vec<String>,
720    #[serde(default, skip_serializing_if = "Vec::is_empty")]
721    pub errors: Vec<String>,
722    #[serde(default, skip_serializing_if = "Option::is_none")]
723    pub reason: Option<String>,
724    #[serde(default, skip_serializing_if = "Option::is_none")]
725    pub actuator_command: Option<String>,
726}
727
728// ============================================================================
729// Policy snapshot signing
730// ============================================================================
731
732/// Signature algorithm used for policy snapshot evidence.
733#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, Default)]
734#[serde(rename_all = "snake_case")]
735pub enum PolicySignatureAlgorithm {
736    /// HMAC-SHA256 over canonical policy snapshot JSON bytes.
737    #[default]
738    HmacSha256,
739}
740
741/// Policy snapshot signing config in cockpit.toml.
742#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Default)]
743pub struct PolicySigningConfig {
744    /// Enable policy snapshot signing.
745    #[serde(default)]
746    pub enabled: bool,
747    /// Signature algorithm.
748    #[serde(default)]
749    pub algorithm: PolicySignatureAlgorithm,
750    /// Path to signing key bytes.
751    #[serde(default, skip_serializing_if = "Option::is_none")]
752    pub key_path: Option<String>,
753    /// Environment variable containing signing key bytes.
754    #[serde(default, skip_serializing_if = "Option::is_none")]
755    pub key_env: Option<String>,
756    /// Optional key identifier attached to produced evidence.
757    #[serde(default, skip_serializing_if = "Option::is_none")]
758    pub key_id: Option<String>,
759}
760
761/// Schema identifier for policy signature evidence.
762pub const POLICY_SIGNATURE_SCHEMA_ID: &str = "cockpit.policy_signature.v1";
763
764/// Signed evidence for the policy snapshot used to compute a cockpit verdict.
765#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
766pub struct PolicySignatureEvidence {
767    pub schema: SchemaId,
768    pub algorithm: PolicySignatureAlgorithm,
769    /// SHA-256 digest (hex) of canonical policy snapshot JSON bytes.
770    pub policy_sha256: String,
771    /// Signature (hex) produced by the configured algorithm.
772    pub signature: String,
773    #[serde(default, skip_serializing_if = "Option::is_none")]
774    pub key_id: Option<String>,
775}
776
777// ============================================================================
778// Hook config types
779// ============================================================================
780
781/// When a hook should run.
782#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, Default)]
783#[serde(rename_all = "snake_case")]
784pub enum HookWhen {
785    /// Run after ingest completes.
786    #[default]
787    AfterIngest,
788}
789
790/// Configuration for a post-processing hook.
791#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
792pub struct HookConfig {
793    pub name: String,
794    pub command: String,
795    #[serde(default)]
796    pub when: HookWhen,
797    #[serde(default = "default_hook_timeout_ms")]
798    pub timeout_ms: u64,
799}
800
801fn default_hook_timeout_ms() -> u64 {
802    default_buildfix_actuator_timeout_ms()
803}