koi-embedded 0.5.1

Embed local network discovery, DNS, health, and TLS directly in your Rust application
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
use std::net::IpAddr;
use std::path::PathBuf;

use koi_common::firewall::{FirewallPort, FirewallProtocol};
use koi_dns::DnsConfig;
use koi_runtime::RuntimeBackendKind;

#[derive(Debug, Clone)]
pub struct KoiConfig {
    pub data_dir: Option<PathBuf>,
    pub service_endpoint: String,
    /// Daemon Access Token for a remote (client) handle's authenticated calls.
    /// `None` → the remote client adopts the local breadcrumb's token when its
    /// endpoint matches `service_endpoint`; set this explicitly when targeting a
    /// remote daemon whose token is not in the local breadcrumb. Without a token,
    /// DAT-gated reads (e.g. posture) and all mutations fail from a remote handle.
    pub service_token: Option<String>,
    pub service_mode: ServiceMode,
    pub http_enabled: bool,
    pub mdns_enabled: bool,
    pub dns_enabled: bool,
    pub health_enabled: bool,
    pub certmesh_enabled: bool,
    pub proxy_enabled: bool,
    pub udp_enabled: bool,
    pub runtime_enabled: bool,
    pub runtime_backend: RuntimeBackendKind,
    /// Translate runtime (container) lifecycle events into mDNS/DNS/health/proxy entries.
    /// Opt-in (default false): a leaf embedded host usually only wants the event stream.
    pub orchestrator_enabled: bool,
    /// Run the certmesh role-driven background loop (trust-bundle pull — policy refresh +
    /// revocation detection — plus cert renewal). Opt-in (default false): only a clustered
    /// embedded CA host needs it.
    pub certmesh_background_enabled: bool,
    pub http_port: u16,
    pub dashboard_enabled: bool,
    pub api_docs_enabled: bool,
    pub mdns_browser_enabled: bool,
    pub announce_http: bool,
    /// Optional Daemon Access Token for the embedded HTTP adapter. `Some` requires the
    /// `x-koi-token` header on every mutation (parity with the daemon); `None` leaves
    /// mutations unauthenticated — safe only behind the loopback bind that is the default.
    pub http_token: Option<String>,
    pub dns_config: DnsConfig,
    pub dns_auto_start: bool,
    pub health_auto_start: bool,
    pub proxy_auto_start: bool,
}

impl KoiConfig {
    /// Collect firewall ports required by the currently-enabled capabilities.
    ///
    /// This mirrors the logic in the standalone Koi daemon's
    /// `firewall_ports_for_config`, but derives from the embedded config.
    pub fn firewall_ports(&self) -> Vec<FirewallPort> {
        use std::collections::HashSet;

        let mut ports = Vec::new();
        if self.mdns_enabled {
            ports.extend(koi_mdns::firewall_ports());
        }
        // Skip an ephemeral port (http_port == 0): the OS assigns it at bind time,
        // so there is no fixed port to pre-open a firewall rule for.
        if self.http_enabled && self.http_port != 0 {
            ports.push(FirewallPort::new(
                "HTTP",
                FirewallProtocol::Tcp,
                self.http_port,
            ));
        }
        if self.dns_enabled {
            ports.extend(koi_dns::firewall_ports(&self.dns_config));
        }

        // Deduplicate by (protocol, port)
        let mut seen = HashSet::new();
        ports
            .into_iter()
            .filter(|p| seen.insert((p.protocol, p.port)))
            .collect()
    }
}

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ServiceMode {
    Auto,
    EmbeddedOnly,
    ClientOnly,
}

impl Default for KoiConfig {
    fn default() -> Self {
        Self {
            data_dir: None,
            service_endpoint: "http://127.0.0.1:5641".to_string(),
            service_token: None,
            service_mode: ServiceMode::Auto,
            http_enabled: false,
            mdns_enabled: true,
            dns_enabled: true,
            health_enabled: false,
            certmesh_enabled: false,
            proxy_enabled: false,
            udp_enabled: false,
            runtime_enabled: false,
            runtime_backend: RuntimeBackendKind::Auto,
            orchestrator_enabled: false,
            certmesh_background_enabled: false,
            http_port: 5641,
            dashboard_enabled: false,
            api_docs_enabled: false,
            mdns_browser_enabled: false,
            announce_http: false,
            http_token: None,
            dns_config: DnsConfig::default(),
            dns_auto_start: false,
            health_auto_start: false,
            proxy_auto_start: false,
        }
    }
}

