kaptein_viewmodel/audit.rs
1//! The single write-audit record: one format, two consumers.
2//!
3//! Used by both the local audit log and the incident-timeline export. Records
4//! **operations, not values** — secrets are never persisted here. Fully `serde`-
5//! serializable so it can cross the `serve`/gRPC-Web boundary and be exported.
6
7use serde::{Deserialize, Serialize};
8
9/// A reference to a Kubernetes resource.
10#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
11pub struct ResourceRef {
12 pub group: String,
13 pub kind: String,
14 pub namespace: String,
15 pub name: String,
16}
17
18/// The operation that was performed. `McpToolCall` is intentionally **absent**: MCP is a
19/// *transport*, captured in `AuditEvent::source`, not a distinct operation. An agent that
20/// scales a deployment logs `Operation::Scale` with `source: Surface::Mcp`.
21#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
22pub enum Operation {
23 // Read operations (governance requires visibility into reads, not just writes).
24 List,
25 Describe,
26 Logs,
27 Diagnose,
28 // Write operations.
29 Delete,
30 Scale,
31 Restart,
32 Cordon,
33 Drain,
34 Evict,
35 Exec,
36 PortForward,
37 /// A GitOps write path action (branch + PR), not an API-server write.
38 GitPrOpened,
39 /// An operator viewed (unmasked) a secret — the single most audit-relevant event for
40 /// a tool that masks secrets by default.
41 SecretViewed,
42}
43
44/// The outcome of a write attempt.
45#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
46pub enum Outcome {
47 Applied,
48 DryRun,
49 Rejected,
50}
51
52/// Which projection initiated the action. MCP is a *source*, not an operation.
53#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
54#[serde(rename_all = "snake_case")]
55pub enum Source {
56 Tui,
57 Gui,
58 Browser,
59 Mcp,
60 Headless,
61}
62
63/// The actor who performed the operation. An agent has its **own** identity, so agent
64/// actions are distinguishable from human actions (ADR-0007, ADR-0010).
65#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
66pub struct Actor {
67 pub kind: ActorKind,
68 pub name: String,
69}
70
71#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
72#[serde(rename_all = "snake_case")]
73pub enum ActorKind {
74 Human,
75 Agent,
76}
77
78/// A single audit record. Serialized with `serde`; the same shape feeds the incident
79/// timeline export (one format, two consumers).
80#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
81pub struct AuditEvent {
82 /// Unix epoch milliseconds — a typed instant, not a pre-formatted string, so it
83 /// sorts and localizes correctly.
84 pub timestamp: i64,
85 pub actor: Actor,
86 /// Cluster/context id — never a secret.
87 pub context: String,
88 pub operation: Operation,
89 pub target: ResourceRef,
90 pub outcome: Outcome,
91 /// Which projection initiated the action (MCP is a source, not an operation).
92 pub source: Source,
93 /// Identifies the debugging session / multi-step agent invocation, so the incident
94 /// timeline can group related events.
95 pub session_id: String,
96 /// Recorded break-glass justification (required for the break-glass guardrail to be
97 /// a complete control).
98 pub reason: Option<String>,
99 /// Who initiated the session on the agent's behalf (an agent acts under its own
100 /// ServiceAccount, but the audit question is still "who asked").
101 pub on_behalf_of: Option<String>,
102}