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}