pub struct DnsConfigBuilder {
    config: DnsConfig,
}

impl DnsConfigBuilder {
    pub fn new(config: DnsConfig) -> Self {
        Self { config }
    }

    pub fn bind_addr(mut self, addr: IpAddr) -> Self {
        self.config.bind_addr = addr;
        self
    }

    pub fn port(mut self, port: u16) -> Self {
        self.config.port = port;
        self
    }

    pub fn zone(mut self, zone: impl Into<String>) -> Self {
        self.config.zone = zone.into();
        self
    }

    pub fn local_ttl(mut self, ttl: u32) -> Self {
        self.config.local_ttl = ttl;
        self
    }

    pub fn allow_public_clients(mut self, allow: bool) -> Self {
        self.config.allow_public_clients = allow;
        self
    }

    pub fn max_qps(mut self, max_qps: u32) -> Self {
        self.config.max_qps = max_qps;
        self
    }

    pub fn local_zone(mut self, enabled: bool) -> Self {
        self.config.local_zone = enabled;
        self
    }

    pub fn build(self) -> DnsConfig {
        self.config
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use std::net::{IpAddr, Ipv4Addr};

    // ── KoiConfig defaults ─────────────────────────────────────────

    #[test]
    fn default_config_has_expected_values() {
        let cfg = KoiConfig::default();
        assert_eq!(cfg.service_endpoint, "http://127.0.0.1:5641");
        assert_eq!(cfg.service_mode, ServiceMode::Auto);
        assert!(!cfg.http_enabled);
        assert!(cfg.mdns_enabled);
        assert!(cfg.dns_enabled);
        assert!(!cfg.health_enabled);
        assert!(!cfg.certmesh_enabled);
        assert!(!cfg.proxy_enabled);
        assert!(!cfg.udp_enabled);
        assert!(!cfg.runtime_enabled);
        assert_eq!(cfg.runtime_backend, RuntimeBackendKind::Auto);
        assert_eq!(cfg.http_port, 5641);
        assert!(!cfg.dashboard_enabled);
        assert!(!cfg.api_docs_enabled);
        assert!(!cfg.mdns_browser_enabled);
        assert!(!cfg.announce_http);
        assert!(!cfg.dns_auto_start);
        assert!(!cfg.health_auto_start);
        assert!(!cfg.proxy_auto_start);
        assert!(cfg.data_dir.is_none());
    }

    #[test]
    fn default_config_clone_is_equal() {
        let cfg = KoiConfig::default();
        let cloned = cfg.clone();
        assert_eq!(cfg.http_port, cloned.http_port);
        assert_eq!(cfg.mdns_enabled, cloned.mdns_enabled);
        assert_eq!(cfg.service_endpoint, cloned.service_endpoint);
    }

    #[test]
    fn default_config_debug_does_not_panic() {
        let cfg = KoiConfig::default();
        let debug = format!("{cfg:?}");
        assert!(debug.contains("KoiConfig"));
    }

    // ── Firewall ports ─────────────────────────────────────────────

    #[test]
    fn firewall_ports_includes_http_when_enabled() {
        let cfg = KoiConfig {
            http_enabled: true,
            mdns_enabled: false,
            dns_enabled: false,
            ..Default::default()
        };
        let ports = cfg.firewall_ports();
        assert!(
            ports.iter().any(|p| p.port == 5641),
            "expected HTTP port 5641 in firewall ports"
        );
    }

    #[test]
    fn firewall_ports_respects_custom_http_port() {
        let cfg = KoiConfig {
            http_enabled: true,
            http_port: 9999,
            mdns_enabled: false,
            dns_enabled: false,
            ..Default::default()
        };
        let ports = cfg.firewall_ports();
        assert!(
            ports.iter().any(|p| p.port == 9999),
            "expected custom HTTP port 9999"
        );
        assert!(
            !ports.iter().any(|p| p.port == 5641),
            "should not have default port when overridden"
        );
    }

    #[test]
    fn firewall_ports_empty_when_all_disabled() {
        let cfg = KoiConfig {
            http_enabled: false,
            mdns_enabled: false,
            dns_enabled: false,
            ..Default::default()
        };
        let ports = cfg.firewall_ports();
        assert!(ports.is_empty(), "all disabled should yield no ports");
    }

    #[test]
    fn firewall_ports_deduplicates() {
        // DNS default port is 53 (TCP+UDP), mDNS is 5353 (UDP).
        // With both enabled we should not have duplicate (protocol, port) pairs.
        let cfg = KoiConfig {
            http_enabled: false,
            mdns_enabled: true,
            dns_enabled: true,
            ..Default::default()
        };
        let ports = cfg.firewall_ports();
        let mut seen = std::collections::HashSet::new();
        for p in &ports {
            assert!(
                seen.insert((p.protocol, p.port)),
                "duplicate firewall port: {:?} {}",
                p.protocol,
                p.port
            );
        }
    }

    // ── ServiceMode ────────────────────────────────────────────────

    #[test]
    fn service_mode_equality() {
        assert_eq!(ServiceMode::Auto, ServiceMode::Auto);
        assert_eq!(ServiceMode::EmbeddedOnly, ServiceMode::EmbeddedOnly);
        assert_eq!(ServiceMode::ClientOnly, ServiceMode::ClientOnly);
        assert_ne!(ServiceMode::Auto, ServiceMode::EmbeddedOnly);
        assert_ne!(ServiceMode::Auto, ServiceMode::ClientOnly);
    }

    #[test]
    fn service_mode_is_copy() {
        let mode = ServiceMode::Auto;
        let copy = mode;
        assert_eq!(mode, copy);
    }

    #[test]
    fn service_mode_debug() {
        let debug = format!("{:?}", ServiceMode::EmbeddedOnly);
        assert!(debug.contains("EmbeddedOnly"));
    }

    // ── DnsConfigBuilder ───────────────────────────────────────────

    #[test]
    fn dns_config_builder_defaults_match_dns_config() {
        let dns_default = DnsConfig::default();
        let built = DnsConfigBuilder::new(DnsConfig::default()).build();
        assert_eq!(built.port, dns_default.port);
        assert_eq!(built.zone, dns_default.zone);
        assert_eq!(built.local_ttl, dns_default.local_ttl);
        assert_eq!(built.allow_public_clients, dns_default.allow_public_clients);
        assert_eq!(built.max_qps, dns_default.max_qps);
        assert_eq!(built.local_zone, dns_default.local_zone);
    }

    #[test]
    fn dns_config_builder_port() {
        let cfg = DnsConfigBuilder::new(DnsConfig::default())
            .port(5353)
            .build();
        assert_eq!(cfg.port, 5353);
    }

    #[test]
    fn dns_config_builder_bind_addr() {
        let addr = IpAddr::V4(Ipv4Addr::new(192, 168, 1, 1));
        let cfg = DnsConfigBuilder::new(DnsConfig::default())
            .bind_addr(addr)
            .build();
        assert_eq!(cfg.bind_addr, addr);
    }

    #[test]
    fn dns_config_builder_zone() {
        let cfg = DnsConfigBuilder::new(DnsConfig::default())
            .zone("home")
            .build();
        assert_eq!(cfg.zone, "home");
    }

    #[test]
    fn dns_config_builder_local_ttl() {
        let cfg = DnsConfigBuilder::new(DnsConfig::default())
            .local_ttl(120)
            .build();
        assert_eq!(cfg.local_ttl, 120);
    }

    #[test]
    fn dns_config_builder_allow_public_clients() {
        let cfg = DnsConfigBuilder::new(DnsConfig::default())
            .allow_public_clients(true)
            .build();
        assert!(cfg.allow_public_clients);
    }

    #[test]
    fn dns_config_builder_max_qps() {
        let cfg = DnsConfigBuilder::new(DnsConfig::default())
            .max_qps(500)
            .build();
        assert_eq!(cfg.max_qps, 500);
    }

    #[test]
    fn dns_config_builder_local_zone() {
        let cfg = DnsConfigBuilder::new(DnsConfig::default())
            .local_zone(false)
            .build();
        assert!(!cfg.local_zone);
    }

    #[test]
    fn dns_config_builder_chaining() {
        let cfg = DnsConfigBuilder::new(DnsConfig::default())
            .port(5353)
            .zone("office")
            .local_ttl(300)
            .allow_public_clients(true)
            .max_qps(1000)
            .local_zone(false)
            .build();
        assert_eq!(cfg.port, 5353);
        assert_eq!(cfg.zone, "office");
        assert_eq!(cfg.local_ttl, 300);
        assert!(cfg.allow_public_clients);
        assert_eq!(cfg.max_qps, 1000);
        assert!(!cfg.local_zone);
    }
}