actr_config/config.rs
1//! Final configuration structures - fully parsed and validated
2
3use actr_protocol::{Acl, ActrType, Realm};
4use std::collections::HashMap;
5use std::path::PathBuf;
6use url::Url;
7
8/// Manifest configuration — parsed from `manifest.toml`.
9///
10/// Carries workload package metadata, proto exports, dependencies, ACL, and scripts.
11/// Does **not** contain runtime fields like `signaling_url`, `realm`, or `ais_endpoint`
12/// — those belong to [`RuntimeConfig`] parsed from `actr.toml`.
13#[derive(Debug, Clone)]
14pub struct ManifestConfig {
15 /// Package info
16 pub package: PackageInfo,
17
18 /// Exported proto files (contents loaded)
19 pub exports: Vec<ProtoFile>,
20
21 /// Service dependencies (expanded)
22 pub dependencies: Vec<Dependency>,
23
24 /// Access control list
25 pub acl: Option<Acl>,
26
27 /// Service tags (e.g., "latest", "stable", "v1.0")
28 pub tags: Vec<String>,
29
30 /// Script commands
31 pub scripts: HashMap<String, String>,
32
33 /// Final packaged binary configuration
34 pub binary: Option<BinaryConfig>,
35
36 /// Source build configuration
37 pub build: Option<BuildConfig>,
38
39 /// Directory containing `manifest.toml`
40 pub config_dir: PathBuf,
41}
42
43/// Runtime configuration — parsed from `actr.toml`.
44///
45/// Carries all deployment and networking settings needed by the actor runtime.
46/// Required fields (`signaling_url`, `realm`, `ais_endpoint`) are **non-Option**;
47/// the parser validates their presence before construction.
48#[derive(Debug, Clone)]
49pub struct RuntimeConfig {
50 /// Package info (provided by caller, e.g. from the .actr package or lock file)
51 pub package: PackageInfo,
52
53 // ── Required runtime fields (non-Option) ──
54 /// Signaling server URL (validated)
55 pub signaling_url: Url,
56
57 /// Owning Realm (Security Realm)
58 pub realm: Realm,
59
60 /// AIS (Actor Identity Service) HTTP endpoint
61 pub ais_endpoint: String,
62
63 // ── Optional runtime fields ──
64 /// Realm secret for AIS registration authentication
65 pub realm_secret: Option<String>,
66
67 /// Whether visible in service discovery
68 pub visible_in_discovery: bool,
69
70 /// Access control list
71 pub acl: Option<Acl>,
72
73 /// Mailbox database path
74 ///
75 /// - `Some(path)`: use persistent SQLite database
76 /// - `None`: use in-memory mode (`:memory:`)
77 pub mailbox_path: Option<PathBuf>,
78
79 /// Script commands
80 pub scripts: HashMap<String, String>,
81
82 /// WebRTC configuration
83 pub webrtc: WebRtcConfig,
84
85 /// Port for listening to inbound WebSocket connections (direct mode, optional)
86 ///
87 /// When configured, the node starts a WebSocket server on this port at startup.
88 /// Peer nodes can connect directly via `ws://<host>:<port>` without relaying.
89 pub websocket_listen_port: Option<u16>,
90
91 /// WebSocket hostname or IP advertised to the signaling server
92 ///
93 /// Used together with `websocket_listen_port`. Reported to the signaling server
94 /// during registration so that peer nodes know how to connect directly.
95 ///
96 /// Defaults to "127.0.0.1" (suitable for local testing only).
97 pub websocket_advertised_host: Option<String>,
98
99 /// Observability configuration (logging + tracing)
100 pub observability: ObservabilityConfig,
101
102 /// Directory containing the source configuration file (`manifest.toml` or runtime `actr.toml`)
103 ///
104 /// Used for resolving relative paths and finding lock files
105 pub config_dir: PathBuf,
106
107 /// Trust anchors for verifying `.actr` package signatures.
108 ///
109 /// One entry means a single trust provider; multiple entries means an
110 /// automatic fallback chain (first match wins). Consumed by the CLI /
111 /// host layer to construct an `actr_hyper::TrustProvider`.
112 pub trust: Vec<TrustAnchor>,
113
114 /// Path to the workload package (.actr file)
115 pub package_path: Option<PathBuf>,
116
117 /// Web server configuration for `actr run --web`
118 pub web: Option<WebConfig>,
119}
120
121/// Trust anchor config — a single `[[trust]]` table in `actr.toml`.
122///
123/// Pure configuration data; the concrete `TrustProvider` instantiation
124/// (resolving `pubkey_file` to bytes, wiring up `StaticTrust` / `RegistryTrust`
125/// / `ChainTrust`) lives in the host crate so `actr-config` stays free of an
126/// `actr-hyper` dependency.
127#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
128#[serde(tag = "kind", rename_all = "snake_case")]
129pub enum TrustAnchor {
130 /// Pre-shared Ed25519 public key. Accepts any manufacturer.
131 Static {
132 /// Path to a JSON file containing `public_key` (base64-encoded 32-byte key).
133 /// Resolved relative to the `actr.toml` directory during parsing.
134 #[serde(default, skip_serializing_if = "Option::is_none")]
135 pubkey_file: Option<PathBuf>,
136 /// Inline base64 Ed25519 public key (32 bytes). Overrides
137 /// `pubkey_file` when both are present.
138 #[serde(default, skip_serializing_if = "Option::is_none")]
139 pubkey_b64: Option<String>,
140 },
141 /// Look up MFR public keys from an AIS HTTP endpoint.
142 Registry {
143 /// AIS HTTP endpoint, e.g. `"http://localhost:8081/ais"`.
144 endpoint: String,
145 },
146}
147
148/// Package info
149#[derive(Debug, Clone)]
150pub struct PackageInfo {
151 /// Package name
152 pub name: String,
153
154 /// Actor type
155 pub actr_type: ActrType,
156
157 /// Description
158 pub description: Option<String>,
159
160 /// Author list
161 pub authors: Vec<String>,
162
163 /// License
164 pub license: Option<String>,
165}
166
167/// Final packaged binary metadata
168#[derive(Debug, Clone)]
169pub struct BinaryConfig {
170 /// Final artifact path on disk
171 pub path: PathBuf,
172
173 /// Target triple written into the package manifest
174 pub target: Option<String>,
175}
176
177/// Build tool kind
178#[derive(Debug, Clone, Copy, PartialEq, Eq)]
179pub enum BuildTool {
180 Cargo,
181}
182
183/// Cargo artifact kind
184#[derive(Debug, Clone, Copy, PartialEq, Eq)]
185pub enum BuildArtifact {
186 Lib,
187 Bin,
188}
189
190/// Cargo profile kind
191#[derive(Debug, Clone, Copy, PartialEq, Eq)]
192pub enum BuildProfile {
193 Dev,
194 Release,
195}
196
197/// Source build configuration
198#[derive(Debug, Clone)]
199pub struct BuildConfig {
200 pub tool: BuildTool,
201 pub manifest_path: PathBuf,
202 pub artifact: BuildArtifact,
203 pub target: Option<String>,
204 pub profile: BuildProfile,
205 pub features: Vec<String>,
206 pub no_default_features: bool,
207 pub post_build: Vec<String>,
208}
209
210/// Parsed proto file (file level)
211#[derive(Debug, Clone)]
212pub struct ProtoFile {
213 /// File path (absolute)
214 pub path: PathBuf,
215
216 /// File content
217 pub content: String,
218}
219
220/// Expanded dependency
221#[derive(Debug, Clone)]
222pub struct Dependency {
223 /// Dependency alias (key in dependencies)
224 pub alias: String,
225
226 /// Owning Realm
227 pub realm: Realm,
228
229 /// Actor type (manufacturer:name:version)
230 pub actr_type: Option<ActrType>,
231
232 /// Strict service reference for exact fingerprint matching.
233 /// Parsed from `service = "ServiceName:fingerprint"`.
234 pub service: Option<ServiceRef>,
235}
236
237/// Strict service reference: proto service name + semantic fingerprint.
238///
239/// When present on a dependency, the runtime only connects to service
240/// instances whose registered fingerprint exactly matches.
241#[derive(Debug, Clone)]
242pub struct ServiceRef {
243 /// Proto service name (e.g., "EchoService")
244 pub name: String,
245 /// Proto semantic fingerprint (e.g., "abc1f3d")
246 pub fingerprint: String,
247}
248
249/// ICE transport policy
250#[derive(Clone, Debug, Default, PartialEq, Eq)]
251pub enum IceTransportPolicy {
252 /// Use all available candidates (default)
253 #[default]
254 All,
255 /// Use only TURN relay candidates
256 Relay,
257}
258
259/// ICE server configuration
260#[derive(Clone, Debug, Default)]
261pub struct IceServer {
262 /// Server URL list
263 pub urls: Vec<String>,
264 /// Username (required for TURN servers)
265 pub username: Option<String>,
266 /// Credential (required for TURN servers)
267 pub credential: Option<String>,
268}
269
270/// UDP port configuration
271type UdpPorts = Option<(u16, u16)>;
272
273/// WebRTC advanced parameter configuration
274#[derive(Clone, Debug)]
275pub struct WebRtcAdvancedConfig {
276 /// UDP port policy
277 pub udp_ports: UdpPorts,
278 /// NAT 1:1 public IP mapping
279 pub public_ips: Vec<String>,
280 /// ICE host candidate acceptance min wait (ms)
281 pub ice_host_acceptance_min_wait: u64,
282 /// ICE srflx candidate acceptance min wait (ms)
283 pub ice_srflx_acceptance_min_wait: u64,
284 /// ICE prflx candidate acceptance min wait (ms)
285 pub ice_prflx_acceptance_min_wait: u64,
286 /// ICE relay candidate acceptance min wait (ms)
287 pub ice_relay_acceptance_min_wait: u64,
288}
289
290impl WebRtcAdvancedConfig {
291 /// Check whether advanced parameters are configured and prefer being Answerer
292 pub fn prefer_answerer(&self) -> bool {
293 // If port range or public_ips are configured, prefer being Answerer
294 self.udp_ports.is_some() || !self.public_ips.is_empty()
295 }
296}
297
298impl Default for WebRtcAdvancedConfig {
299 fn default() -> Self {
300 Self {
301 udp_ports: UdpPorts::default(),
302 public_ips: Vec::new(),
303 ice_host_acceptance_min_wait: 0,
304 ice_srflx_acceptance_min_wait: 20,
305 ice_prflx_acceptance_min_wait: 40,
306 ice_relay_acceptance_min_wait: 100,
307 }
308 }
309}
310
311/// WebRTC configuration
312#[derive(Clone, Debug, Default)]
313pub struct WebRtcConfig {
314 /// ICE server list
315 pub ice_servers: Vec<IceServer>,
316 /// ICE transport policy (All or Relay)
317 pub ice_transport_policy: IceTransportPolicy,
318 /// Advanced parameter configuration
319 pub advanced: WebRtcAdvancedConfig,
320}
321/// Observability configuration (logging + tracing) resolved from raw config
322#[derive(Debug, Clone)]
323pub struct ObservabilityConfig {
324 /// Filter level (e.g., "info", "debug", "warn", "info,webrtc=debug").
325 /// Used when RUST_LOG environment variable is not set. Default: "info".
326 pub filter_level: String,
327
328 /// Whether to enable distributed tracing
329 pub tracing_enabled: bool,
330
331 /// OTLP/Jaeger gRPC endpoint
332 pub tracing_endpoint: String,
333
334 /// Service name reported to the tracing backend
335 pub tracing_service_name: String,
336}
337
338/// Web server configuration (resolved from raw config)
339#[derive(Debug, Clone)]
340pub struct WebConfig {
341 /// HTTP server port
342 pub port: u16,
343
344 /// HTTP server bind host
345 pub host: String,
346
347 /// Absolute path to the directory to serve static files from
348 pub static_dir: PathBuf,
349
350 /// URL path to the .actr package (served from static dir)
351 pub package_url: Option<String>,
352
353 /// URL path to the shared runtime WASM
354 pub runtime_wasm_url: Option<String>,
355}
356
357// ============================================================================
358// ManifestConfig helper methods
359// ============================================================================
360
361impl ManifestConfig {
362 /// Get the package's ActrType
363 pub fn actr_type(&self) -> &ActrType {
364 &self.package.actr_type
365 }
366
367 /// Get all proto file paths
368 pub fn proto_paths(&self) -> Vec<&PathBuf> {
369 self.exports.iter().map(|p| &p.path).collect()
370 }
371
372 /// Get all proto contents (for computing service fingerprint)
373 pub fn proto_contents(&self) -> Vec<&str> {
374 self.exports.iter().map(|p| p.content.as_str()).collect()
375 }
376
377 /// Find a dependency by alias
378 pub fn get_dependency(&self, alias: &str) -> Option<&Dependency> {
379 self.dependencies.iter().find(|d| d.alias == alias)
380 }
381
382 /// Get a script command
383 pub fn get_script(&self, name: &str) -> Option<&str> {
384 self.scripts.get(name).map(|s| s.as_str())
385 }
386
387 /// List all script names
388 pub fn list_scripts(&self) -> Vec<&str> {
389 self.scripts.keys().map(|s| s.as_str()).collect()
390 }
391}
392
393// ============================================================================
394// RuntimeConfig helper methods
395// ============================================================================
396
397impl RuntimeConfig {
398 /// Get the package's ActrType (for registration)
399 pub fn actr_type(&self) -> &ActrType {
400 &self.package.actr_type
401 }
402
403 /// Get all cross-Realm dependencies.
404 ///
405 /// Returns an empty vec (runtime config does not carry dependencies).
406 pub fn cross_realm_dependencies(&self) -> Vec<&Dependency> {
407 // RuntimeConfig does not have dependencies field
408 vec![]
409 }
410
411 /// Get a script command
412 pub fn get_script(&self, name: &str) -> Option<&str> {
413 self.scripts.get(name).map(|s| s.as_str())
414 }
415}
416
417// ============================================================================
418// PackageInfo helper methods
419// ============================================================================
420
421impl PackageInfo {
422 /// Get manufacturer (ActrType.manufacturer)
423 pub fn manufacturer(&self) -> &str {
424 &self.actr_type.manufacturer
425 }
426
427 /// Get type name (ActrType.name)
428 pub fn type_name(&self) -> &str {
429 &self.actr_type.name
430 }
431}
432
433impl BuildProfile {
434 pub fn as_str(self) -> &'static str {
435 match self {
436 Self::Dev => "dev",
437 Self::Release => "release",
438 }
439 }
440}
441
442// ============================================================================
443// Dependency helper methods
444// ============================================================================
445
446impl Dependency {
447 /// Whether this is a cross-Realm dependency
448 pub fn is_cross_realm(&self, self_realm: &Realm) -> bool {
449 self.realm.realm_id != self_realm.realm_id
450 }
451
452 /// Check whether exact fingerprint matching is required (i.e., `service` field exists)
453 pub fn requires_exact_fingerprint(&self) -> bool {
454 self.service.is_some()
455 }
456
457 /// Check whether the fingerprint matches
458 ///
459 /// - No `service` field: always matches (loose dependency)
460 /// - Has `service` field: must match exactly
461 pub fn matches_fingerprint(&self, fingerprint: &str) -> bool {
462 self.service
463 .as_ref()
464 .map(|s| s.fingerprint == fingerprint)
465 .unwrap_or(true)
466 }
467}
468
469// ============================================================================
470// ProtoFile helper methods
471// ============================================================================
472
473impl ProtoFile {
474 /// Get file name
475 pub fn file_name(&self) -> Option<&str> {
476 self.path.file_name()?.to_str()
477 }
478
479 /// Get file extension
480 pub fn extension(&self) -> Option<&str> {
481 self.path.extension()?.to_str()
482 }
483}
484
485#[cfg(test)]
486mod tests {
487 use super::*;
488
489 #[test]
490 fn test_manifest_config_methods() {
491 let config = ManifestConfig {
492 package: PackageInfo {
493 name: "test-service".to_string(),
494 actr_type: ActrType {
495 manufacturer: "acme".to_string(),
496 name: "test-service".to_string(),
497 version: "1.0.0".to_string(),
498 },
499 description: None,
500 authors: vec![],
501 license: None,
502 },
503 exports: vec![],
504 dependencies: vec![
505 Dependency {
506 alias: "user-service".to_string(),
507 realm: Realm { realm_id: 1001 },
508 actr_type: Some(ActrType {
509 manufacturer: "acme".to_string(),
510 name: "user-service".to_string(),
511 version: "2.1.0".to_string(),
512 }),
513 service: Some(ServiceRef {
514 name: "UserService".to_string(),
515 fingerprint: "abc123".to_string(),
516 }),
517 },
518 Dependency {
519 alias: "shared-logger".to_string(),
520 realm: Realm { realm_id: 9999 },
521 actr_type: Some(ActrType {
522 manufacturer: "common".to_string(),
523 name: "logging-service".to_string(),
524 version: "1.0.0".to_string(),
525 }),
526 service: None,
527 },
528 ],
529 acl: None,
530 tags: vec![],
531 scripts: HashMap::new(),
532 binary: None,
533 build: None,
534 config_dir: PathBuf::from("."),
535 };
536
537 // Test dependency lookup
538 assert!(config.get_dependency("user-service").is_some());
539 assert!(config.get_dependency("not-exists").is_none());
540
541 // Test fingerprint matching
542 let user_dep = config.get_dependency("user-service").unwrap();
543 assert!(user_dep.matches_fingerprint("abc123"));
544 assert!(!user_dep.matches_fingerprint("different"));
545
546 let logger_dep = config.get_dependency("shared-logger").unwrap();
547 assert!(logger_dep.matches_fingerprint("any-fingerprint"));
548 assert!(!logger_dep.requires_exact_fingerprint());
549 }
550
551 #[test]
552 fn test_runtime_config_methods() {
553 let config = RuntimeConfig {
554 package: PackageInfo {
555 name: "test-service".to_string(),
556 actr_type: ActrType {
557 manufacturer: "acme".to_string(),
558 name: "test-service".to_string(),
559 version: "1.0.0".to_string(),
560 },
561 description: None,
562 authors: vec![],
563 license: None,
564 },
565 signaling_url: Url::parse("ws://localhost:8081").unwrap(),
566 realm: Realm { realm_id: 1001 },
567 ais_endpoint: "http://localhost:8081/ais".to_string(),
568 realm_secret: None,
569 visible_in_discovery: true,
570 acl: None,
571 mailbox_path: None,
572 scripts: HashMap::new(),
573 webrtc: WebRtcConfig::default(),
574 websocket_listen_port: None,
575 websocket_advertised_host: None,
576 observability: ObservabilityConfig {
577 filter_level: "info".to_string(),
578 tracing_enabled: false,
579 tracing_endpoint: "http://localhost:4317".to_string(),
580 tracing_service_name: "test-service".to_string(),
581 },
582 config_dir: PathBuf::from("."),
583 trust: vec![],
584 package_path: None,
585 web: None,
586 };
587
588 assert_eq!(config.actr_type().name, "test-service");
589 assert!(config.cross_realm_dependencies().is_empty());
590 }
591}