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}
70
71impl Default for HiveConfig {
72    fn default() -> Self {
73        Self {
74            enabled: None,
75            url: None,
76            root_key: None,
77            auto_capture: true,
78            pull_interval_minutes: DEFAULT_PULL_INTERVAL_MINUTES,
79            metrics_upload: true,
80            receipts_upload: true,
81            bus: HiveBusMode::Auto,
82            vault_labels: true,
83            runtime_consent: false,
84            worker: HiveWorkerConfig::default(),
85        }
86    }
87}
88
89/// Default minutes between scheduler passes (SPEC-ENGINE §3.16).
90pub const DEFAULT_PULL_INTERVAL_MINUTES: u32 = 5;
91/// Allowed `pull_interval_minutes` (SPEC-ENGINE §3.16).
92pub const PULL_INTERVAL_BOUNDS: std::ops::RangeInclusive<u32> = 1..=60;
93
94impl HiveConfig {
95    /// `pull_interval_minutes` within [`PULL_INTERVAL_BOUNDS`].
96    pub fn pull_interval(&self) -> u32 {
97        self.pull_interval_minutes
98            .clamp(*PULL_INTERVAL_BOUNDS.start(), *PULL_INTERVAL_BOUNDS.end())
99    }
100}
101
102/// Applies the `[hive]` off switches to the connector's local state.
103pub type OffSwitches = fn(&HiveConfig);
104
105static OFF_SWITCHES: OnceLock<OffSwitches> = OnceLock::new();
106
107/// Register what a `config set` of a `hive.` key applies at once (the
108/// connector stops its local recording when a switch is off). The first
109/// registration wins.
110pub fn register_off_switches(apply: OffSwitches) {
111    let _ = OFF_SWITCHES.set(apply);
112}
113
114/// Apply the off switches in `config`. Without a registration there is no
115/// connector state to stop, so nothing happens.
116pub(crate) fn apply_off_switches(config: &HiveConfig) {
117    if let Some(apply) = OFF_SWITCHES.get() {
118        apply(config);
119    }
120}
121
122/// `[hive.worker]`: stricter local caps on top of the server's budget.
123#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
124#[serde(default)]
125pub struct HiveWorkerConfig {
126    #[serde(skip_serializing_if = "Option::is_none")]
127    pub max_tasks_per_day: Option<u32>,
128    #[serde(skip_serializing_if = "Option::is_none")]
129    pub max_tokens_per_day: Option<u64>,
130    #[serde(skip_serializing_if = "Option::is_none")]
131    pub max_usd_micros_per_day: Option<u64>,
132}
133
134/// `[hive]` in `lean-ctx config schema` / `config validate`.
135pub fn schema_section() -> crate::core::config::schema::SectionSchema {
136    use crate::core::config::schema::{KeySchema, SectionSchema};
137    let defaults = HiveConfig::default();
138    let key = |ty: &str, default: serde_json::Value, description: &str, values: Option<&[&str]>| {
139        KeySchema {
140            ty: ty.into(),
141            default,
142            description: description.into(),
143            values: values.map(|v| v.iter().map(|s| (*s).to_string()).collect()),
144            env_override: None,
145        }
146    };
147    let keys = [
148        (
149            "enabled",
150            key(
151                "bool",
152                serde_json::Value::Null,
153                "Pause all Hive traffic with false; unset means on while this machine is connected.",
154                None,
155            ),
156        ),
157        (
158            "url",
159            key(
160                "string",
161                serde_json::json!(HIVE_DEFAULT_URL),
162                "Hive origin (https; a self-hosted Hive may use any https URL).",
163                None,
164            ),
165        ),
166        (
167            "root_key",
168            key(
169                "string",
170                serde_json::Value::Null,
171                "Root public key (base64 Ed25519) of a self-hosted Hive.",
172                None,
173            ),
174        ),
175        (
176            "auto_capture",
177            key(
178                "bool",
179                serde_json::json!(defaults.auto_capture),
180                "Run the capture gate on session learnings and share what passes (renamed from auto_draft).",
181                None,
182            ),
183        ),
184        (
185            "pull_interval_minutes",
186            key(
187                "integer",
188                serde_json::json!(defaults.pull_interval_minutes),
189                "Minutes between Hive sync passes (1-60; values outside are clamped).",
190                None,
191            ),
192        ),
193        (
194            "metrics_upload",
195            key(
196                "bool",
197                serde_json::json!(defaults.metrics_upload),
198                "Send hourly instance metrics to Hive (counts only, including full re-fetches and unregulated program names; never content).",
199                None,
200            ),
201        ),
202        (
203            "receipts_upload",
204            key(
205                "bool",
206                serde_json::json!(defaults.receipts_upload),
207                "Upload context receipts to Hive.",
208                None,
209            ),
210        ),
211        (
212            "bus",
213            key(
214                "string",
215                serde_json::json!("auto"),
216                "Relay the agent bus through Hive (auto) or keep it local (off).",
217                Some(&["auto", "off"]),
218            ),
219        ),
220        (
221            "vault_labels",
222            key(
223                "bool",
224                serde_json::json!(defaults.vault_labels),
225                "Send checkout folder names as vault labels.",
226                None,
227            ),
228        ),
229        (
230            "runtime_consent",
231            key(
232                "bool",
233                serde_json::json!(defaults.runtime_consent),
234                "You agreed to install the proprietary Intelligence runtime your plan licenses (set by `hive connect` or `hive runtime install`).",
235                None,
236            ),
237        ),
238        (
239            "worker.max_tasks_per_day",
240            key(
241                "integer",
242                serde_json::Value::Null,
243                "Worker mode: at most this many claimed tasks per day on this machine; only tightens the opt-in budget set on the web.",
244                None,
245            ),
246        ),
247        (
248            "worker.max_tokens_per_day",
249            key(
250                "integer",
251                serde_json::Value::Null,
252                "Worker mode: stop claiming once the day's results reported this many tokens; only tightens the opt-in budget.",
253                None,
254            ),
255        ),
256        (
257            "worker.max_usd_micros_per_day",
258            key(
259                "integer",
260                serde_json::Value::Null,
261                "Worker mode: stop claiming once the day's results reported this cost (USD millionths); only tightens the opt-in budget.",
262                None,
263            ),
264        ),
265    ];
266    SectionSchema {
267        description: "LeanCTX | Hive: workspace knowledge, policies and the shared agent bus."
268            .into(),
269        keys: keys
270            .into_iter()
271            .map(|(name, key)| (name.to_string(), key))
272            .collect(),
273    }
274}