Skip to main content

kaptein_viewmodel/
semantic.rs

1//! Layer 2 — the semantic layer.
2//!
3//! The genuinely renderer-agnostic part: actions, RBAC state, status inference, blast
4//! radius. Identical for every frontend.
5//!
6//! The view-model emits **message keys + args**, never localized strings (see ADR-0005);
7//! the frontend resolves keys for i18n. Structured data (e.g. which verb/resource is
8//! forbidden) is carried as fields so the MCP surface can reason about it programmatically.
9
10use serde::{Deserialize, Serialize};
11
12/// An action the user (or an agent) can take, and whether it is currently allowed.
13#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
14pub struct Action {
15    pub id: String,
16    /// Message key resolved by the frontend for i18n.
17    pub label_key: String,
18    /// RBAC-preflight result: `Allowed` (enabled) vs `Forbidden` (greyed out *before*
19    /// the user tries), possibly with a structured reason.
20    pub state: ActionState,
21}
22
23#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
24pub enum ActionState {
25    Allowed,
26    /// Disallowed by RBAC preflight — shown greyed out, never a post-hoc 403. Carries
27    /// the specific missing permission so the MCP surface can act on it.
28    Forbidden {
29        verb: String,
30        resource: String,
31        namespace: Option<String>,
32    },
33    /// Allowed but gated behind a guardrail (e.g. prod "break glass").
34    Gated {
35        /// Message key (localized by the frontend), not a pre-formatted sentence.
36        reason_key: String,
37    },
38}
39
40/// The overall status of the current view.
41#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
42pub enum Status {
43    Ok,
44    Warning {
45        message_key: String,
46    },
47    Error {
48        message_key: String,
49    },
50    /// Read-only because the context is unknown or `impersonate` is unavailable.
51    ReadOnly {
52        message_key: String,
53    },
54}