actr-config 0.3.0

Configuration file parser and project manifest support for Actor-RTC framework
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
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
//! Final configuration structures - fully parsed and validated

use actr_protocol::{Acl, ActrType, Realm};
use std::collections::HashMap;
use std::path::PathBuf;
use url::Url;

/// Manifest configuration — parsed from `manifest.toml`.
///
/// Carries workload package metadata, proto exports, dependencies, ACL, and scripts.
/// Does **not** contain runtime fields like `signaling_url`, `realm`, or `ais_endpoint`
/// — those belong to [`RuntimeConfig`] parsed from `actr.toml`.
#[derive(Debug, Clone)]
pub struct ManifestConfig {
    /// Package info
    pub package: PackageInfo,

    /// Exported proto files (contents loaded)
    pub exports: Vec<ProtoFile>,

    /// Service dependencies (expanded)
    pub dependencies: Vec<Dependency>,

    /// Access control list
    pub acl: Option<Acl>,

    /// Service tags (e.g., "latest", "stable", "v1.0")
    pub tags: Vec<String>,

    /// Script commands
    pub scripts: HashMap<String, String>,

    /// Final packaged binary configuration
    pub binary: Option<BinaryConfig>,

    /// Source build configuration
    pub build: Option<BuildConfig>,

    /// Directory containing `manifest.toml`
    pub config_dir: PathBuf,
}

/// Runtime configuration — parsed from `actr.toml`.
///
/// Carries all deployment and networking settings needed by the actor runtime.
/// Required fields (`signaling_url`, `realm`, `ais_endpoint`) are **non-Option**;
/// the parser validates their presence before construction.
#[derive(Debug, Clone)]
pub struct RuntimeConfig {
    /// Package info (provided by caller, e.g. from the .actr package or lock file)
    pub package: PackageInfo,

    // ── Required runtime fields (non-Option) ──
    /// Signaling server URL (validated)
    pub signaling_url: Url,

    /// Owning Realm (Security Realm)
    pub realm: Realm,

    /// AIS (Actor Identity Service) HTTP endpoint
    pub ais_endpoint: String,

    // ── Optional runtime fields ──
    /// Realm secret for AIS registration authentication
    pub realm_secret: Option<String>,

    /// Whether visible in service discovery
    pub visible_in_discovery: bool,

    /// Access control list
    pub acl: Option<Acl>,

    /// Mailbox database path
    ///
    /// - `Some(path)`: use persistent SQLite database
    /// - `None`: use in-memory mode (`:memory:`)
    pub mailbox_path: Option<PathBuf>,

    /// Script commands
    pub scripts: HashMap<String, String>,

    /// WebRTC configuration
    pub webrtc: WebRtcConfig,

    /// Port for listening to inbound WebSocket connections (direct mode, optional)
    ///
    /// When configured, the node starts a WebSocket server on this port at startup.
    /// Peer nodes can connect directly via `ws://<host>:<port>` without relaying.
    pub websocket_listen_port: Option<u16>,

    /// WebSocket hostname or IP advertised to the signaling server
    ///
    /// Used together with `websocket_listen_port`. Reported to the signaling server
    /// during registration so that peer nodes know how to connect directly.
    ///
    /// Defaults to "127.0.0.1" (suitable for local testing only).
    pub websocket_advertised_host: Option<String>,

    /// Observability configuration (logging + tracing)
    pub observability: ObservabilityConfig,

    /// Directory containing the source configuration file (`manifest.toml` or runtime `actr.toml`)
    ///
    /// Used for resolving relative paths and finding lock files
    pub config_dir: PathBuf,

    /// Trust anchors for verifying `.actr` package signatures.
    ///
    /// One entry means a single trust provider; multiple entries means an
    /// automatic fallback chain (first match wins). Consumed by the CLI /
    /// host layer to construct an `actr_hyper::TrustProvider`.
    pub trust: Vec<TrustAnchor>,

    /// Path to the workload package (.actr file)
    pub package_path: Option<PathBuf>,

    /// Web server configuration for `actr run --web`
    pub web: Option<WebConfig>,
}

