Skip to main content

lean_ctx/core/config/
hive.rs

1// SPDX-License-Identifier: Apache-2.0
2//! `[hive]` configuration.
3//!
4//! ```toml
5//! [hive]
6//! # enabled = true          # unset: on while this machine is connected
7//! url = "https://hive.leanctx.com"
8//! # root_key = "<base64>"   # self-hosted Hive: its root public key
9//! auto_capture = true       # 3.11.5 name `auto_draft` is read as an alias
10//! pull_interval_minutes = 5  # 1..=60
11//! metrics_upload = true
12//! receipts_upload = true
13//! bus = "auto"              # auto | off
14//! vault_labels = true
15//!
16//! [hive.worker]             # stricter local caps on the opt-in budget
17//! # max_tasks_per_day = 3
18//! # max_tokens_per_day = 200000
19//! # max_usd_micros_per_day = 2000000
20//! ```
21//!
22//! The Hive connector reads this section and re-exports these types as
23//! its own config. It also registers what a `config set`
24//! of a `hive.` key applies at once ([`register_off_switches`]).
25
26use std::sync::OnceLock;
27
28use serde::{Deserialize, Serialize};
29
30use lean_ctx_protocol::hive::HIVE_DEFAULT_URL;
31
32#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
33#[serde(rename_all = "snake_case")]
34pub enum HiveBusMode {
35    #[default]
36    Auto,
37    Off,
38}
39
40#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
41#[serde(default)]
42pub struct HiveConfig {
43    /// `None`: on while this machine is connected. `false` pauses all Hive
44    /// traffic without disconnecting.
45    #[serde(skip_serializing_if = "Option::is_none")]
46    pub enabled: Option<bool>,
47    /// Hive origin; any `https` URL for a self-hosted Hive.
48    #[serde(skip_serializing_if = "Option::is_none")]
49    pub url: Option<String>,
50    /// Root public key (base64 Ed25519) of a self-hosted Hive.
51    #[serde(skip_serializing_if = "Option::is_none")]
52    pub root_key: Option<String>,
53    /// Run the capture gate on session learnings (3.11.5: `auto_draft`,
54    /// still read as an alias, `core::config::renamed`).
55    pub auto_capture: bool,
56    /// Minutes between scheduler passes; [`HiveConfig::pull_interval`]
57    /// clamps it to [`PULL_INTERVAL_BOUNDS`].
58    pub pull_interval_minutes: u32,
59    pub metrics_upload: bool,
60    pub receipts_upload: bool,
61    pub bus: HiveBusMode,
62    pub vault_labels: bool,
63    /// The user agreed to install the proprietary Intelligence runtime that
64    /// the lease licenses (`hive connect` asks, `hive runtime install`
65    /// records it). Nothing installs it without this consent.
66    pub runtime_consent: bool,
67    /// Worker mode: local caps that only tighten the server opt-in budget.
68    pub worker: HiveWorkerConfig,
69    /// How the bus treats a sender presenting as the Queen that is no
70    /// registered Queen session: `warn` (unset: pass, log, mark) or
71    /// `enforce` (refuse); any other value counts as `enforce`. Kept as
72    /// text so a wrong value never breaks the whole config.
73    #[serde(skip_serializing_if = "Option::is_none")]
74    pub queen_identity: Option<String>,
75}
76
77impl Default for HiveConfig {
78    fn default() -> Self {
79        Self {
80            enabled: None,
81            url: None,
82            root_key: None,
83            auto_capture: true,
84            pull_interval_minutes: DEFAULT_PULL_INTERVAL_MINUTES,
85            metrics_upload: true,
86            receipts_upload: true,
87            bus: HiveBusMode::Auto,
88            vault_labels: true,
89            runtime_consent: false,
90            worker: HiveWorkerConfig::default(),
91            queen_identity: None,
92        }
93    }
94}
95
96/// Default minutes between scheduler passes (SPEC-ENGINE §3.16).
97pub const DEFAULT_PULL_INTERVAL_MINUTES: u32 = 5;
98/// Allowed `pull_interval_minutes` (SPEC-ENGINE §3.16).
99pub const PULL_INTERVAL_BOUNDS: std::ops::RangeInclusive<u32> = 1..=60;
100
101impl HiveConfig {
102    /// `pull_interval_minutes` within [`PULL_INTERVAL_BOUNDS`].
103    pub fn pull_interval(&self) -> u32 {
104        self.pull_interval_minutes
105            .clamp(*PULL_INTERVAL_BOUNDS.start(), *PULL_INTERVAL_BOUNDS.end())
106    }
107}
108
109/// Applies the `[hive]` off switches to the connector's local state.
110pub type OffSwitches = fn(&HiveConfig);
111
112static OFF_SWITCHES: OnceLock<OffSwitches> = OnceLock::new();
113
114/// Register what a `config set` of a `hive.` key applies at once (the
115/// connector stops its local recording when a switch is off). The first
116/// registration wins.
117pub fn register_off_switches(apply: OffSwitches) {
118    let _ = OFF_SWITCHES.set(apply);
119}
120
121/// Apply the off switches in `config`. Without a registration there is no
122/// connector state to stop, so nothing happens.
123pub(crate) fn apply_off_switches(config: &HiveConfig) {
124    if let Some(apply) = OFF_SWITCHES.get() {
125        apply(config);
126    }
127}
128
129/// `[hive.worker]`: stricter local caps on top of the server's budget.
130#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
131#[serde(default)]
132pub struct HiveWorkerConfig {
133    #[serde(skip_serializing_if = "Option::is_none")]
134    pub max_tasks_per_day: Option<u32>,
135    #[serde(skip_serializing_if = "Option::is_none")]
136    pub max_tokens_per_day: Option<u64>,
137    #[serde(skip_serializing_if = "Option::is_none")]
138    pub max_usd_micros_per_day: Option<u64>,
139}
140
141/// `[hive]` in `lean-ctx config schema` / `config validate`.
142pub fn schema_section() -> crate::core::config::schema::SectionSchema {
143    use crate::core::config::schema::{KeySchema, SectionSchema};
144    let defaults = HiveConfig::default();
145    let key = |ty: &str, default: serde_json::Value, description: &str, values: Option<&[&str]>| {
146        KeySchema {
147            ty: ty.into(),
148            default,
149            description: description.into(),
150            values: values.map(|v| v.iter().map(|s| (*s).to_string()).collect()),
151            env_override: None,
152        }
153    };
154    let keys = [
155        (
156            "enabled",
157            key(
158                "bool",
159                serde_json::Value::Null,
160                "Pause all Hive traffic with false; unset means on while this machine is connected.",
161                None,
162            ),
163        ),
164        (
165            "url",
166            key(
167                "string",
168                serde_json::json!(HIVE_DEFAULT_URL),
169                "Hive origin (https; a self-hosted Hive may use any https URL).",
170                None,
171            ),
172        ),
173        (
174            "root_key",
175            key(
176                "string",
177                serde_json::Value::Null,
178                "Root public key (base64 Ed25519) of a self-hosted Hive.",
179                None,
180            ),
181        ),
182        (
183            "auto_capture",
184            key(
185                "bool",
186                serde_json::json!(defaults.auto_capture),
187                "Run the capture gate on session learnings and share what passes (renamed from auto_draft).",
188                None,
189            ),
190        ),
191        (
192            "pull_interval_minutes",
193            key(
194                "integer",
195                serde_json::json!(defaults.pull_interval_minutes),
196                "Minutes between Hive sync passes (1-60; values outside are clamped).",
197                None,
198            ),
199        ),
200        (
201            "metrics_upload",
202            key(
203                "bool",
204                serde_json::json!(defaults.metrics_upload),
205                "Send hourly instance metrics to Hive (counts only, including full re-fetches and unregulated program names; never content).",
206                None,
207            ),
208        ),
209        (
210            "receipts_upload",
211            key(
212                "bool",
213                serde_json::json!(defaults.receipts_upload),
214                "Upload context receipts to Hive.",
215                None,
216            ),
217        ),
218        (
219            "bus",
220            key(
221                "string",
222                serde_json::json!("auto"),
223                "Relay the agent bus through Hive (auto) or keep it local (off).",
224                Some(&["auto", "off"]),
225            ),
226        ),
227        (
228            "queen_identity",
229            KeySchema {
230                env_override: Some("LEAN_CTX_HIVE_QUEEN_IDENTITY".into()),
231                ..key(
232                    "string",
233                    serde_json::json!("warn"),
234                    "A bus sender presenting as the Queen that is no registered Queen session: warn (pass, log in bus/queen-warnings.json, mark for the receiver) or enforce (refuse); any other value counts as enforce. Enforce only once queen-sessions.json lists every Queen session.",
235                    Some(&["warn", "enforce"]),
236                )
237            },
238        ),
239        (
240            "vault_labels",
241            key(
242                "bool",
243                serde_json::json!(defaults.vault_labels),
244                "Send checkout folder names as vault labels.",
245                None,
246            ),
247        ),
248        (
249            "runtime_consent",
250            key(
251                "bool",
252                serde_json::json!(defaults.runtime_consent),
253                "You agreed to install the proprietary Intelligence runtime your plan licenses (set by `hive connect` or `hive runtime install`).",
254                None,
255            ),
256        ),
257        (
258            "worker.max_tasks_per_day",
259            key(
260                "integer",
261                serde_json::Value::Null,
262                "Worker mode: at most this many claimed tasks per day on this machine; only tightens the opt-in budget set on the web.",
263                None,
264            ),
265        ),
266        (
267            "worker.max_tokens_per_day",
268            key(
269                "integer",
270                serde_json::Value::Null,
271                "Worker mode: stop claiming once the day's results reported this many tokens; only tightens the opt-in budget.",
272                None,
273            ),
274        ),
275        (
276            "worker.max_usd_micros_per_day",
277            key(
278                "integer",
279                serde_json::Value::Null,
280                "Worker mode: stop claiming once the day's results reported this cost (USD millionths); only tightens the opt-in budget.",
281                None,
282            ),
283        ),
284    ];
285    SectionSchema {
286        description: "LeanCTX | Hive: workspace knowledge, policies and the shared agent bus."
287            .into(),
288        keys: keys
289            .into_iter()
290            .map(|(name, key)| (name.to_string(), key))
291            .collect(),
292    }
293}