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}
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
89pub const DEFAULT_PULL_INTERVAL_MINUTES: u32 = 5;
91pub const PULL_INTERVAL_BOUNDS: std::ops::RangeInclusive<u32> = 1..=60;
93
94impl HiveConfig {
95 pub fn pull_interval(&self) -> u32 {
97 self.pull_interval_minutes
98 .clamp(*PULL_INTERVAL_BOUNDS.start(), *PULL_INTERVAL_BOUNDS.end())
99 }
100}
101
102pub type OffSwitches = fn(&HiveConfig);
104
105static OFF_SWITCHES: OnceLock<OffSwitches> = OnceLock::new();
106
107pub fn register_off_switches(apply: OffSwitches) {
111 let _ = OFF_SWITCHES.set(apply);
112}
113
114pub(crate) fn apply_off_switches(config: &HiveConfig) {
117 if let Some(apply) = OFF_SWITCHES.get() {
118 apply(config);
119 }
120}
121
122#[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
134pub 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}