Skip to main content

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}