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    /// mz6: origin of the Hive models gateway that serves the subscription
70    /// models (`leanctx` provider). `None`: `https://models.<hive host>`.
71    /// Honoured only when its host is the Hive host or a direct subdomain
72    /// of it (loopback for tests); any other value is refused and the
73    /// instance token is never sent (`core::hive_models::models_origin`).
74    #[serde(skip_serializing_if = "Option::is_none")]
75    pub models_url: Option<String>,
76    /// mz6 owner question Q3 (open): whether Anthropic-shaped agents may
77    /// reach the subscription models through `shape-xlat` routing aliases.
78    /// Default `false`: OpenAI-shaped agents only, the rel2 launch scope;
79    /// `true` has no effect until Q3 is decided (routing refuses aliases to
80    /// `leanctx`).
81    pub models_anthropic_agents: bool,
82    /// How the bus treats a sender presenting as the Queen that is no
83    /// registered Queen session: `warn` (unset: pass, log, mark) or
84    /// `enforce` (refuse); any other value counts as `enforce`. Kept as
85    /// text so a wrong value never breaks the whole config.
86    #[serde(skip_serializing_if = "Option::is_none")]
87    pub queen_identity: Option<String>,
88}
89
90impl Default for HiveConfig {
91    fn default() -> Self {
92        Self {
93            enabled: None,
94            url: None,
95            root_key: None,
96            auto_capture: true,
97            pull_interval_minutes: DEFAULT_PULL_INTERVAL_MINUTES,
98            metrics_upload: true,
99            receipts_upload: true,
100            bus: HiveBusMode::Auto,
101            vault_labels: true,
102            runtime_consent: false,
103            worker: HiveWorkerConfig::default(),
104            models_url: None,
105            models_anthropic_agents: false,
106            queen_identity: None,
107        }
108    }
109}
110
111/// Default minutes between scheduler passes (SPEC-ENGINE §3.16).
112pub const DEFAULT_PULL_INTERVAL_MINUTES: u32 = 5;
113/// Allowed `pull_interval_minutes` (SPEC-ENGINE §3.16).
114pub const PULL_INTERVAL_BOUNDS: std::ops::RangeInclusive<u32> = 1..=60;
115
116impl HiveConfig {
117    /// `pull_interval_minutes` within [`PULL_INTERVAL_BOUNDS`].
118    pub fn pull_interval(&self) -> u32 {
119        self.pull_interval_minutes
120            .clamp(*PULL_INTERVAL_BOUNDS.start(), *PULL_INTERVAL_BOUNDS.end())
121    }
122}
123
124/// Applies the `[hive]` off switches to the connector's local state.
125pub type OffSwitches = fn(&HiveConfig);
126
127static OFF_SWITCHES: OnceLock<OffSwitches> = OnceLock::new();
128
129/// Register what a `config set` of a `hive.` key applies at once (the
130/// connector stops its local recording when a switch is off). The first
131/// registration wins.
132pub fn register_off_switches(apply: OffSwitches) {
133    let _ = OFF_SWITCHES.set(apply);
134}
135
136/// Apply the off switches in `config`. Without a registration there is no
137/// connector state to stop, so nothing happens.
138pub(crate) fn apply_off_switches(config: &HiveConfig) {
139    if let Some(apply) = OFF_SWITCHES.get() {
140        apply(config);
141    }
142}
143
144/// `[hive.worker]`: stricter local caps on top of the server's budget.
145#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
146#[serde(default)]
147pub struct HiveWorkerConfig {
148    #[serde(skip_serializing_if = "Option::is_none")]
149    pub max_tasks_per_day: Option<u32>,
150    #[serde(skip_serializing_if = "Option::is_none")]
151    pub max_tokens_per_day: Option<u64>,
152    #[serde(skip_serializing_if = "Option::is_none")]
153    pub max_usd_micros_per_day: Option<u64>,
154}
155
156/// `[hive]` in `lean-ctx config schema` / `config validate`.
157pub fn schema_section() -> crate::core::config::schema::SectionSchema {
158    use crate::core::config::schema::{KeySchema, SectionSchema};
159    let defaults = HiveConfig::default();
160    let key = |ty: &str, default: serde_json::Value, description: &str, values: Option<&[&str]>| {
161        KeySchema {
162            ty: ty.into(),
163            default,
164            description: description.into(),
165            values: values.map(|v| v.iter().map(|s| (*s).to_string()).collect()),
166            env_override: None,
167        }
168    };
169    let keys = [
170        (
171            "enabled",
172            key(
173                "bool",
174                serde_json::Value::Null,
175                "Pause all Hive traffic with false; unset means on while this machine is connected.",
176                None,
177            ),
178        ),
179        (
180            "url",
181            key(
182                "string",
183                serde_json::json!(HIVE_DEFAULT_URL),
184                "Hive origin (https; a self-hosted Hive may use any https URL).",
185                None,
186            ),
187        ),
188        (
189            "root_key",
190            key(
191                "string",
192                serde_json::Value::Null,
193                "Root public key (base64 Ed25519) of a self-hosted Hive.",
194                None,
195            ),
196        ),
197        (
198            "auto_capture",
199            key(
200                "bool",
201                serde_json::json!(defaults.auto_capture),
202                "Run the capture gate on session learnings and share what passes (renamed from auto_draft).",
203                None,
204            ),
205        ),
206        (
207            "pull_interval_minutes",
208            key(
209                "integer",
210                serde_json::json!(defaults.pull_interval_minutes),
211                "Minutes between Hive sync passes (1-60; values outside are clamped).",
212                None,
213            ),
214        ),
215        (
216            "metrics_upload",
217            key(
218                "bool",
219                serde_json::json!(defaults.metrics_upload),
220                "Send hourly instance metrics to Hive (counts only, including full re-fetches and unregulated program names; never content).",
221                None,
222            ),
223        ),
224        (
225            "receipts_upload",
226            key(
227                "bool",
228                serde_json::json!(defaults.receipts_upload),
229                "Upload context receipts to Hive.",
230                None,
231            ),
232        ),
233        (
234            "bus",
235            key(
236                "string",
237                serde_json::json!("auto"),
238                "Relay the agent bus through Hive (auto) or keep it local (off).",
239                Some(&["auto", "off"]),
240            ),
241        ),
242        (
243            "queen_identity",
244            KeySchema {
245                env_override: Some("LEAN_CTX_HIVE_QUEEN_IDENTITY".into()),
246                ..key(
247                    "string",
248                    serde_json::json!("warn"),
249                    "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.",
250                    Some(&["warn", "enforce"]),
251                )
252            },
253        ),
254        (
255            "vault_labels",
256            key(
257                "bool",
258                serde_json::json!(defaults.vault_labels),
259                "Send checkout folder names as vault labels.",
260                None,
261            ),
262        ),
263        (
264            "runtime_consent",
265            key(
266                "bool",
267                serde_json::json!(defaults.runtime_consent),
268                "You agreed to install the proprietary Intelligence runtime your plan licenses (set by `hive connect` or `hive runtime install`).",
269                None,
270            ),
271        ),
272        (
273            "models_url",
274            key(
275                "string",
276                serde_json::Value::Null,
277                "Origin of the Hive models gateway for the leanctx provider; unset means https://models.<hive host>. Only the Hive host or a direct subdomain of it is honoured.",
278                None,
279            ),
280        ),
281        (
282            "models_anthropic_agents",
283            key(
284                "bool",
285                serde_json::json!(defaults.models_anthropic_agents),
286                "Let Anthropic-shaped agents reach the subscription models through shape-xlat routing aliases; false means OpenAI-shaped agents only. Owner question Q3 is open: true takes effect only once it is decided.",
287                None,
288            ),
289        ),
290        (
291            "worker.max_tasks_per_day",
292            key(
293                "integer",
294                serde_json::Value::Null,
295                "Worker mode: at most this many claimed tasks per day on this machine; only tightens the opt-in budget set on the web.",
296                None,
297            ),
298        ),
299        (
300            "worker.max_tokens_per_day",
301            key(
302                "integer",
303                serde_json::Value::Null,
304                "Worker mode: stop claiming once the day's results reported this many tokens; only tightens the opt-in budget.",
305                None,
306            ),
307        ),
308        (
309            "worker.max_usd_micros_per_day",
310            key(
311                "integer",
312                serde_json::Value::Null,
313                "Worker mode: stop claiming once the day's results reported this cost (USD millionths); only tightens the opt-in budget.",
314                None,
315            ),
316        ),
317    ];
318    SectionSchema {
319        description: "LeanCTX | Hive: workspace knowledge, policies and the shared agent bus."
320            .into(),
321        keys: keys
322            .into_iter()
323            .map(|(name, key)| (name.to_string(), key))
324            .collect(),
325    }
326}