1use 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 #[serde(skip_serializing_if = "Option::is_none")]
46 pub enabled: Option<bool>,
47 #[serde(skip_serializing_if = "Option::is_none")]
49 pub url: Option<String>,
50 #[serde(skip_serializing_if = "Option::is_none")]
52 pub root_key: Option<String>,
53 pub auto_capture: bool,
56 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 pub runtime_consent: bool,
67 pub worker: HiveWorkerConfig,
69 #[serde(skip_serializing_if = "Option::is_none")]
75 pub models_url: Option<String>,
76 pub models_anthropic_agents: bool,
82 #[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
111pub const DEFAULT_PULL_INTERVAL_MINUTES: u32 = 5;
113pub const PULL_INTERVAL_BOUNDS: std::ops::RangeInclusive<u32> = 1..=60;
115
116impl HiveConfig {
117 pub fn pull_interval(&self) -> u32 {
119 self.pull_interval_minutes
120 .clamp(*PULL_INTERVAL_BOUNDS.start(), *PULL_INTERVAL_BOUNDS.end())
121 }
122}
123
124pub type OffSwitches = fn(&HiveConfig);
126
127static OFF_SWITCHES: OnceLock<OffSwitches> = OnceLock::new();
128
129pub fn register_off_switches(apply: OffSwitches) {
133 let _ = OFF_SWITCHES.set(apply);
134}
135
136pub(crate) fn apply_off_switches(config: &HiveConfig) {
139 if let Some(apply) = OFF_SWITCHES.get() {
140 apply(config);
141 }
142}
143
144#[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
156pub 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}