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/// Whether an org policy is in force or was lifted (1.2).
42#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
43#[serde(rename_all = "snake_case")]
44pub enum HiveOrgPolicyStateV1 {
45    Required,
46    Unpinned,
47}
48
49/// The last verified org policy (1.2).
50#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
51#[serde(deny_unknown_fields)]
52pub struct HiveStatusPolicyV1 {
53    pub version: u64,
54    pub state: HiveOrgPolicyStateV1,
55    pub verified_at: UtcTimestamp,
56    /// End of the signed validity, for a policy in force.
57    pub not_after: Option<UtcTimestamp>,
58    /// Whether this engine enforces it (needs the Intelligence runtime).
59    pub enforced: bool,
60    /// C 1.8, additive status field (no wire change): the filter packs the
61    /// policy requires, in its signed order. Left out when it requires none,
62    /// so earlier output is unchanged; readers treat it as optional.
63    #[serde(default, skip_serializing_if = "Vec::is_empty")]
64    pub required_filter_packs: Vec<HiveStatusRequiredPackV1>,
65}
66
67/// A filter pack the org policy requires (C 1.8, status only).
68#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
69#[serde(deny_unknown_fields)]
70pub struct HiveStatusRequiredPackV1 {
71    pub pack_id: String,
72    pub version: u64,
73    /// Whether the installed filter set holds this pack exactly as the
74    /// policy signed it and applies it in full (the policy runtime's rule).
75    pub installed: bool,
76}
77
78/// An active filter pack (1.2). Bundles verify as a whole, so every listed
79/// pack is active; a refused bundle leaves the previous list in place.
80#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
81#[serde(deny_unknown_fields)]
82pub struct HiveStatusFilterPackV1 {
83    pub pack_id: String,
84    pub version: u64,
85}
86
87/// What a bus control asks of an agent (1.5).
88#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
89#[serde(rename_all = "snake_case")]
90pub enum HiveBusControlActionV1 {
91    Hold,
92    Stop,
93    Veto,
94    #[serde(other)]
95    Unknown,
96}
97
98/// A bus control in force on this machine (1.5).
99#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
100#[serde(deny_unknown_fields)]
101pub struct HiveStatusBusControlV1 {
102    /// The agent it targets; `null`: every agent of this instance.
103    pub target_agent: Option<String>,
104    pub action: HiveBusControlActionV1,
105    /// What a veto names (a lease or handoff reference).
106    pub target_ref: Option<String>,
107    pub reason: String,
108    pub issued_by: String,
109    pub since: UtcTimestamp,
110}
111
112/// Why recorded bus controls do not apply right now (1.5).
113#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
114#[serde(rename_all = "snake_case")]
115pub enum HiveBusControlsInactiveV1 {
116    NotEnrolled,
117    LeaseLapsed,
118    #[serde(other)]
119    Unknown,
120}
121
122/// Output of `lean-ctx hive status --json`. `null` fields are printed.
123#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
124#[serde(deny_unknown_fields)]
125pub struct HiveCliStatusV1 {
126    /// The engine build has the Hive connector.
127    pub available: bool,
128    /// Enrolled and the lease is not lapsed.
129    pub connected: bool,
130    pub instance: Option<HiveStatusInstanceV1>,
131    pub org: Option<HiveStatusOrgV1>,
132    /// Last successful exchange with Hive.
133    pub last_seen: Option<UtcTimestamp>,
134    /// Hive web origin (the configured `[hive] url`, default hosted).
135    pub url: String,
136    /// 1.2.
137    #[serde(default)]
138    pub drafts: HiveStatusDraftsV1,
139    /// 1.2.
140    #[serde(default)]
141    pub policy: Option<HiveStatusPolicyV1>,
142    /// 1.2.
143    #[serde(default)]
144    pub filter_packs: Vec<HiveStatusFilterPackV1>,
145    /// 1.2: why the newest policy bundle was refused (the last verified one stays).
146    #[serde(default)]
147    pub policy_refusal: Option<String>,
148    /// 1.3: origin of the Hive web app ("Open Hive"): the one enrollment
149    /// named, else `url`.
150    #[serde(default)]
151    pub web_url: Option<String>,
152    /// 1.5: bus controls in force. Left out when empty, so readers of
153    /// earlier shapes see unchanged output.
154    #[serde(default, skip_serializing_if = "Vec::is_empty")]
155    pub bus_controls: Vec<HiveStatusBusControlV1>,
156    /// 1.5: why recorded controls do not apply; left out when they do or
157    /// none are recorded.
158    #[serde(default, skip_serializing_if = "Option::is_none")]
159    pub bus_controls_inactive: Option<HiveBusControlsInactiveV1>,
160    /// Engine metrics v2 (telemetry SPEC §4); left out when not connected.
161    #[serde(default, skip_serializing_if = "Option::is_none")]
162    pub metrics_v2: Option<HiveStatusMetricsV2>,
163}
164
165/// `metrics_v2` of `hive status --json` (telemetry SPEC §4).
166#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
167#[serde(deny_unknown_fields)]
168pub struct HiveStatusMetricsV2 {
169    /// The workspace's sharing state as Hive last answered it (or the plan
170    /// default before the first answer).
171    pub sharing: super::metrics_v2::SharingStateV2,
172    pub last_sent_at: Option<String>,
173    pub buffered_buckets: u64,
174    pub dropped_buckets: u64,
175    /// `false` while engine metrics are not released: nothing is shared,
176    /// whatever `sharing` says. Older engines omit it.
177    #[serde(default, skip_serializing_if = "Option::is_none")]
178    pub released: Option<bool>,
179}
180
181impl HiveCliStatusV1 {
182    /// Status of an engine that was never connected.
183    pub fn not_connected(url: Option<&str>) -> Self {
184        Self {
185            available: true,
186            connected: false,
187            instance: None,
188            org: None,
189            last_seen: None,
190            url: url.unwrap_or(HIVE_DEFAULT_URL).to_string(),
191            drafts: HiveStatusDraftsV1::default(),
192            policy: None,
193            filter_packs: Vec::new(),
194            policy_refusal: None,
195            web_url: Some(url.unwrap_or(HIVE_DEFAULT_URL).to_string()),
196            bus_controls: Vec::new(),
197            bus_controls_inactive: None,
198            metrics_v2: None,
199        }
200    }
201}
202
203#[cfg(test)]
204mod tests {
205    use super::*;
206
207    #[test]
208    fn not_connected_prints_nulls() {
209        assert_eq!(
210            serde_json::to_string(&HiveCliStatusV1::not_connected(None)).unwrap(),
211            r#"{"available":true,"connected":false,"instance":null,"org":null,"last_seen":null,"url":"https://hive.leanctx.com","drafts":{"queued":0,"refused":0},"policy":null,"filter_packs":[],"policy_refusal":null,"web_url":"https://hive.leanctx.com"}"#
212        );
213    }
214
215    #[test]
216    fn bus_controls_appear_only_when_present() {
217        let mut status = HiveCliStatusV1::not_connected(None);
218        let quiet = serde_json::to_string(&status).unwrap();
219        assert!(!quiet.contains("bus_controls"));
220        status.bus_controls_inactive = Some(HiveBusControlsInactiveV1::LeaseLapsed);
221        assert!(
222            serde_json::to_string(&status)
223                .unwrap()
224                .contains(r#""bus_controls_inactive":"lease_lapsed""#)
225        );
226    }
227
228    #[test]
229    fn a_1_0_status_still_reads() {
230        let old = r#"{"available":true,"connected":false,"instance":null,"org":null,"last_seen":null,"url":"https://hive.leanctx.com"}"#;
231        let status: HiveCliStatusV1 = serde_json::from_str(old).unwrap();
232        assert_eq!(
233            status,
234            HiveCliStatusV1 {
235                web_url: None,
236                ..HiveCliStatusV1::not_connected(None)
237            }
238        );
239    }
240}