Skip to main content

lean_ctx_protocol/hive/
status.rs

1// SPDX-License-Identifier: Apache-2.0
2
3//! `lean-ctx hive status --json`: the one shape the local cockpit reads.
4//!
5//! It is built from local state only (no network) and prints `available:
6//! true` whenever the engine has the Hive connector, connected or not.
7//! The six fields of 1.0 are always printed; 1.2 adds `drafts`, `policy`,
8//! `filter_packs` and `policy_refusal` (readers treat them as "not
9//! reported" when an older engine leaves them out).
10
11use serde::{Deserialize, Serialize};
12
13use super::HIVE_DEFAULT_URL;
14use crate::UtcTimestamp;
15
16/// Instance shown in the cockpit.
17#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
18#[serde(deny_unknown_fields)]
19pub struct HiveStatusInstanceV1 {
20    pub id: String,
21    pub label: String,
22}
23
24/// Organization shown in the cockpit.
25#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
26#[serde(deny_unknown_fields)]
27pub struct HiveStatusOrgV1 {
28    pub name: String,
29}
30
31/// Session drafts not yet in Hive (1.2).
32#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
33#[serde(deny_unknown_fields)]
34pub struct HiveStatusDraftsV1 {
35    /// Queued in the outbox, sent with the next sync.
36    pub queued: u32,
37    /// Refused by Hive for good; kept on this machine.
38    pub refused: u32,
39}
40
41/// An active filter pack (1.2). Bundles verify as a whole, so every listed
42/// pack is active; a refused bundle leaves the previous list in place.
43#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
44#[serde(deny_unknown_fields)]
45pub struct HiveStatusFilterPackV1 {
46    pub pack_id: String,
47    pub version: u64,
48}
49
50/// What a bus control asks of an agent (1.5).
51#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
52#[serde(rename_all = "snake_case")]
53pub enum HiveBusControlActionV1 {
54    Hold,
55    Stop,
56    Veto,
57    #[serde(other)]
58    Unknown,
59}
60
61/// A bus control in force on this machine (1.5).
62#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
63#[serde(deny_unknown_fields)]
64pub struct HiveStatusBusControlV1 {
65    /// The agent it targets; `null`: every agent of this instance.
66    pub target_agent: Option<String>,
67    pub action: HiveBusControlActionV1,
68    /// What a veto names (a lease or handoff reference).
69    pub target_ref: Option<String>,
70    pub reason: String,
71    pub issued_by: String,
72    pub since: UtcTimestamp,
73}
74
75/// Why recorded bus controls do not apply right now (1.5).
76#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
77#[serde(rename_all = "snake_case")]
78pub enum HiveBusControlsInactiveV1 {
79    NotEnrolled,
80    LeaseLapsed,
81    #[serde(other)]
82    Unknown,
83}
84
85/// Output of `lean-ctx hive status --json`. `null` fields are printed.
86#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
87#[serde(deny_unknown_fields)]
88pub struct HiveCliStatusV1 {
89    /// The engine build has the Hive connector.
90    pub available: bool,
91    /// Enrolled and the lease is not lapsed.
92    pub connected: bool,
93    pub instance: Option<HiveStatusInstanceV1>,
94    pub org: Option<HiveStatusOrgV1>,
95    /// Last successful exchange with Hive.
96    pub last_seen: Option<UtcTimestamp>,
97    /// Hive web origin (the configured `[hive] url`, default hosted).
98    pub url: String,
99    /// 1.2.
100    #[serde(default)]
101    pub drafts: HiveStatusDraftsV1,
102    /// 1.2.
103    #[serde(default)]
104    pub filter_packs: Vec<HiveStatusFilterPackV1>,
105    /// 1.2: why the newest policy bundle was refused (the last verified one stays).
106    #[serde(default)]
107    pub policy_refusal: Option<String>,
108    /// 1.3: origin of the Hive web app ("Open Hive"): the one enrollment
109    /// named, else `url`.
110    #[serde(default)]
111    pub web_url: Option<String>,
112    /// 1.5: bus controls in force. Left out when empty, so readers of
113    /// earlier shapes see unchanged output.
114    #[serde(default, skip_serializing_if = "Vec::is_empty")]
115    pub bus_controls: Vec<HiveStatusBusControlV1>,
116    /// 1.5: why recorded controls do not apply; left out when they do or
117    /// none are recorded.
118    #[serde(default, skip_serializing_if = "Option::is_none")]
119    pub bus_controls_inactive: Option<HiveBusControlsInactiveV1>,
120    /// Engine metrics v2 (telemetry SPEC §4); left out when not connected.
121    #[serde(default, skip_serializing_if = "Option::is_none")]
122    pub metrics_v2: Option<HiveStatusMetricsV2>,
123}
124
125/// `metrics_v2` of `hive status --json` (telemetry SPEC §4).
126#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
127#[serde(deny_unknown_fields)]
128pub struct HiveStatusMetricsV2 {
129    /// The workspace's sharing state as Hive last answered it (or the plan
130    /// default before the first answer).
131    pub sharing: super::metrics_v2::SharingStateV2,
132    pub last_sent_at: Option<String>,
133    pub buffered_buckets: u64,
134    pub dropped_buckets: u64,
135    /// `false` while engine metrics are not released: nothing is shared,
136    /// whatever `sharing` says. Older engines omit it.
137    #[serde(default, skip_serializing_if = "Option::is_none")]
138    pub released: Option<bool>,
139}
140
141impl HiveCliStatusV1 {
142    /// Status of an engine that was never connected.
143    pub fn not_connected(url: Option<&str>) -> Self {
144        Self {
145            available: true,
146            connected: false,
147            instance: None,
148            org: None,
149            last_seen: None,
150            url: url.unwrap_or(HIVE_DEFAULT_URL).to_string(),
151            drafts: HiveStatusDraftsV1::default(),
152            filter_packs: Vec::new(),
153            policy_refusal: None,
154            web_url: Some(url.unwrap_or(HIVE_DEFAULT_URL).to_string()),
155            bus_controls: Vec::new(),
156            bus_controls_inactive: None,
157            metrics_v2: None,
158        }
159    }
160}
161
162#[cfg(test)]
163mod tests {
164    use super::*;
165
166    #[test]
167    fn not_connected_prints_nulls() {
168        assert_eq!(
169            serde_json::to_string(&HiveCliStatusV1::not_connected(None)).unwrap(),
170            r#"{"available":true,"connected":false,"instance":null,"org":null,"last_seen":null,"url":"https://hive.leanctx.com","drafts":{"queued":0,"refused":0},"filter_packs":[],"policy_refusal":null,"web_url":"https://hive.leanctx.com"}"#
171        );
172    }
173
174    #[test]
175    fn bus_controls_appear_only_when_present() {
176        let mut status = HiveCliStatusV1::not_connected(None);
177        let quiet = serde_json::to_string(&status).unwrap();
178        assert!(!quiet.contains("bus_controls"));
179        status.bus_controls_inactive = Some(HiveBusControlsInactiveV1::LeaseLapsed);
180        assert!(
181            serde_json::to_string(&status)
182                .unwrap()
183                .contains(r#""bus_controls_inactive":"lease_lapsed""#)
184        );
185    }
186
187    #[test]
188    fn a_1_0_status_still_reads() {
189        let old = r#"{"available":true,"connected":false,"instance":null,"org":null,"last_seen":null,"url":"https://hive.leanctx.com"}"#;
190        let status: HiveCliStatusV1 = serde_json::from_str(old).unwrap();
191        assert_eq!(
192            status,
193            HiveCliStatusV1 {
194                web_url: None,
195                ..HiveCliStatusV1::not_connected(None)
196            }
197        );
198    }
199}