Skip to main content

a3s_box_core/
compose.rs

1//! Compose file types for multi-container orchestration.
2//!
3//! Defines A3S ACL and Docker Compose-compatible YAML schemas for declaring
4//! multi-service workloads. Each service maps to a single MicroVM.
5
6use serde::{Deserialize, Serialize};
7use std::collections::HashMap;
8
9mod acl;
10mod diagnostic;
11mod interpolation;
12mod normalization;
13mod normalized;
14mod schema;
15
16pub use acl::ComposeAclError;
17pub use diagnostic::{ComposeDiagnostic, ComposeDiagnosticCode, ComposeNormalizationError};
18pub use interpolation::{interpolate_compose_yaml, ComposeInterpolationError};
19pub use normalization::{normalize_compose, normalize_compose_config, ComposeSourceFormat};
20pub use normalized::{
21    NormalizedComposeConfig, NormalizedDependsOn, NormalizedHealthcheckConfig,
22    NormalizedNetworkDeclaration, NormalizedServiceConfig, NormalizedServiceNetwork,
23    NormalizedVolumeDeclaration,
24};
25
26/// Top-level compose file configuration.
27///
28/// Compatible with a subset of docker-compose v3 syntax:
29/// ```yaml
30/// version: "3"
31/// services:
32///   web:
33///     image: nginx:latest
34///     ports: ["8080:80"]
35///     depends_on: [db]
36///   db:
37///     image: postgres:16
38///     environment:
39///       POSTGRES_PASSWORD: secret
40///     volumes: ["pgdata:/var/lib/postgresql/data"]
41/// volumes:
42///   pgdata:
43/// networks:
44///   default:
45/// ```
46#[derive(Debug, Clone, Serialize, Deserialize)]
47#[serde(deny_unknown_fields)]
48pub struct ComposeConfig {
49    /// Compose file version (informational, not enforced).
50    #[serde(default)]
51    pub version: Option<String>,
52
53    /// Service definitions keyed by name.
54    pub services: HashMap<String, ServiceConfig>,
55
56    /// Named volume declarations (value is currently unused, reserved for driver options).
57    #[serde(default)]
58    pub volumes: HashMap<String, Option<VolumeDeclaration>>,
59
60    /// Named network declarations.
61    #[serde(default)]
62    pub networks: HashMap<String, Option<NetworkDeclaration>>,
63}
64
65/// A single service in a compose file.
66#[derive(Debug, Clone, Serialize, Deserialize, Default)]
67#[serde(deny_unknown_fields)]
68pub struct ServiceConfig {
69    /// OCI image reference (e.g., "nginx:latest").
70    #[serde(default)]
71    pub image: Option<String>,
72
73    /// Override the container entrypoint.
74    #[serde(default)]
75    pub entrypoint: Option<StringOrList>,
76
77    /// Override the container command.
78    #[serde(default)]
79    pub command: Option<StringOrList>,
80
81    /// Environment variables.
82    #[serde(default)]
83    pub environment: EnvVars,
84
85    /// Environment files to load before `environment` overrides.
86    #[serde(default)]
87    pub env_file: StringOrList,
88
89    /// Port mappings ("host:container").
90    #[serde(default)]
91    pub ports: Vec<String>,
92
93    /// Volume mounts ("name:/path" or "/host:/container").
94    #[serde(default)]
95    pub volumes: Vec<String>,
96
97    /// Services this service depends on (started first).
98    #[serde(default)]
99    pub depends_on: DependsOn,
100
101    /// Networks to connect to.
102    #[serde(default)]
103    pub networks: ServiceNetworks,
104
105    /// Number of CPUs.
106    #[serde(default)]
107    pub cpus: Option<u32>,
108
109    /// Memory limit (e.g., "512m", "1g").
110    #[serde(default)]
111    pub mem_limit: Option<String>,
112
113    /// Restart policy: "no", "always", "on-failure", "unless-stopped".
114    #[serde(default)]
115    pub restart: Option<String>,
116
117    /// Custom DNS servers.
118    #[serde(default)]
119    pub dns: DnsConfig,
120
121    /// tmpfs mounts.
122    #[serde(default)]
123    pub tmpfs: StringOrList,
124
125    /// Linux capabilities to add.
126    #[serde(default)]
127    pub cap_add: Vec<String>,
128
129    /// Linux capabilities to drop.
130    #[serde(default)]
131    pub cap_drop: Vec<String>,
132
133    /// Privileged mode.
134    #[serde(default)]
135    pub privileged: bool,
136
137    /// Custom labels.
138    #[serde(default)]
139    pub labels: Labels,
140
141    /// Health check configuration.
142    #[serde(default)]
143    pub healthcheck: Option<HealthcheckConfig>,
144
145    /// Working directory inside the container.
146    #[serde(default)]
147    pub working_dir: Option<String>,
148
149    /// Hostname inside the container.
150    #[serde(default)]
151    pub hostname: Option<String>,
152
153    /// Static host entries (`HOST:IP`).
154    #[serde(default)]
155    pub extra_hosts: StringOrList,
156}
157
158/// Health check configuration for a service.
159#[derive(Debug, Clone, Serialize, Deserialize)]
160#[serde(deny_unknown_fields)]
161pub struct HealthcheckConfig {
162    /// Command to run (e.g., ["CMD", "curl", "-f", "http://localhost/"]).
163    #[serde(default)]
164    pub test: StringOrList,
165    /// Disable the image or service health check.
166    #[serde(default)]
167    pub disable: bool,
168    /// Interval between checks (e.g., "30s").
169    #[serde(default)]
170    pub interval: Option<String>,
171    /// Timeout for each check (e.g., "5s").
172    #[serde(default)]
173    pub timeout: Option<String>,
174    /// Number of retries before unhealthy.
175    #[serde(default)]
176    pub retries: Option<u32>,
177    /// Start period before health checks count (e.g., "10s").
178    #[serde(default)]
179    pub start_period: Option<String>,
180}
181
182/// Volume declaration.
183#[derive(Debug, Clone, Serialize, Deserialize, Default)]
184#[serde(deny_unknown_fields)]
185pub struct VolumeDeclaration {
186    /// Volume driver (default: "local").
187    #[serde(default)]
188    pub driver: Option<String>,
189}
190
191/// Network declaration.
192#[derive(Debug, Clone, Serialize, Deserialize, Default)]
193#[serde(deny_unknown_fields)]
194pub struct NetworkDeclaration {
195    /// Network driver (default: "bridge").
196    #[serde(default)]
197    pub driver: Option<String>,
198}
199
200/// A value that can be either a string or a list of strings.
201///
202/// Handles both `command: "echo hello"` and `command: ["echo", "hello"]`.
203#[derive(Debug, Clone, Serialize, Deserialize, Default)]
204#[serde(untagged)]
205pub enum StringOrList {
206    #[default]
207    Empty,
208    Single(String),
209    List(Vec<String>),
210}
211
212impl StringOrList {
213    /// Convert to a Vec<String>, splitting a single string on whitespace.
214    pub fn to_vec(&self) -> Vec<String> {
215        match self {
216            Self::Empty => vec![],
217            Self::Single(s) => s.split_whitespace().map(String::from).collect(),
218            Self::List(v) => v.clone(),
219        }
220    }
221
222    /// Returns true if empty.
223    pub fn is_empty(&self) -> bool {
224        match self {
225            Self::Empty => true,
226            Self::Single(s) => s.is_empty(),
227            Self::List(v) => v.is_empty(),
228        }
229    }
230}
231
232/// Environment variables: supports both map and list format.
233///
234/// Map: `environment: { KEY: value }`
235/// List: `environment: ["KEY=value"]`
236#[derive(Debug, Clone, Serialize, Deserialize, Default)]
237#[serde(untagged)]
238pub enum EnvVars {
239    #[default]
240    Empty,
241    Map(HashMap<String, String>),
242    List(Vec<String>),
243}
244
245impl EnvVars {
246    /// Convert to a list of (key, value) pairs.
247    pub fn to_pairs(&self) -> Vec<(String, String)> {
248        match self {
249            Self::Empty => vec![],
250            Self::Map(m) => {
251                let mut pairs = m
252                    .iter()
253                    .map(|(key, value)| (key.clone(), value.clone()))
254                    .collect::<Vec<_>>();
255                pairs.sort_by(|left, right| left.0.cmp(&right.0));
256                pairs
257            }
258            Self::List(list) => list
259                .iter()
260                .filter_map(|s| {
261                    let (k, v) = s.split_once('=')?;
262                    Some((k.to_string(), v.to_string()))
263                })
264                .collect(),
265        }
266    }
267}
268
269/// depends_on: supports both simple list and extended syntax.
270///
271/// Simple: `depends_on: [db, redis]`
272/// Extended: `depends_on: { db: { condition: service_healthy } }`
273#[derive(Debug, Clone, Serialize, Deserialize, Default)]
274#[serde(untagged)]
275pub enum DependsOn {
276    #[default]
277    Empty,
278    List(Vec<String>),
279    Map(HashMap<String, DependsOnCondition>),
280}
281
282impl DependsOn {
283    /// Get the list of dependency service names.
284    pub fn services(&self) -> Vec<String> {
285        match self {
286            Self::Empty => vec![],
287            Self::List(v) => v.clone(),
288            Self::Map(m) => {
289                let mut services = m.keys().cloned().collect::<Vec<_>>();
290                services.sort();
291                services
292            }
293        }
294    }
295}
296
297/// Condition for a depends_on entry.
298#[derive(Debug, Clone, Serialize, Deserialize)]
299#[serde(deny_unknown_fields)]
300pub struct DependsOnCondition {
301    /// Condition: "service_started" (default) or "service_healthy".
302    #[serde(default = "default_condition")]
303    pub condition: String,
304}
305
306fn default_condition() -> String {
307    "service_started".to_string()
308}
309
310/// Service networks: supports both list and map format.
311///
312/// List: `networks: [frontend, backend]`
313/// Map: `networks: { frontend: {} }`
314#[derive(Debug, Clone, Serialize, Deserialize, Default)]
315#[serde(untagged)]
316pub enum ServiceNetworks {
317    #[default]
318    Empty,
319    List(Vec<String>),
320    Map(HashMap<String, Option<ServiceNetworkConfig>>),
321}
322
323impl ServiceNetworks {
324    /// Get the list of network names.
325    pub fn names(&self) -> Vec<String> {
326        match self {
327            Self::Empty => vec![],
328            Self::List(v) => v.clone(),
329            Self::Map(m) => {
330                let mut names = m.keys().cloned().collect::<Vec<_>>();
331                names.sort();
332                names
333            }
334        }
335    }
336}
337
338/// Per-service network configuration.
339#[derive(Debug, Clone, Serialize, Deserialize, Default)]
340#[serde(deny_unknown_fields)]
341pub struct ServiceNetworkConfig {
342    /// Network aliases for this service.
343    #[serde(default)]
344    pub aliases: Vec<String>,
345}
346
347/// Labels: supports both map and list format.
348#[derive(Debug, Clone, Serialize, Deserialize, Default)]
349#[serde(untagged)]
350pub enum Labels {
351    #[default]
352    Empty,
353    Map(HashMap<String, String>),
354    List(Vec<String>),
355}
356
357impl Labels {
358    /// Convert labels to key/value pairs.
359    pub fn to_map(&self) -> HashMap<String, String> {
360        match self {
361            Self::Empty => HashMap::new(),
362            Self::Map(map) => map.clone(),
363            Self::List(list) => list
364                .iter()
365                .map(|entry| {
366                    let (key, value) = entry.split_once('=').unwrap_or((entry, ""));
367                    (key.to_string(), value.to_string())
368                })
369                .collect(),
370        }
371    }
372}
373
374/// DNS config: supports both single string and list.
375#[derive(Debug, Clone, Serialize, Deserialize, Default)]
376#[serde(untagged)]
377pub enum DnsConfig {
378    #[default]
379    Empty,
380    Single(String),
381    List(Vec<String>),
382}
383
384impl DnsConfig {
385    /// Convert to a list of DNS server addresses.
386    pub fn to_vec(&self) -> Vec<String> {
387        match self {
388            Self::Empty => vec![],
389            Self::Single(s) => vec![s.clone()],
390            Self::List(v) => v.clone(),
391        }
392    }
393}
394
395impl ComposeConfig {
396    /// Parse an A3S Compose ACL document using the process environment for
397    /// `env("NAME")` calls.
398    pub fn from_acl_str(source: &str) -> Result<Self, ComposeAclError> {
399        let environment = std::env::vars().collect();
400        Self::from_acl_str_with_environment(source, &environment)
401    }
402
403    /// Parse an A3S Compose ACL document using an explicit environment.
404    pub fn from_acl_str_with_environment(
405        source: &str,
406        environment: &HashMap<String, String>,
407    ) -> Result<Self, ComposeAclError> {
408        acl::parse_compose_acl(source, environment)
409    }
410
411    /// Parse a compose config from YAML bytes.
412    pub fn from_yaml(yaml: &[u8]) -> Result<Self, serde_yaml::Error> {
413        serde_yaml::from_slice(yaml)
414    }
415
416    /// Parse a compose config from a YAML string.
417    pub fn from_yaml_str(yaml: &str) -> Result<Self, serde_yaml::Error> {
418        serde_yaml::from_str(yaml)
419    }
420
421    /// Compute a topological ordering of services based on depends_on.
422    ///
423    /// Returns an error if there is a dependency cycle.
424    pub fn service_order(&self) -> Result<Vec<String>, String> {
425        let mut order = Vec::new();
426        // 0 = unvisited, 1 = in-progress, 2 = done
427        let mut state: HashMap<String, u8> = HashMap::new();
428
429        let mut service_names = self.services.keys().collect::<Vec<_>>();
430        service_names.sort();
431        for name in service_names {
432            if !state.contains_key(name) {
433                self.topo_visit(name, &mut state, &mut order)?;
434            }
435        }
436
437        Ok(order)
438    }
439
440    fn topo_visit(
441        &self,
442        name: &str,
443        state: &mut HashMap<String, u8>,
444        order: &mut Vec<String>,
445    ) -> Result<(), String> {
446        match state.get(name) {
447            Some(1) => {
448                return Err(format!(
449                    "Dependency cycle detected involving service '{}'",
450                    name
451                ));
452            }
453            Some(2) => return Ok(()), // already fully visited
454            _ => {}
455        }
456
457        state.insert(name.to_string(), 1); // in-progress
458
459        if let Some(svc) = self.services.get(name) {
460            let deps = svc.depends_on.services();
461            for dep in &deps {
462                if !self.services.contains_key(dep) {
463                    return Err(format!(
464                        "Service '{}' depends on '{}' which is not defined",
465                        name, dep
466                    ));
467                }
468                self.topo_visit(dep, state, order)?;
469            }
470        }
471
472        state.insert(name.to_string(), 2); // done
473        order.push(name.to_string());
474        Ok(())
475    }
476}
477
478#[cfg(test)]
479mod tests {
480    use super::*;
481
482    #[test]
483    fn test_parse_minimal_compose() {
484        let yaml = r#"
485services:
486  web:
487    image: nginx:latest
488"#;
489        let config = ComposeConfig::from_yaml_str(yaml).unwrap();
490        assert_eq!(config.services.len(), 1);
491        assert_eq!(
492            config.services["web"].image.as_deref(),
493            Some("nginx:latest")
494        );
495    }
496
497    #[test]
498    fn test_raw_yaml_parser_never_silently_ignores_unknown_fields() {
499        let error = ComposeConfig::from_yaml_str(
500            "services:\n  api:\n    image: api:latest\n    build: .\n",
501        )
502        .unwrap_err();
503
504        assert!(error.to_string().contains("unknown field `build`"));
505    }
506
507    #[test]
508    fn test_parse_full_compose() {
509        let yaml = r#"
510version: "3"
511services:
512  web:
513    image: nginx:latest
514    ports:
515      - "8080:80"
516    depends_on:
517      - db
518    environment:
519      APP_ENV: production
520    volumes:
521      - "static:/usr/share/nginx/html"
522  db:
523    image: postgres:16
524    environment:
525      - POSTGRES_PASSWORD=secret
526    volumes:
527      - "pgdata:/var/lib/postgresql/data"
528    mem_limit: "1g"
529    cpus: 2
530volumes:
531  pgdata:
532  static:
533networks:
534  default:
535"#;
536        let config = ComposeConfig::from_yaml_str(yaml).unwrap();
537        assert_eq!(config.services.len(), 2);
538        assert_eq!(config.volumes.len(), 2);
539        assert!(config.services["web"]
540            .depends_on
541            .services()
542            .contains(&"db".to_string()));
543        assert_eq!(config.services["web"].ports, vec!["8080:80"]);
544        assert_eq!(config.services["db"].cpus, Some(2));
545        assert_eq!(config.services["db"].mem_limit.as_deref(), Some("1g"));
546    }
547
548    #[test]
549    fn test_service_order_simple() {
550        let yaml = r#"
551services:
552  web:
553    image: nginx
554    depends_on: [api]
555  api:
556    image: myapi
557    depends_on: [db]
558  db:
559    image: postgres
560"#;
561        let config = ComposeConfig::from_yaml_str(yaml).unwrap();
562        let order = config.service_order().unwrap();
563        let db_pos = order.iter().position(|s| s == "db").unwrap();
564        let api_pos = order.iter().position(|s| s == "api").unwrap();
565        let web_pos = order.iter().position(|s| s == "web").unwrap();
566        assert!(db_pos < api_pos);
567        assert!(api_pos < web_pos);
568    }
569
570    #[test]
571    fn test_service_order_cycle_detected() {
572        let yaml = r#"
573services:
574  a:
575    image: img
576    depends_on: [b]
577  b:
578    image: img
579    depends_on: [a]
580"#;
581        let config = ComposeConfig::from_yaml_str(yaml).unwrap();
582        let result = config.service_order();
583        assert!(result.is_err());
584        assert!(result.unwrap_err().contains("cycle"));
585    }
586
587    #[test]
588    fn test_service_order_missing_dependency() {
589        let yaml = r#"
590services:
591  web:
592    image: nginx
593    depends_on: [nonexistent]
594"#;
595        let config = ComposeConfig::from_yaml_str(yaml).unwrap();
596        let result = config.service_order();
597        assert!(result.is_err());
598        assert!(result.unwrap_err().contains("not defined"));
599    }
600
601    #[test]
602    fn test_service_order_no_deps() {
603        let yaml = r#"
604services:
605  a:
606    image: img
607  b:
608    image: img
609  c:
610    image: img
611"#;
612        let config = ComposeConfig::from_yaml_str(yaml).unwrap();
613        let order = config.service_order().unwrap();
614        assert_eq!(order.len(), 3);
615    }
616
617    #[test]
618    fn test_env_vars_map() {
619        let yaml = r#"
620services:
621  web:
622    image: nginx
623    environment:
624      KEY1: val1
625      KEY2: val2
626"#;
627        let config = ComposeConfig::from_yaml_str(yaml).unwrap();
628        let pairs = config.services["web"].environment.to_pairs();
629        assert_eq!(pairs.len(), 2);
630    }
631
632    #[test]
633    fn test_env_vars_list() {
634        let yaml = r#"
635services:
636  web:
637    image: nginx
638    environment:
639      - KEY1=val1
640      - KEY2=val2
641"#;
642        let config = ComposeConfig::from_yaml_str(yaml).unwrap();
643        let pairs = config.services["web"].environment.to_pairs();
644        assert_eq!(pairs.len(), 2);
645        assert!(pairs.iter().any(|(k, v)| k == "KEY1" && v == "val1"));
646    }
647
648    #[test]
649    fn test_string_or_list_single() {
650        let sol = StringOrList::Single("echo hello world".to_string());
651        assert_eq!(sol.to_vec(), vec!["echo", "hello", "world"]);
652        assert!(!sol.is_empty());
653    }
654
655    #[test]
656    fn test_string_or_list_list() {
657        let sol = StringOrList::List(vec!["echo".into(), "hello world".into()]);
658        assert_eq!(sol.to_vec(), vec!["echo", "hello world"]);
659    }
660
661    #[test]
662    fn test_string_or_list_empty() {
663        let sol = StringOrList::Empty;
664        assert!(sol.is_empty());
665        assert!(sol.to_vec().is_empty());
666    }
667
668    #[test]
669    fn test_depends_on_list() {
670        let yaml = r#"
671services:
672  web:
673    image: nginx
674    depends_on:
675      - db
676      - redis
677"#;
678        let config = ComposeConfig::from_yaml_str(yaml).unwrap();
679        let deps = config.services["web"].depends_on.services();
680        assert_eq!(deps.len(), 2);
681        assert!(deps.contains(&"db".to_string()));
682        assert!(deps.contains(&"redis".to_string()));
683    }
684
685    #[test]
686    fn test_depends_on_map() {
687        let yaml = r#"
688services:
689  web:
690    image: nginx
691    depends_on:
692      db:
693        condition: service_healthy
694"#;
695        let config = ComposeConfig::from_yaml_str(yaml).unwrap();
696        let deps = config.services["web"].depends_on.services();
697        assert_eq!(deps, vec!["db"]);
698    }
699
700    #[test]
701    fn test_dns_config_single() {
702        let dns = DnsConfig::Single("8.8.8.8".to_string());
703        assert_eq!(dns.to_vec(), vec!["8.8.8.8"]);
704    }
705
706    #[test]
707    fn test_dns_config_list() {
708        let dns = DnsConfig::List(vec!["8.8.8.8".into(), "1.1.1.1".into()]);
709        assert_eq!(dns.to_vec().len(), 2);
710    }
711
712    #[test]
713    fn test_service_networks_list() {
714        let yaml = r#"
715services:
716  web:
717    image: nginx
718    networks:
719      - frontend
720      - backend
721"#;
722        let config = ComposeConfig::from_yaml_str(yaml).unwrap();
723        let nets = config.services["web"].networks.names();
724        assert_eq!(nets.len(), 2);
725    }
726
727    #[test]
728    fn test_healthcheck_config() {
729        let yaml = r#"
730services:
731  web:
732    image: nginx
733    healthcheck:
734      test: ["CMD", "curl", "-f", "http://localhost/"]
735      interval: "30s"
736      timeout: "5s"
737      retries: 3
738"#;
739        let config = ComposeConfig::from_yaml_str(yaml).unwrap();
740        let hc = config.services["web"].healthcheck.as_ref().unwrap();
741        assert_eq!(hc.retries, Some(3));
742        assert_eq!(hc.interval.as_deref(), Some("30s"));
743        assert!(!hc.disable);
744    }
745
746    #[test]
747    fn test_healthcheck_disable() {
748        let yaml = r#"
749services:
750  web:
751    image: nginx
752    healthcheck:
753      disable: true
754"#;
755        let config = ComposeConfig::from_yaml_str(yaml).unwrap();
756        let hc = config.services["web"].healthcheck.as_ref().unwrap();
757        assert!(hc.disable);
758        assert!(hc.test.is_empty());
759    }
760
761    #[test]
762    fn test_compose_serde_roundtrip() {
763        let yaml = r#"
764version: "3"
765services:
766  web:
767    image: nginx:latest
768    ports:
769      - "8080:80"
770"#;
771        let config = ComposeConfig::from_yaml_str(yaml).unwrap();
772        let serialized = serde_yaml::to_string(&config).unwrap();
773        let reparsed = ComposeConfig::from_yaml_str(&serialized).unwrap();
774        assert_eq!(reparsed.services.len(), 1);
775        assert_eq!(
776            reparsed.services["web"].image.as_deref(),
777            Some("nginx:latest")
778        );
779    }
780
781    #[test]
782    fn test_labels_map() {
783        let yaml = r#"
784services:
785  web:
786    image: nginx
787    labels:
788      com.example.env: production
789"#;
790        let config = ComposeConfig::from_yaml_str(yaml).unwrap();
791        assert!(matches!(config.services["web"].labels, Labels::Map(_)));
792        assert_eq!(
793            config.services["web"]
794                .labels
795                .to_map()
796                .get("com.example.env")
797                .map(String::as_str),
798            Some("production")
799        );
800    }
801
802    #[test]
803    fn test_labels_list() {
804        let yaml = r#"
805services:
806  web:
807    image: nginx
808    labels:
809      - "com.example.env=production"
810      - "com.example.debug=true"
811      - "com.example.flag"
812"#;
813        let config = ComposeConfig::from_yaml_str(yaml).unwrap();
814        let labels = config.services["web"].labels.to_map();
815        assert_eq!(
816            labels.get("com.example.env").map(String::as_str),
817            Some("production")
818        );
819        assert_eq!(
820            labels.get("com.example.debug").map(String::as_str),
821            Some("true")
822        );
823        assert_eq!(labels.get("com.example.flag").map(String::as_str), Some(""));
824    }
825
826    #[test]
827    fn test_service_config_defaults() {
828        let svc = ServiceConfig::default();
829        assert!(svc.image.is_none());
830        assert!(svc.ports.is_empty());
831        assert!(svc.volumes.is_empty());
832        assert!(!svc.privileged);
833    }
834}