/// Trust anchor config — a single `[[trust]]` table in `actr.toml`.
///
/// Pure configuration data; the concrete `TrustProvider` instantiation
/// (resolving `pubkey_file` to bytes, wiring up `StaticTrust` / `RegistryTrust`
/// / `ChainTrust`) lives in the host crate so `actr-config` stays free of an
/// `actr-hyper` dependency.
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
#[serde(tag = "kind", rename_all = "snake_case")]
pub enum TrustAnchor {
    /// Pre-shared Ed25519 public key. Accepts any manufacturer.
    Static {
        /// Path to a JSON file containing `public_key` (base64-encoded 32-byte key).
        /// Resolved relative to the `actr.toml` directory during parsing.
        #[serde(default, skip_serializing_if = "Option::is_none")]
        pubkey_file: Option<PathBuf>,
        /// Inline base64 Ed25519 public key (32 bytes). Overrides
        /// `pubkey_file` when both are present.
        #[serde(default, skip_serializing_if = "Option::is_none")]
        pubkey_b64: Option<String>,
    },
    /// Look up MFR public keys from an AIS HTTP endpoint.
    Registry {
        /// AIS HTTP endpoint, e.g. `"http://localhost:8081/ais"`.
        endpoint: String,
    },
}

/// Package info
#[derive(Debug, Clone)]
pub struct PackageInfo {
    /// Package name
    pub name: String,

    /// Actor type
    pub actr_type: ActrType,

    /// Description
    pub description: Option<String>,

    /// Author list
    pub authors: Vec<String>,

    /// License
    pub license: Option<String>,
}

/// Final packaged binary metadata
#[derive(Debug, Clone)]
pub struct BinaryConfig {
    /// Final artifact path on disk
    pub path: PathBuf,

    /// Target triple written into the package manifest
    pub target: Option<String>,
}

/// Build tool kind
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum BuildTool {
    Cargo,
}

/// Cargo artifact kind
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum BuildArtifact {
    Lib,
    Bin,
}

/// Cargo profile kind
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum BuildProfile {
    Dev,
    Release,
}

/// Source build configuration
#[derive(Debug, Clone)]
pub struct BuildConfig {
    pub tool: BuildTool,
    pub manifest_path: PathBuf,
    pub artifact: BuildArtifact,
    pub target: Option<String>,
    pub profile: BuildProfile,
    pub features: Vec<String>,
    pub no_default_features: bool,
    pub post_build: Vec<String>,
}

/// Parsed proto file (file level)
#[derive(Debug, Clone)]
pub struct ProtoFile {
    /// File path (absolute)
    pub path: PathBuf,

    /// File content
    pub content: String,
}

/// Expanded dependency
#[derive(Debug, Clone)]
pub struct Dependency {
    /// Dependency alias (key in dependencies)
    pub alias: String,

    /// Owning Realm
    pub realm: Realm,

    /// Actor type (manufacturer:name:version)
    pub actr_type: Option<ActrType>,

    /// Strict service reference for exact fingerprint matching.
    /// Parsed from `service = "ServiceName:fingerprint"`.
    pub service: Option<ServiceRef>,
}

/// Strict service reference: proto service name + semantic fingerprint.
///
/// When present on a dependency, the runtime only connects to service
/// instances whose registered fingerprint exactly matches.
#[derive(Debug, Clone)]
pub struct ServiceRef {
    /// Proto service name (e.g., "EchoService")
    pub name: String,
    /// Proto semantic fingerprint (e.g., "abc1f3d")
    pub fingerprint: String,
}

/// ICE transport policy
#[derive(Clone, Debug, Default, PartialEq, Eq)]
pub enum IceTransportPolicy {
    /// Use all available candidates (default)
    #[default]
    All,
    /// Use only TURN relay candidates
    Relay,
}

/// ICE server configuration
#[derive(Clone, Debug, Default)]
pub struct IceServer {
    /// Server URL list
    pub urls: Vec<String>,
    /// Username (required for TURN servers)
    pub username: Option<String>,
    /// Credential (required for TURN servers)
    pub credential: Option<String>,
}

/// UDP port configuration
type UdpPorts = Option<(u16, u16)>;

/// WebRTC advanced parameter configuration
#[derive(Clone, Debug)]
pub struct WebRtcAdvancedConfig {
    /// UDP port policy
    pub udp_ports: UdpPorts,
    /// NAT 1:1 public IP mapping
    pub public_ips: Vec<String>,
    /// ICE host candidate acceptance min wait (ms)
    pub ice_host_acceptance_min_wait: u64,
    /// ICE srflx candidate acceptance min wait (ms)
    pub ice_srflx_acceptance_min_wait: u64,
    /// ICE prflx candidate acceptance min wait (ms)
    pub ice_prflx_acceptance_min_wait: u64,
    /// ICE relay candidate acceptance min wait (ms)
    pub ice_relay_acceptance_min_wait: u64,
}

impl WebRtcAdvancedConfig {
    /// Check whether advanced parameters are configured and prefer being Answerer
    pub fn prefer_answerer(&self) -> bool {
        // If port range or public_ips are configured, prefer being Answerer
        self.udp_ports.is_some() || !self.public_ips.is_empty()
    }
}

impl Default for WebRtcAdvancedConfig {
    fn default() -> Self {
        Self {
            udp_ports: UdpPorts::default(),
            public_ips: Vec::new(),
            ice_host_acceptance_min_wait: 0,
            ice_srflx_acceptance_min_wait: 20,
            ice_prflx_acceptance_min_wait: 40,
            ice_relay_acceptance_min_wait: 100,
        }
    }
}

/// WebRTC configuration
#[derive(Clone, Debug, Default)]
pub struct WebRtcConfig {
    /// ICE server list
    pub ice_servers: Vec<IceServer>,
    /// ICE transport policy (All or Relay)
    pub ice_transport_policy: IceTransportPolicy,
    /// Advanced parameter configuration
    pub advanced: WebRtcAdvancedConfig,
}
/// Observability configuration (logging + tracing) resolved from raw config
#[derive(Debug, Clone)]
pub struct ObservabilityConfig {
    /// Filter level (e.g., "info", "debug", "warn", "info,webrtc=debug").
    /// Used when RUST_LOG environment variable is not set. Default: "info".
    pub filter_level: String,

    /// Whether to enable distributed tracing
    pub tracing_enabled: bool,

    /// OTLP/Jaeger gRPC endpoint
    pub tracing_endpoint: String,

    /// Service name reported to the tracing backend
    pub tracing_service_name: String,
}

/// Web server configuration (resolved from raw config)
#[derive(Debug, Clone)]
pub struct WebConfig {
    /// HTTP server port
    pub port: u16,

    /// HTTP server bind host
    pub host: String,

    /// Absolute path to the directory to serve static files from
    pub static_dir: PathBuf,

    /// URL path to the .actr package (served from static dir)
    pub package_url: Option<String>,

    /// URL path to the shared runtime WASM
    pub runtime_wasm_url: Option<String>,
}

// ============================================================================
// ManifestConfig helper methods
// ============================================================================

impl ManifestConfig {
    /// Get the package's ActrType
    pub fn actr_type(&self) -> &ActrType {
        &self.package.actr_type
    }

    /// Get all proto file paths
    pub fn proto_paths(&self) -> Vec<&PathBuf> {
        self.exports.iter().map(|p| &p.path).collect()
    }

    /// Get all proto contents (for computing service fingerprint)
    pub fn proto_contents(&self) -> Vec<&str> {
        self.exports.iter().map(|p| p.content.as_str()).collect()
    }

    /// Find a dependency by alias
    pub fn get_dependency(&self, alias: &str) -> Option<&Dependency> {
        self.dependencies.iter().find(|d| d.alias == alias)
    }

    /// Get a script command
    pub fn get_script(&self, name: &str) -> Option<&str> {
        self.scripts.get(name).map(|s| s.as_str())
    }

    /// List all script names
    pub fn list_scripts(&self) -> Vec<&str> {
        self.scripts.keys().map(|s| s.as_str()).collect()
    }
}

// ============================================================================
// RuntimeConfig helper methods
// ============================================================================

impl RuntimeConfig {
    /// Get the package's ActrType (for registration)
    pub fn actr_type(&self) -> &ActrType {
        &self.package.actr_type
    }

    /// Get all cross-Realm dependencies.
    ///
    /// Returns an empty vec (runtime config does not carry dependencies).
    pub fn cross_realm_dependencies(&self) -> Vec<&Dependency> {
        // RuntimeConfig does not have dependencies field
        vec![]
    }

    /// Get a script command
    pub fn get_script(&self, name: &str) -> Option<&str> {
        self.scripts.get(name).map(|s| s.as_str())
    }
}

// ============================================================================
// PackageInfo helper methods
// ============================================================================

impl PackageInfo {
    /// Get manufacturer (ActrType.manufacturer)
    pub fn manufacturer(&self) -> &str {
        &self.actr_type.manufacturer
    }

    /// Get type name (ActrType.name)
    pub fn type_name(&self) -> &str {
        &self.actr_type.name
    }
}

impl BuildProfile {
    pub fn as_str(self) -> &'static str {
        match self {
            Self::Dev => "dev",
            Self::Release => "release",
        }
    }
}

// ============================================================================
// Dependency helper methods
// ============================================================================

impl Dependency {
    /// Whether this is a cross-Realm dependency
    pub fn is_cross_realm(&self, self_realm: &Realm) -> bool {
        self.realm.realm_id != self_realm.realm_id
    }

    /// Check whether exact fingerprint matching is required (i.e., `service` field exists)
    pub fn requires_exact_fingerprint(&self) -> bool {
        self.service.is_some()
    }

    /// Check whether the fingerprint matches
    ///
    /// - No `service` field: always matches (loose dependency)
    /// - Has `service` field: must match exactly
    pub fn matches_fingerprint(&self, fingerprint: &str) -> bool {
        self.service
            .as_ref()
            .map(|s| s.fingerprint == fingerprint)
            .unwrap_or(true)
    }
}

// ============================================================================
// ProtoFile helper methods
// ============================================================================

impl ProtoFile {
    /// Get file name
    pub fn file_name(&self) -> Option<&str> {
        self.path.file_name()?.to_str()
    }

    /// Get file extension
    pub fn extension(&self) -> Option<&str> {
        self.path.extension()?.to_str()
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_manifest_config_methods() {
        let config = ManifestConfig {
            package: PackageInfo {
                name: "test-service".to_string(),
                actr_type: ActrType {
                    manufacturer: "acme".to_string(),
                    name: "test-service".to_string(),
                    version: "1.0.0".to_string(),
                },
                description: None,
                authors: vec![],
                license: None,
            },
            exports: vec![],
            dependencies: vec![
                Dependency {
                    alias: "user-service".to_string(),
                    realm: Realm { realm_id: 1001 },
                    actr_type: Some(ActrType {
                        manufacturer: "acme".to_string(),
                        name: "user-service".to_string(),
                        version: "2.1.0".to_string(),
                    }),
                    service: Some(ServiceRef {
                        name: "UserService".to_string(),
                        fingerprint: "abc123".to_string(),
                    }),
                },
                Dependency {
                    alias: "shared-logger".to_string(),
                    realm: Realm { realm_id: 9999 },
                    actr_type: Some(ActrType {
                        manufacturer: "common".to_string(),
                        name: "logging-service".to_string(),
                        version: "1.0.0".to_string(),
                    }),
                    service: None,
                },
            ],
            acl: None,
            tags: vec![],
            scripts: HashMap::new(),
            binary: None,
            build: None,
            config_dir: PathBuf::from("."),
        };

        // Test dependency lookup
        assert!(config.get_dependency("user-service").is_some());
        assert!(config.get_dependency("not-exists").is_none());

        // Test fingerprint matching
        let user_dep = config.get_dependency("user-service").unwrap();
        assert!(user_dep.matches_fingerprint("abc123"));
        assert!(!user_dep.matches_fingerprint("different"));

        let logger_dep = config.get_dependency("shared-logger").unwrap();
        assert!(logger_dep.matches_fingerprint("any-fingerprint"));
        assert!(!logger_dep.requires_exact_fingerprint());
    }

    #[test]
    fn test_runtime_config_methods() {
        let config = RuntimeConfig {
            package: PackageInfo {
                name: "test-service".to_string(),
                actr_type: ActrType {
                    manufacturer: "acme".to_string(),
                    name: "test-service".to_string(),
                    version: "1.0.0".to_string(),
                },
                description: None,
                authors: vec![],
                license: None,
            },
            signaling_url: Url::parse("ws://localhost:8081").unwrap(),
            realm: Realm { realm_id: 1001 },
            ais_endpoint: "http://localhost:8081/ais".to_string(),
            realm_secret: None,
            visible_in_discovery: true,
            acl: None,
            mailbox_path: None,
            scripts: HashMap::new(),
            webrtc: WebRtcConfig::default(),
            websocket_listen_port: None,
            websocket_advertised_host: None,
            observability: ObservabilityConfig {
                filter_level: "info".to_string(),
                tracing_enabled: false,
                tracing_endpoint: "http://localhost:4317".to_string(),
                tracing_service_name: "test-service".to_string(),
            },
            config_dir: PathBuf::from("."),
            trust: vec![],
            package_path: None,
            web: None,
        };

        assert_eq!(config.actr_type().name, "test-service");
        assert!(config.cross_realm_dependencies().is_empty());
    }
}