Revert "[cm_fidl_analyzer][scrutiny] Replace NodePath with AbsoluteMoniker"

This reverts commit a2bc51fd452d0cf85b3f770b7ae09c82e256737d.

Reason for revert: Disabled verification of routes at session scope, or
deeper.

Original change's description:
> [cm_fidl_analyzer][scrutiny] Replace NodePath with AbsoluteMoniker
>
> The `AbsoluteMoniker` type is now a 1:1 replacement for
> cm_fidl_analyzer's `NodePath` type (which was originally
> a workaround to avoid handling instance IDs).
>
> Bug: 102801
>
> Change-Id: I41406a0be450849c5093537609ef4eecbfd41e0a
> Reviewed-on: https://fuchsia-review.googlesource.com/c/fuchsia/+/688902
> Commit-Queue: Laura Peskin <pesk@google.com>
> Reviewed-by: Yaneury Fermin <yaneury@google.com>

Bug: 102801, 103259
Change-Id: I82adaa52266fb1d8ad1f51aa64db4c78e11e0f5a
Reviewed-on: https://fuchsia-review.googlesource.com/c/fuchsia/+/693704
Reviewed-by: Wez <wez@google.com>
Commit-Queue: Wez <wez@google.com>
Reviewed-by: RubberStamper 🤖 <android-build-ayeaye@system.gserviceaccount.com>
diff --git a/src/developer/ffx/plugins/scrutiny/verify/src/component_resolvers.rs b/src/developer/ffx/plugins/scrutiny/verify/src/component_resolvers.rs
index 7e99b35..a709a7b 100644
--- a/src/developer/ffx/plugins/scrutiny/verify/src/component_resolvers.rs
+++ b/src/developer/ffx/plugins/scrutiny/verify/src/component_resolvers.rs
@@ -11,33 +11,33 @@
     std::{collections::HashSet, fs, path::PathBuf},
 };
 
-type Moniker = String;
+type NodePath = String;
 
 #[derive(Debug, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord, Clone)]
 struct ComponentResolversRequest {
     scheme: String,
-    moniker: Moniker,
+    moniker: NodePath,
     protocol: String,
 }
 
 #[derive(Debug, Serialize, Deserialize, PartialEq, Eq, Clone)]
 struct ComponentResolversResponse {
     deps: HashSet<PathBuf>,
-    monikers: Vec<Moniker>,
+    monikers: Vec<NodePath>,
 }
 
 #[derive(Debug, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord)]
 struct AllowListEntry {
     #[serde(flatten)]
     query: ComponentResolversRequest,
-    components: Vec<Moniker>,
+    components: Vec<NodePath>,
 }
 
 #[derive(Debug, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord)]
 struct AllowList(Vec<AllowListEntry>);
 
 impl AllowList {
-    pub fn iter(&self) -> impl Iterator<Item = (ComponentResolversRequest, &[Moniker])> {
+    pub fn iter(&self) -> impl Iterator<Item = (ComponentResolversRequest, &[NodePath])> {
         self.0.iter().map(|entry| (entry.query.clone(), entry.components.as_slice()))
     }
 }
@@ -49,7 +49,7 @@
     fn query(
         &self,
         scheme: String,
-        moniker: Moniker,
+        moniker: NodePath,
         protocol: String,
     ) -> Result<ComponentResolversResponse>;
 }
@@ -67,7 +67,7 @@
     fn query(
         &self,
         scheme: String,
-        moniker: Moniker,
+        moniker: NodePath,
         protocol: String,
     ) -> Result<ComponentResolversResponse> {
         let request = ComponentResolversRequest { scheme, moniker, protocol };
@@ -110,7 +110,7 @@
     let mut deps = HashSet::new();
 
     for (query, allowed_monikers) in allowlist.iter() {
-        let allowed_monikers: HashSet<&Moniker> = allowed_monikers.into_iter().collect();
+        let allowed_monikers: HashSet<&NodePath> = allowed_monikers.into_iter().collect();
 
         let response = scrutiny
             .query(query.scheme.clone(), query.moniker.clone(), query.protocol.clone())
@@ -176,7 +176,7 @@
 
     #[derive(Debug)]
     struct MockQueryComponentResolvers {
-        responses: HashMap<(String, Moniker, String), String>,
+        responses: HashMap<(String, NodePath, String), String>,
     }
 
     impl MockQueryComponentResolvers {
@@ -186,8 +186,8 @@
 
         fn with_response(
             self,
-            query: (String, Moniker, String),
-            response: Vec<Moniker>,
+            query: (String, NodePath, String),
+            response: Vec<NodePath>,
             response_deps: Vec<String>,
         ) -> Self {
             let raw_response = serde_json::to_string(&ComponentResolversResponse {
@@ -198,7 +198,11 @@
             self.with_raw_response(query, raw_response)
         }
 
-        fn with_raw_response(mut self, query: (String, Moniker, String), response: String) -> Self {
+        fn with_raw_response(
+            mut self,
+            query: (String, NodePath, String),
+            response: String,
+        ) -> Self {
             self.responses.insert(query, response);
             self
         }
@@ -208,7 +212,7 @@
         fn query(
             &self,
             scheme: String,
-            moniker: Moniker,
+            moniker: NodePath,
             protocol: String,
         ) -> Result<ComponentResolversResponse> {
             let key = (scheme, moniker, protocol);
diff --git a/src/security/scrutiny/plugins/src/verify/collector/component_model.rs b/src/security/scrutiny/plugins/src/verify/collector/component_model.rs
index 6fc386d..0accdf2 100644
--- a/src/security/scrutiny/plugins/src/verify/collector/component_model.rs
+++ b/src/security/scrutiny/plugins/src/verify/collector/component_model.rs
@@ -11,13 +11,12 @@
         verify::collection::V2ComponentModel,
     },
     anyhow::{anyhow, Context, Result},
-    cm_fidl_analyzer::component_model::ModelBuilderForAnalyzer,
+    cm_fidl_analyzer::{component_model::ModelBuilderForAnalyzer, node_path::NodePath},
     cm_rust::{ComponentDecl, FidlIntoNative, RegistrationSource, RunnerRegistration},
     fidl::encoding::decode_persistent,
     fidl_fuchsia_component_decl as fdecl, fidl_fuchsia_component_internal as component_internal,
     fuchsia_url::{boot_url::BootUrl, AbsoluteComponentUrl},
     log::{error, info, warn},
-    moniker::AbsoluteMoniker,
     once_cell::sync::Lazy,
     routing::{
         component_id_index::ComponentIdIndex, config::RuntimeConfig, environment::RunnerRegistry,
@@ -54,7 +53,7 @@
 
 #[derive(Deserialize, Serialize)]
 pub struct ComponentTreeConfig {
-    pub dynamic_components: HashMap<AbsoluteMoniker, DynamicComponent>,
+    pub dynamic_components: HashMap<NodePath, DynamicComponent>,
 }
 
 pub struct V2ComponentModelDataCollector {}
@@ -178,7 +177,7 @@
 
     fn load_dynamic_components(
         component_tree_config_path: &Option<PathBuf>,
-    ) -> Result<HashMap<AbsoluteMoniker, (AbsoluteComponentUrl, Option<String>)>> {
+    ) -> Result<HashMap<NodePath, (AbsoluteComponentUrl, Option<String>)>> {
         if component_tree_config_path.is_none() {
             return Ok(HashMap::new());
         }
@@ -191,10 +190,9 @@
                 .context("Failed to parse component tree configuration file")?;
 
         let mut dynamic_components = HashMap::new();
-        for (abs_moniker, dynamic_component) in component_tree_config.dynamic_components.into_iter()
-        {
+        for (node_path, dynamic_component) in component_tree_config.dynamic_components.into_iter() {
             dynamic_components
-                .insert(abs_moniker, (dynamic_component.url, dynamic_component.environment));
+                .insert(node_path, (dynamic_component.url, dynamic_component.environment));
         }
         Ok(dynamic_components)
     }
diff --git a/src/security/scrutiny/plugins/src/verify/controller/component_resolvers.rs b/src/security/scrutiny/plugins/src/verify/controller/component_resolvers.rs
index 9ea725b..1bb13aa 100644
--- a/src/security/scrutiny/plugins/src/verify/controller/component_resolvers.rs
+++ b/src/security/scrutiny/plugins/src/verify/controller/component_resolvers.rs
@@ -24,7 +24,7 @@
 
 /// ComponentResolversController
 ///
-/// A DataController which returns a list of absolute monikers of all
+/// A DataController which returns a list component node paths of all
 /// components that, in their environment, contain a resolver with the
 ///  given moniker for a scheme with access to a protocol.
 #[derive(Default)]
@@ -35,7 +35,7 @@
 pub struct ComponentResolverRequest {
     /// `resolver` URI scheme of interest
     pub scheme: String,
-    /// Absolute moniker of the `resolver`
+    /// Node path of the `resolver`
     pub moniker: String,
     /// Filter the results to components resolved with a `resolver` with access to a protocol
     pub protocol: String,
@@ -50,10 +50,10 @@
     pub monikers: Vec<String>,
 }
 
-/// Walks the tree for the absolute monikers of all components that,
-/// in their environment, contain a resolver with the given moniker
-/// for a scheme with access to a protocol.  `monikers` contains the
-/// components which match the `request` parameters.
+/// Walks the tree for component node paths of all components that, in their
+/// environment, contain a resolver with the given moniker for a scheme
+/// with access to a protocol.
+/// `monikers` contains the components which match the `request` parameters.
 struct ComponentResolversVisitor {
     request: ComponentResolverRequest,
     monikers: Vec<String>,
@@ -156,13 +156,13 @@
     }
 
     fn usage(&self) -> String {
-        "Finds the absolute monikers of all components that, in their environment,
-         contain a resolver for `scheme`, provided by a component with the given `moniker`
-         and with access to `protocol`.
+        "Finds the component node paths of all components that, in their
+environment, contain a resolver with the given moniker for scheme with
+access to protocol.
 
 Required parameters:
 --scheme:  the resolver URI scheme to query
---moniker: the absolute moniker of the component providing the resolver capability
+--moniker: the node path of the resolver
 --resolver: filter results to components resolved with a resolver that has access to the given protocol"
             .to_string()
     }
diff --git a/src/security/scrutiny/plugins/src/verify/controller/route_sources.rs b/src/security/scrutiny/plugins/src/verify/controller/route_sources.rs
index 28ef6df..c3e43f2 100644
--- a/src/security/scrutiny/plugins/src/verify/controller/route_sources.rs
+++ b/src/security/scrutiny/plugins/src/verify/controller/route_sources.rs
@@ -10,13 +10,13 @@
     anyhow::{anyhow, Context, Error, Result},
     cm_fidl_analyzer::{
         component_model::ComponentModelForAnalyzer,
+        node_path::NodePath,
         route::{CapabilityRouteError, RouteSegment, VerifyRouteResult},
     },
     cm_rust::{
         CapabilityDecl, CapabilityName, CapabilityPath, CapabilityTypeName, ComponentDecl,
         ExposeDecl, OfferDecl, UseDecl,
     },
-    moniker::AbsoluteMoniker,
     routing::component_instance::ComponentInstanceInterface,
     scrutiny::model::{controller::DataController, model::DataModel},
     serde::{Deserialize, Serialize},
@@ -60,9 +60,8 @@
 /// Each route must be listed, either to be verified or skipped by the verifier.
 #[derive(Deserialize, Serialize)]
 pub struct RouteSourcesSpec {
-    /// TODO(fxbug.dev/102801): rename to `target_moniker`.
-    /// `AbsoluteMoniker` of the component instance whose routes are to be verified.
-    pub target_node_path: AbsoluteMoniker,
+    /// Absolute path to the component instance whose routes are to be verified.
+    pub target_node_path: NodePath,
     /// Routes that are expected to be present, but do not require verification.
     pub routes_to_skip: Vec<UseSpec>,
     /// Route specification and route source matching information for routes
@@ -114,9 +113,9 @@
 /// Input query type for matching a route source.
 #[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
 pub struct SourceSpec {
-    /// `AbsoluteMoniker` prefix expected at the source instance.
-    #[serde(rename = "source_moniker")]
-    abs_moniker: AbsoluteMoniker,
+    /// Node path prefix expected at the source instance.
+    #[serde(rename = "source_node_path")]
+    node_path: NodePath,
     /// Capability declaration expected at the source instance.
     #[serde(flatten)]
     capability: SourceDeclSpec,
@@ -276,8 +275,8 @@
 /// Output type: The source of a capability route.
 #[derive(Debug, Deserialize, PartialEq, Serialize)]
 pub struct Source {
-    /// The `AbsoluteMoniker` of the declaring component instance.
-    abs_moniker: AbsoluteMoniker,
+    /// The node path of the declaring component instance.
+    node_path: NodePath,
     /// The capability declaration.
     capability: CapabilityDecl,
 }
@@ -288,7 +287,7 @@
 pub enum RouteSourceError {
     CapabilityRouteError(CapabilityRouteError),
     RouteSegmentWithoutComponent(RouteSegment),
-    RouteSegmentAbsMonikerNotFoundInTree(RouteSegment),
+    RouteSegmentNodePathNotFoundInTree(RouteSegment),
     ComponentInstanceLookupByUrlFailed(String),
     MultipleComponentsWithSameUrl(Vec<Component>),
     RouteSegmentComponentFromUntrustedSource(RouteSegment, ComponentSource),
@@ -370,7 +369,7 @@
 fn gather_routes<'a>(
     component_routes: &'a RouteSourcesSpec,
     component_decl: &'a ComponentDecl,
-    abs_moniker: &'a AbsoluteMoniker,
+    node_path: &'a NodePath,
 ) -> Result<(Vec<&'a UseDecl>, Vec<Binding<'a>>)> {
     let uses = &component_decl.uses;
     let routes_to_skip = component_routes
@@ -416,26 +415,26 @@
             uses.iter().filter(|use_decl| !deduped_matches.contains(use_decl)).collect();
         if duped_matches.len() > 0 {
             Err(anyhow!(
-                "{}. {}; moniker: {} routes matched multiple times: {:#?}; unmatched routes: {:#?}",
+                "{}. {}; component node path: {} routes matched multiple times: {:#?}; unmatched routes: {:#?}",
                 ROUTE_LISTS_OVERLAP,
                 ROUTE_LISTS_INCOMPLETE,
-                abs_moniker,
+                node_path,
                 duped_matches,
                 missed_routes
             ))
         } else {
             Err(anyhow!(
-                "{}; moniker: {}; unmatched routes: {:#?}",
+                "{}; component node path: {}; unmatched routes: {:#?}",
                 ROUTE_LISTS_INCOMPLETE,
-                abs_moniker,
+                node_path,
                 missed_routes
             ))
         }
     } else if duped_matches.len() > 0 {
         Err(anyhow!(
-            "{}; moniker: {}; routes matched multiple times: {:#?}",
+            "{}; component node path: {}; routes matched multiple times: {:#?}",
             ROUTE_LISTS_OVERLAP,
-            abs_moniker,
+            node_path,
             duped_matches
         ))
     } else {
@@ -448,15 +447,15 @@
     component_model: &Arc<ComponentModelForAnalyzer>,
     components: &Vec<Component>,
 ) -> Option<RouteSourceError> {
-    let abs_moniker = route_segment.abs_moniker();
-    if abs_moniker.is_none() {
+    let node_path = route_segment.node_path();
+    if node_path.is_none() {
         return Some(RouteSourceError::RouteSegmentWithoutComponent(route_segment.clone()));
     }
-    let abs_moniker = abs_moniker.unwrap();
+    let node_path = node_path.unwrap();
 
-    let get_instance_result = component_model.get_instance(abs_moniker);
+    let get_instance_result = component_model.get_instance(node_path);
     if get_instance_result.is_err() {
-        return Some(RouteSourceError::RouteSegmentAbsMonikerNotFoundInTree(route_segment.clone()));
+        return Some(RouteSourceError::RouteSegmentNodePathNotFoundInTree(route_segment.clone()));
     }
     let instance = get_instance_result.unwrap();
     let instance_url_str = instance.url();
@@ -507,9 +506,9 @@
                     json_or_unformatted(&route.route_match.target, "route target")
                 )
             })?;
-            if let RouteSegment::DeclareBy { abs_moniker, capability } = route_source {
+            if let RouteSegment::DeclareBy { node_path, capability } = route_source {
                 let source =
-                    Source { abs_moniker: abs_moniker.clone(), capability: capability.clone() };
+                    Source { node_path: node_path.clone(), capability: capability.clone() };
                 let matches_result: Result<Vec<bool>> = vec![
                     route.route_match.source.capability.matches(capability),
                     route.route_match.source.capability.matches(&route_details),
@@ -548,20 +547,24 @@
     ) -> Result<HashMap<String, Vec<VerifyRouteSourcesResult>>> {
         let mut results = HashMap::new();
         for component_routes in config.component_routes.iter() {
-            let target_moniker = &component_routes.target_node_path;
-            let target_instance = component_model.get_instance(target_moniker).context(format!(
-                "{}; target instance: {}",
-                MISSING_TARGET_INSTANCE,
-                target_moniker.clone()
-            ))?;
+            let target_node_path = &component_routes.target_node_path;
+            let target_instance =
+                component_model.get_instance(target_node_path).context(format!(
+                    "{}; target instance: {}",
+                    MISSING_TARGET_INSTANCE,
+                    target_node_path.clone()
+                ))?;
 
-            let (_, routes_to_verify) =
-                gather_routes(component_routes, target_instance.decl_for_testing(), target_moniker)
-                    .context(format!(
-                        "{}; target instance: {}",
-                        GATHER_FAILED,
-                        target_moniker.clone()
-                    ))?;
+            let (_, routes_to_verify) = gather_routes(
+                component_routes,
+                target_instance.decl_for_testing(),
+                target_node_path,
+            )
+            .context(format!(
+                "{}; target instance: {}",
+                GATHER_FAILED,
+                target_node_path.clone()
+            ))?;
 
             let mut component_results = Vec::new();
             for route in routes_to_verify.into_iter() {
@@ -578,7 +581,7 @@
                 }
             }
 
-            results.insert(target_moniker.to_string(), component_results);
+            results.insert(format!("/{}", target_node_path.as_vec().join("/")), component_results);
         }
         Ok(results)
     }
@@ -617,7 +620,9 @@
             verify::{collection::V2ComponentModel, collector::component_model::DEFAULT_ROOT_URL},
         },
         anyhow::Result,
-        cm_fidl_analyzer::{component_model::ModelBuilderForAnalyzer, route::RouteSegment},
+        cm_fidl_analyzer::{
+            component_model::ModelBuilderForAnalyzer, node_path::NodePath, route::RouteSegment,
+        },
         cm_rust::{
             Availability, CapabilityName, CapabilityPath, CapabilityTypeName, ChildDecl,
             ComponentDecl, DependencyType, DirectoryDecl, ExposeDirectoryDecl, ExposeSource,
@@ -627,7 +632,6 @@
         fidl_fuchsia_component_decl as fdecl, fidl_fuchsia_io as fio,
         fuchsia_merkle::{Hash, HASH_SIZE},
         maplit::{hashmap, hashset},
-        moniker::{AbsoluteMoniker, AbsoluteMonikerBase},
         routing::{
             component_id_index::ComponentIdIndex, config::RuntimeConfig,
             environment::RunnerRegistry,
@@ -1009,7 +1013,7 @@
         // Vacuous request: Confirms that @root_url uses no input capabilities.
         let config = RouteSourcesConfig {
             component_routes: vec![RouteSourcesSpec {
-                target_node_path: AbsoluteMoniker::root(),
+                target_node_path: NodePath::absolute_from_vec(vec![]),
                 routes_to_skip: vec![],
                 routes_to_verify: vec![],
             }],
@@ -1030,7 +1034,7 @@
         // Request checking routes of a component instance that does not exist.
         let config = RouteSourcesConfig {
             component_routes: vec![RouteSourcesSpec {
-                target_node_path: AbsoluteMoniker::parse_str("/does:0/not:1/exist:2").unwrap(),
+                target_node_path: NodePath::absolute_from_vec(vec!["does:0", "not:1", "exist:2"]),
                 routes_to_skip: vec![],
                 routes_to_verify: vec![],
             }],
@@ -1056,7 +1060,7 @@
         // are listed in `routes_to_skip` + `routes_to_verify`.
         let config = RouteSourcesConfig {
             component_routes: vec![RouteSourcesSpec {
-                target_node_path: AbsoluteMoniker::parse_str("/two_dir_user").unwrap(),
+                target_node_path: NodePath::absolute_from_vec(vec!["two_dir_user"]),
                 routes_to_skip: vec![],
                 routes_to_verify: vec![],
             }],
@@ -1079,7 +1083,7 @@
         // @two_dir_user_url.
         let config = RouteSourcesConfig {
             component_routes: vec![RouteSourcesSpec {
-                target_node_path: AbsoluteMoniker::parse_str("/two_dir_user").unwrap(),
+                target_node_path: NodePath::absolute_from_vec(vec!["two_dir_user"]),
                 routes_to_skip: vec![
                     UseSpec {
                         type_name: CapabilityTypeName::Directory,
@@ -1121,7 +1125,7 @@
         // the corresponding component manifest.
         let config = RouteSourcesConfig {
             component_routes: vec![RouteSourcesSpec {
-                target_node_path: AbsoluteMoniker::parse_str("/two_dir_user").unwrap(),
+                target_node_path: NodePath::absolute_from_vec(vec!["two_dir_user"]),
                 routes_to_skip: vec![
                     UseSpec {
                         type_name: CapabilityTypeName::Directory,
@@ -1163,7 +1167,7 @@
         let components = &data_model.get::<Components>()?.entries;
         let config = RouteSourcesConfig {
             component_routes: vec![RouteSourcesSpec {
-                target_node_path: AbsoluteMoniker::parse_str("/two_dir_user").unwrap(),
+                target_node_path: NodePath::absolute_from_vec(vec!["two_dir_user"]),
                 routes_to_skip: vec![
                     // Skip @root_url -> @two_dir_user_url route.
                     UseSpec {
@@ -1185,7 +1189,7 @@
                             name: None,
                         },
                         source: SourceSpec {
-                            abs_moniker: AbsoluteMoniker::parse_str("/one_dir_provider").unwrap(),
+                            node_path: NodePath::absolute_from_vec(vec!["one_dir_provider"]),
                             capability: SourceDeclSpec {
                                 // Match complete path with routed subdirs.
                                 path_prefix: Some(
@@ -1210,7 +1214,7 @@
                     VerifyRouteSourcesResult{
                         query: config.component_routes[0].routes_to_verify[0].clone(),
                         result: Ok(Source {
-                            abs_moniker: config.component_routes[0].routes_to_verify[0].source.abs_moniker.clone(),
+                            node_path: config.component_routes[0].routes_to_verify[0].source.node_path.clone(),
                             capability: DirectoryDecl{
                                 name: CapabilityName("provider_dir".to_string()),
                                 source_path: Some(CapabilityPath::from_str("/data/to/user").unwrap()),
@@ -1235,7 +1239,7 @@
         let components = &data_model.get::<Components>()?.entries;
         let config = RouteSourcesConfig {
             component_routes: vec![RouteSourcesSpec {
-                target_node_path: AbsoluteMoniker::parse_str("/two_dir_user").unwrap(),
+                target_node_path: NodePath::absolute_from_vec(vec!["two_dir_user"]),
                 routes_to_skip: vec![
                     // Skip @root_url -> @two_dir_user_url route.
                     UseSpec {
@@ -1257,7 +1261,7 @@
                             name: None,
                         },
                         source: SourceSpec {
-                            abs_moniker: AbsoluteMoniker::parse_str("/one_dir_provider").unwrap(),
+                            node_path: NodePath::absolute_from_vec(vec!["one_dir_provider"]),
                             capability: SourceDeclSpec {
                                 // Match partial path with some (not all) routed
                                 // subdirs.
@@ -1282,7 +1286,7 @@
                         VerifyRouteSourcesResult{
                             query: config.component_routes[0].routes_to_verify[0].clone(),
                             result: Ok(Source {
-                                abs_moniker: config.component_routes[0].routes_to_verify[0].source.abs_moniker.clone(),
+                                node_path: config.component_routes[0].routes_to_verify[0].source.node_path.clone(),
                                 capability: DirectoryDecl{
                                     name: CapabilityName("provider_dir".to_string()),
                                     source_path: Some(CapabilityPath::from_str("/data/to/user").unwrap()),
@@ -1307,7 +1311,7 @@
         let components = &data_model.get::<Components>()?.entries;
         let config = RouteSourcesConfig {
             component_routes: vec![RouteSourcesSpec {
-                target_node_path: AbsoluteMoniker::parse_str("/two_dir_user").unwrap(),
+                target_node_path: NodePath::absolute_from_vec(vec!["two_dir_user"]),
                 routes_to_skip: vec![],
                 routes_to_verify: vec![
                     // config.component_routes[0].routes_to_verify[0]:
@@ -1319,7 +1323,7 @@
                             name: None,
                         },
                         source: SourceSpec {
-                            abs_moniker: AbsoluteMoniker::root(),
+                            node_path: NodePath::absolute_from_vec(vec![]),
                             capability: SourceDeclSpec {
                                 // Match complete path with routed subdirs.
                                 path_prefix: Some(
@@ -1344,7 +1348,7 @@
                             name: None,
                         },
                         source: SourceSpec {
-                            abs_moniker: AbsoluteMoniker::parse_str("/one_dir_provider").unwrap(),
+                            node_path: NodePath::absolute_from_vec(vec!["one_dir_provider"]),
                             capability: SourceDeclSpec {
                                 // Match complete path with routed subdirs.
                                 path_prefix: Some(
@@ -1368,7 +1372,7 @@
                         VerifyRouteSourcesResult{
                             query: config.component_routes[0].routes_to_verify[0].clone(),
                             result: Ok(Source {
-                                abs_moniker: config.component_routes[0].routes_to_verify[0].source.abs_moniker.clone(),
+                                node_path: config.component_routes[0].routes_to_verify[0].source.node_path.clone(),
                                 capability: DirectoryDecl{
                                     name: CapabilityName("root_dir".to_string()),
                                     source_path: Some(CapabilityPath::from_str("/data/to/user").unwrap()),
@@ -1379,7 +1383,7 @@
                         VerifyRouteSourcesResult{
                             query: config.component_routes[0].routes_to_verify[1].clone(),
                             result: Ok(Source {
-                                abs_moniker: config.component_routes[0].routes_to_verify[1].source.abs_moniker.clone(),
+                                node_path: config.component_routes[0].routes_to_verify[1].source.node_path.clone(),
                                 capability: DirectoryDecl{
                                     name: CapabilityName("provider_dir".to_string()),
                                     source_path: Some(CapabilityPath::from_str("/data/to/user").unwrap()),
@@ -1406,13 +1410,13 @@
             component_routes: vec![
                 // Match empty set of routes used by @root_url.
                 RouteSourcesSpec {
-                    target_node_path: AbsoluteMoniker::root(),
+                    target_node_path: NodePath::absolute_from_vec(vec![]),
                     routes_to_skip: vec![],
                     routes_to_verify: vec![],
                 },
                 // Match all routes used by @two_dir_user_url.
                 RouteSourcesSpec {
-                    target_node_path: AbsoluteMoniker::parse_str("/two_dir_user").unwrap(),
+                    target_node_path: NodePath::absolute_from_vec(vec!["two_dir_user"]),
                     routes_to_skip: vec![],
                     routes_to_verify: vec![
                         // config.component_routes[1].routes_to_verify[0]:
@@ -1424,7 +1428,7 @@
                                 name: None,
                             },
                             source: SourceSpec {
-                                abs_moniker: AbsoluteMoniker::root(),
+                                node_path: NodePath::absolute_from_vec(vec![]),
                                 capability: SourceDeclSpec {
                                     path_prefix: Some(
                                         CapabilityPath::from_str(
@@ -1450,8 +1454,7 @@
                                 name: None,
                             },
                             source: SourceSpec {
-                                abs_moniker: AbsoluteMoniker::parse_str("/one_dir_provider")
-                                    .unwrap(),
+                                node_path: NodePath::absolute_from_vec(vec!["one_dir_provider"]),
                                 capability: SourceDeclSpec {
                                     path_prefix: Some(
                                         CapabilityPath::from_str(
@@ -1476,7 +1479,7 @@
                         VerifyRouteSourcesResult{
                             query: config.component_routes[1].routes_to_verify[0].clone(),
                             result: Ok(Source {
-                                abs_moniker: config.component_routes[1].routes_to_verify[0].source.abs_moniker.clone(),
+                                node_path: config.component_routes[1].routes_to_verify[0].source.node_path.clone(),
                                 capability: DirectoryDecl{
                                     name: CapabilityName("root_dir".to_string()),
                                     source_path: Some(CapabilityPath::from_str("/data/to/user").unwrap()),
@@ -1487,7 +1490,7 @@
                         VerifyRouteSourcesResult{
                             query: config.component_routes[1].routes_to_verify[1].clone(),
                             result: Ok(Source {
-                                abs_moniker: config.component_routes[1].routes_to_verify[1].source.abs_moniker.clone(),
+                                node_path: config.component_routes[1].routes_to_verify[1].source.node_path.clone(),
                                 capability: DirectoryDecl{
                                     name: CapabilityName("provider_dir".to_string()),
                                     source_path: Some(CapabilityPath::from_str("/data/to/user").unwrap()),
@@ -1512,7 +1515,7 @@
         let source_name = "routed_from_provider";
         let config = RouteSourcesConfig {
             component_routes: vec![RouteSourcesSpec {
-                target_node_path: AbsoluteMoniker::parse_str("/two_dir_user").unwrap(),
+                target_node_path: NodePath::absolute_from_vec(vec!["two_dir_user"]),
                 routes_to_skip: vec![
                     UseSpec {
                         type_name: CapabilityTypeName::Directory,
@@ -1561,7 +1564,7 @@
         let source_name = "routed_from_provider";
         let config = RouteSourcesConfig {
             component_routes: vec![RouteSourcesSpec {
-                target_node_path: AbsoluteMoniker::parse_str("/two_dir_user").unwrap(),
+                target_node_path: NodePath::absolute_from_vec(vec!["two_dir_user"]),
                 routes_to_skip: vec![
                     UseSpec {
                         type_name: CapabilityTypeName::Directory,
@@ -1611,7 +1614,7 @@
         let bad_path = format!("{}/{}", bad_dirname, bad_basename);
         let config = RouteSourcesConfig {
             component_routes: vec![RouteSourcesSpec {
-                target_node_path: AbsoluteMoniker::parse_str("/two_dir_user").unwrap(),
+                target_node_path: NodePath::absolute_from_vec(vec!["two_dir_user"]),
                 routes_to_skip: vec![
                     UseSpec {
                         type_name: CapabilityTypeName::Directory,
@@ -1653,7 +1656,7 @@
         let dup_name = "/data/from/root";
         let config = RouteSourcesConfig {
             component_routes: vec![RouteSourcesSpec {
-                target_node_path: AbsoluteMoniker::parse_str("/two_dir_user").unwrap(),
+                target_node_path: NodePath::absolute_from_vec(vec!["two_dir_user"]),
                 routes_to_skip: vec![
                     UseSpec {
                         type_name: CapabilityTypeName::Directory,
@@ -1693,7 +1696,7 @@
         let components = &data_model.get::<Components>()?.entries;
         let config = RouteSourcesConfig {
             component_routes: vec![RouteSourcesSpec {
-                target_node_path: AbsoluteMoniker::parse_str("/two_dir_user").unwrap(),
+                target_node_path: NodePath::absolute_from_vec(vec!["two_dir_user"]),
                 routes_to_skip: vec![UseSpec {
                     type_name: CapabilityTypeName::Directory,
                     path: Some(CapabilityPath::from_str("/data/from/root").unwrap()),
@@ -1707,7 +1710,7 @@
                             name: None,
                         },
                         source: SourceSpec {
-                            abs_moniker: AbsoluteMoniker::parse_str("/one_dir_provider").unwrap(),
+                            node_path: NodePath::absolute_from_vec(vec!["one_dir_provider"]),
                             capability: SourceDeclSpec {
                                 path_prefix: Some(
                                     CapabilityPath::from_str(
@@ -1728,7 +1731,7 @@
                             name: None,
                         },
                         source: SourceSpec {
-                            abs_moniker: AbsoluteMoniker::parse_str("/one_dir_provider").unwrap(),
+                            node_path: NodePath::absolute_from_vec(vec!["one_dir_provider"]),
                             capability: SourceDeclSpec {
                                 path_prefix: Some(
                                     CapabilityPath::from_str("/data/to/user").unwrap(),
@@ -1757,7 +1760,7 @@
         let components = &data_model.get::<Components>()?.entries;
         let config = RouteSourcesConfig {
             component_routes: vec![RouteSourcesSpec {
-                target_node_path: AbsoluteMoniker::parse_str("/two_dir_user").unwrap(),
+                target_node_path: NodePath::absolute_from_vec(vec!["two_dir_user"]),
                 routes_to_skip: vec![
                     UseSpec {
                         type_name: CapabilityTypeName::Directory,
@@ -1780,7 +1783,7 @@
                             name: None,
                         },
                         source: SourceSpec {
-                            abs_moniker: AbsoluteMoniker::parse_str("/one_dir_provider").unwrap(),
+                            node_path: NodePath::absolute_from_vec(vec!["one_dir_provider"]),
                             capability: SourceDeclSpec {
                                 path_prefix: Some(
                                     CapabilityPath::from_str(
@@ -1812,7 +1815,7 @@
         let components = &data_model.get::<Components>()?.entries;
         let config = RouteSourcesConfig {
             component_routes: vec![RouteSourcesSpec {
-                target_node_path: AbsoluteMoniker::parse_str("/two_dir_user").unwrap(),
+                target_node_path: NodePath::absolute_from_vec(vec!["two_dir_user"]),
                 routes_to_skip: vec![
                     // Intentional error: No match for `/data/from/root`. That
                     // way number of routes to skip + number of routes to verify
@@ -1833,7 +1836,7 @@
                             name: None,
                         },
                         source: SourceSpec {
-                            abs_moniker: AbsoluteMoniker::parse_str("/one_dir_provider").unwrap(),
+                            node_path: NodePath::absolute_from_vec(vec!["one_dir_provider"]),
                             capability: SourceDeclSpec {
                                 path_prefix: Some(
                                     CapabilityPath::from_str(
@@ -1865,7 +1868,7 @@
         let components = &data_model.get::<Components>()?.entries;
         let config = RouteSourcesConfig {
             component_routes: vec![RouteSourcesSpec {
-                target_node_path: AbsoluteMoniker::parse_str("/two_dir_user").unwrap(),
+                target_node_path: NodePath::absolute_from_vec(vec!["two_dir_user"]),
                 routes_to_skip: vec![],
                 routes_to_verify: vec![
                     // config.component_routes[0].routes_to_verify[0]:
@@ -1877,7 +1880,7 @@
                             name: None,
                         },
                         source: SourceSpec {
-                            abs_moniker: AbsoluteMoniker::root(),
+                            node_path: NodePath::absolute_from_vec(vec![]),
                             capability: SourceDeclSpec {
                                 // Match complete path with routed subdirs.
                                 path_prefix: Some(
@@ -1902,7 +1905,7 @@
                             name: None,
                         },
                         source: SourceSpec {
-                            abs_moniker: AbsoluteMoniker::parse_str("/one_dir_provider").unwrap(),
+                            node_path: NodePath::absolute_from_vec(vec!["one_dir_provider"]),
                             capability: SourceDeclSpec {
                                 // Match complete path with routed subdirs.
                                 path_prefix: Some(
@@ -1949,7 +1952,7 @@
         let components = &data_model.get::<Components>()?.entries;
         let config = RouteSourcesConfig {
             component_routes: vec![RouteSourcesSpec {
-                target_node_path: AbsoluteMoniker::parse_str("/two_dir_user").unwrap(),
+                target_node_path: NodePath::absolute_from_vec(vec!["two_dir_user"]),
                 routes_to_skip: vec![],
                 routes_to_verify: vec![
                     // config.component_routes[0].routes_to_verify[0]:
@@ -1961,7 +1964,7 @@
                             name: None,
                         },
                         source: SourceSpec {
-                            abs_moniker: AbsoluteMoniker::root(),
+                            node_path: NodePath::absolute_from_vec(vec![]),
                             capability: SourceDeclSpec {
                                 // Match complete path with routed subdirs.
                                 path_prefix: Some(
@@ -1986,7 +1989,7 @@
                             name: None,
                         },
                         source: SourceSpec {
-                            abs_moniker: AbsoluteMoniker::parse_str("/one_dir_provider").unwrap(),
+                            node_path: NodePath::absolute_from_vec(vec!["one_dir_provider"]),
                             capability: SourceDeclSpec {
                                 // Match complete path with routed subdirs.
                                 path_prefix: Some(
@@ -2033,7 +2036,7 @@
         let components = &data_model.get::<Components>()?.entries;
         let config = RouteSourcesConfig {
             component_routes: vec![RouteSourcesSpec {
-                target_node_path: AbsoluteMoniker::parse_str("/two_dir_user").unwrap(),
+                target_node_path: NodePath::absolute_from_vec(vec!["two_dir_user"]),
                 routes_to_skip: vec![],
                 routes_to_verify: vec![
                     // config.component_routes[0].routes_to_verify[0]:
@@ -2045,7 +2048,7 @@
                             name: None,
                         },
                         source: SourceSpec {
-                            abs_moniker: AbsoluteMoniker::root(),
+                            node_path: NodePath::absolute_from_vec(vec![]),
                             capability: SourceDeclSpec {
                                 // Match complete path with routed subdirs.
                                 path_prefix: Some(
@@ -2070,7 +2073,7 @@
                             name: None,
                         },
                         source: SourceSpec {
-                            abs_moniker: AbsoluteMoniker::parse_str("/one_dir_provider").unwrap(),
+                            node_path: NodePath::absolute_from_vec(vec!["one_dir_provider"]),
                             capability: SourceDeclSpec {
                                 // Match complete path with routed subdirs.
                                 path_prefix: Some(
@@ -2095,7 +2098,7 @@
                         VerifyRouteSourcesResult{
                             query: config.component_routes[0].routes_to_verify[0].clone(),
                             result: Err(RouteSourceError::RouteSegmentComponentFromUntrustedSource(RouteSegment::UseBy {
-                                abs_moniker: AbsoluteMoniker::parse_str("/two_dir_user").unwrap(),
+                                node_path: NodePath::absolute_from_vec(vec!["two_dir_user"]),
                                 capability: UseDirectoryDecl{
                                     source: UseSource::Parent,
                                     source_name: CapabilityName("routed_from_root".to_string()),
@@ -2110,7 +2113,7 @@
                         VerifyRouteSourcesResult{
                             query: config.component_routes[0].routes_to_verify[1].clone(),
                             result: Err(RouteSourceError::RouteSegmentComponentFromUntrustedSource(RouteSegment::UseBy {
-                                abs_moniker: AbsoluteMoniker::parse_str("/two_dir_user").unwrap(),
+                                node_path: NodePath::absolute_from_vec(vec!["two_dir_user"]),
                                 capability: UseDirectoryDecl{
                                     source: UseSource::Parent,
                                     source_name: CapabilityName("routed_from_provider".to_string()),
diff --git a/src/security/scrutiny/plugins/src/verify/mod.rs b/src/security/scrutiny/plugins/src/verify/mod.rs
index 6d1f8ca..1c15b6c 100644
--- a/src/security/scrutiny/plugins/src/verify/mod.rs
+++ b/src/security/scrutiny/plugins/src/verify/mod.rs
@@ -16,12 +16,15 @@
             route_sources::RouteSourcesController,
         },
     },
-    cm_fidl_analyzer::route::{CapabilityRouteError, RouteSegment},
+    cm_fidl_analyzer::{
+        node_path::NodePath,
+        route::{CapabilityRouteError, RouteSegment},
+        serde_ext::ErrorWithMessage,
+    },
     cm_rust::{CapabilityName, CapabilityTypeName},
-    moniker::AbsoluteMoniker,
     scrutiny::prelude::*,
     serde::{Deserialize, Serialize},
-    std::{collections::HashSet, error::Error, path::PathBuf, sync::Arc},
+    std::{collections::HashSet, path::PathBuf, sync::Arc},
 };
 
 pub use controller::route_sources::{
@@ -45,44 +48,6 @@
     vec![PluginDescriptor::new("CorePlugin")]
 );
 
-/// Error for use with serialization: Stores both structured error and message,
-/// and assesses equality using structured error.
-#[derive(Clone, Default, Deserialize, Serialize)]
-pub struct ErrorWithMessage<E: Clone + Error + Serialize> {
-    pub error: E,
-    #[serde(default)]
-    pub message: String,
-}
-
-impl<E: Clone + Error + PartialEq + Serialize> PartialEq<ErrorWithMessage<E>>
-    for ErrorWithMessage<E>
-{
-    fn eq(&self, other: &Self) -> bool {
-        // Ignore `message` when comparing.
-        self.error == other.error
-    }
-}
-
-impl<'de, E> From<E> for ErrorWithMessage<E>
-where
-    E: Clone + Deserialize<'de> + Error + Serialize,
-{
-    fn from(error: E) -> Self {
-        Self::from(&error)
-    }
-}
-
-impl<'de, E> From<&E> for ErrorWithMessage<E>
-where
-    E: Clone + Deserialize<'de> + Error + Serialize,
-{
-    fn from(error: &E) -> Self {
-        let message = error.to_string();
-        let error = error.clone();
-        Self { error, message }
-    }
-}
-
 /// Top-level result type for `CapabilityRouteController` query result.
 #[derive(Deserialize, Serialize)]
 pub struct CapabilityRouteResults {
@@ -112,7 +77,7 @@
 /// Error-severity results from `CapabilityRouteController`.
 #[derive(Clone, Deserialize, PartialEq, Serialize)]
 pub struct ErrorResult {
-    pub using_node: AbsoluteMoniker,
+    pub using_node: NodePath,
     pub capability: CapabilityName,
     pub error: ErrorWithMessage<CapabilityRouteError>,
 }
@@ -120,7 +85,7 @@
 /// Warning-severity results from `CapabilityRouteController`.
 #[derive(Clone, Deserialize, Serialize)]
 pub struct WarningResult {
-    pub using_node: AbsoluteMoniker,
+    pub using_node: NodePath,
     pub capability: CapabilityName,
     pub warning: ErrorWithMessage<CapabilityRouteError>,
 }
@@ -128,7 +93,7 @@
 /// Ok-severity results from `CapabilityRouteController`.
 #[derive(Clone, Deserialize, Serialize)]
 pub struct OkResult {
-    pub using_node: AbsoluteMoniker,
+    pub using_node: NodePath,
     pub capability: CapabilityName,
     #[serde(skip_serializing_if = "Vec::is_empty", default)]
     pub route: Vec<RouteSegment>,
@@ -1213,7 +1178,7 @@
                                       "target_path": "/",
                                       "type": "directory"
                                   },
-                                  "abs_moniker": "/child"
+                                  "node_path": "/child"
                               },
                               {
                                   "action": "offer_by",
@@ -1233,7 +1198,7 @@
                                       "target_name": "good_dir",
                                       "type": "directory"
                                   },
-                                  "abs_moniker": "/"
+                                  "node_path": "/"
                               },
                               {
                                   "action": "declare_by",
@@ -1243,7 +1208,7 @@
                                       "source_path": null,
                                       "type": "directory"
                                   },
-                                  "abs_moniker": "/"
+                                  "node_path": "/"
                               }
                           ],
                           "using_node": "/child"
diff --git a/tools/lib/cm_fidl_analyzer/BUILD.gn b/tools/lib/cm_fidl_analyzer/BUILD.gn
index ca8ddf0..99b2d03 100644
--- a/tools/lib/cm_fidl_analyzer/BUILD.gn
+++ b/tools/lib/cm_fidl_analyzer/BUILD.gn
@@ -36,7 +36,9 @@
     "src/component_model.rs",
     "src/environment.rs",
     "src/lib.rs",
+    "src/node_path.rs",
     "src/route.rs",
+    "src/serde_ext.rs",
   ]
   test_deps = [
     "//src/sys/lib/cm_rust/testing",
diff --git a/tools/lib/cm_fidl_analyzer/src/component_instance.rs b/tools/lib/cm_fidl_analyzer/src/component_instance.rs
index 33d62133..4e137cc 100644
--- a/tools/lib/cm_fidl_analyzer/src/component_instance.rs
+++ b/tools/lib/cm_fidl_analyzer/src/component_instance.rs
@@ -6,12 +6,13 @@
     crate::{
         component_model::{BuildAnalyzerModelError, Child},
         environment::EnvironmentForAnalyzer,
+        node_path::NodePath,
         route::RouteMapper,
     },
     async_trait::async_trait,
     cm_moniker::{InstancedAbsoluteMoniker, InstancedChildMoniker},
     cm_rust::{CapabilityDecl, CollectionDecl, ComponentDecl, ExposeDecl, OfferDecl, UseDecl},
-    moniker::{AbsoluteMoniker, AbsoluteMonikerBase, ChildMoniker},
+    moniker::{AbsoluteMoniker, AbsoluteMonikerBase, ChildMoniker, ChildMonikerBase},
     routing::{
         capability_source::{BuiltinCapabilities, NamespaceCapabilities},
         component_id_index::ComponentIdIndex,
@@ -52,6 +53,13 @@
         &self.decl
     }
 
+    /// Returns a representation of the instance's position in the component instance tree.
+    pub fn node_path(&self) -> NodePath {
+        NodePath::absolute_from_vec(
+            self.abs_moniker.path().into_iter().map(|m| m.as_str()).collect(),
+        )
+    }
+
     // Creates a new root component instance.
     pub(crate) fn new_root(
         decl: ComponentDecl,
diff --git a/tools/lib/cm_fidl_analyzer/src/component_model.rs b/tools/lib/cm_fidl_analyzer/src/component_model.rs
index ca393b5..a75822a 100644
--- a/tools/lib/cm_fidl_analyzer/src/component_model.rs
+++ b/tools/lib/cm_fidl_analyzer/src/component_model.rs
@@ -6,6 +6,7 @@
     crate::{
         component_instance::{ComponentInstanceForAnalyzer, TopInstanceForAnalyzer},
         match_absolute_component_urls,
+        node_path::NodePath,
         route::{RouteMap, RouteSegment, VerifyRouteResult},
         PkgUrlMatch,
     },
@@ -138,25 +139,27 @@
     }
 
     fn load_dynamic_components(
-        input: HashMap<AbsoluteMoniker, (AbsoluteComponentUrl, Option<String>)>,
+        input: HashMap<NodePath, (AbsoluteComponentUrl, Option<String>)>,
     ) -> (HashMap<AbsoluteMoniker, Vec<Child>>, Vec<anyhow::Error>) {
         let mut errors: Vec<anyhow::Error> = vec![];
         let mut dynamic_components: HashMap<AbsoluteMoniker, Vec<Child>> = HashMap::new();
-        for (abs_moniker, (url, environment)) in input.into_iter() {
-            let mut moniker_vec = abs_moniker.path().clone();
-            let child_moniker = moniker_vec.pop();
-            if child_moniker.is_none() {
+        for (node_path, (url, environment)) in input.into_iter() {
+            let mut moniker_vec = node_path.as_vec();
+            let child_moniker_str = moniker_vec.pop();
+            if child_moniker_str.is_none() {
                 errors.push(
                     BuildAnalyzerModelError::DynamicComponentInvalidMoniker(url.to_string()).into(),
                 );
                 continue;
             }
+            let child_moniker_str = child_moniker_str.unwrap();
 
-            let child_moniker = child_moniker.unwrap();
+            let abs_moniker: AbsoluteMoniker = moniker_vec.into();
+            let child_moniker: ChildMoniker = child_moniker_str.into();
             if child_moniker.collection.is_none() {
                 errors.push(
                     BuildAnalyzerModelError::DynamicComponentWithoutCollection(
-                        abs_moniker.to_string(),
+                        node_path.to_string(),
                         url.to_string(),
                     )
                     .into(),
@@ -170,10 +173,11 @@
                     children.push(Child { child_moniker, url, environment });
                 }
                 Err(_) => {
+                    let node_path: NodePath = abs_moniker.into();
                     errors.push(
                         BuildAnalyzerModelError::MalformedUrl(
                             url.to_string(),
-                            abs_moniker.to_string(),
+                            node_path.to_string(),
                         )
                         .into(),
                     );
@@ -201,7 +205,7 @@
 
     pub fn build_with_dynamic_components(
         self,
-        dynamic_components: HashMap<AbsoluteMoniker, (AbsoluteComponentUrl, Option<String>)>,
+        dynamic_components: HashMap<NodePath, (AbsoluteComponentUrl, Option<String>)>,
         decls_by_url: HashMap<Url, ComponentDecl>,
         runtime_config: Arc<RuntimeConfig>,
         component_id_index: Arc<ComponentIdIndex>,
@@ -273,7 +277,9 @@
                         &mut result,
                     );
 
-                    model.instances.insert(root_instance.abs_moniker().clone(), root_instance);
+                    model
+                        .instances
+                        .insert(NodePath::from(root_instance.abs_moniker().clone()), root_instance);
 
                     result.model = Some(Arc::new(model));
                 }
@@ -324,7 +330,7 @@
                     if child.child_moniker.name.is_empty() {
                         result.errors.push(anyhow!(BuildAnalyzerModelError::InvalidChildDecl(
                             absolute_url.to_string(),
-                            instance.abs_moniker().to_string(),
+                            NodePath::from(instance.abs_moniker().clone()).to_string(),
                         )));
                         continue;
                     }
@@ -359,7 +365,7 @@
                                     );
 
                                     model.instances.insert(
-                                        child_instance.abs_moniker().clone(),
+                                        NodePath::from(child_instance.abs_moniker().clone()),
                                         child_instance,
                                     );
                                 }
@@ -372,7 +378,7 @@
                             result.errors.push(anyhow!(
                                 BuildAnalyzerModelError::ComponentDeclNotFound(
                                     absolute_url.to_string(),
-                                    instance.abs_moniker().to_string(),
+                                    NodePath::from(instance.abs_moniker().clone()).to_string(),
                                 )
                             ));
                         }
@@ -393,7 +399,7 @@
     ) -> Result<Url, BuildAnalyzerModelError> {
         let err = BuildAnalyzerModelError::MalformedUrl(
             instance.url().to_string(),
-            instance.abs_moniker().to_string(),
+            instance.node_path().to_string(),
         );
 
         match Url::parse(child_url) {
@@ -485,11 +491,11 @@
 }
 
 /// `ComponentModelForAnalyzer` owns a representation of the v2 component graph and
-/// supports lookup of component instances by `AbsoluteMoniker`.
+/// supports lookup of component instances by `NodePath`.
 #[derive(Default)]
 pub struct ComponentModelForAnalyzer {
     top_instance: Arc<TopInstanceForAnalyzer>,
-    instances: HashMap<AbsoluteMoniker, Arc<ComponentInstanceForAnalyzer>>,
+    instances: HashMap<NodePath, Arc<ComponentInstanceForAnalyzer>>,
     policy_checker: GlobalPolicyChecker,
     component_id_index: Arc<ComponentIdIndex>,
 }
@@ -503,18 +509,20 @@
     pub fn get_root_instance(
         self: &Arc<Self>,
     ) -> Result<Arc<ComponentInstanceForAnalyzer>, ComponentInstanceError> {
-        self.get_instance(&AbsoluteMoniker::root())
+        self.get_instance(&NodePath::absolute_from_vec(vec![]))
     }
 
     /// Returns the component instance corresponding to `id` if it is present in the model, or an
     /// `InstanceNotFound` error if not.
     pub fn get_instance(
         self: &Arc<Self>,
-        abs_moniker: &AbsoluteMoniker,
+        id: &NodePath,
     ) -> Result<Arc<ComponentInstanceForAnalyzer>, ComponentInstanceError> {
-        match self.instances.get(abs_moniker) {
+        match self.instances.get(id) {
             Some(instance) => Ok(Arc::clone(instance)),
-            None => Err(ComponentInstanceError::instance_not_found(abs_moniker.clone())),
+            None => Err(ComponentInstanceError::instance_not_found(
+                AbsoluteMoniker::parse_str(&id.to_string()).unwrap(),
+            )),
         }
     }
 
@@ -682,7 +690,7 @@
                 Ok(()) => {
                     for route in routes.into_iter() {
                         results.push(VerifyRouteResult {
-                            using_node: target.abs_moniker().clone(),
+                            using_node: target.node_path(),
                             capability: capability.clone(),
                             result: Ok(route.into()),
                         });
@@ -690,14 +698,14 @@
                 }
                 Err(err) => {
                     results.push(VerifyRouteResult {
-                        using_node: target.abs_moniker().clone(),
+                        using_node: target.node_path(),
                         capability: capability.clone(),
                         result: Err(err.into()),
                     });
                 }
             },
             (Err(err), capability) => results.push(VerifyRouteResult {
-                using_node: target.abs_moniker().clone(),
+                using_node: target.node_path(),
                 capability: capability.clone(),
                 result: Err(err.into()),
             }),
@@ -723,7 +731,7 @@
                     Err(err) => Err(AnalyzerModelError::from(err).into()),
                 };
                 Some(VerifyRouteResult {
-                    using_node: target.abs_moniker().clone(),
+                    using_node: target.node_path(),
                     capability: expose_decl.target_name().clone(),
                     result,
                 })
@@ -742,20 +750,20 @@
         match program_decl.runner {
             Some(ref runner) => {
                 let mut route = RouteMap::from_segments(vec![RouteSegment::RequireRunner {
-                    abs_moniker: target.abs_moniker().clone(),
+                    node_path: target.node_path(),
                     runner: runner.clone(),
                 }]);
                 match Self::route_capability_sync(RouteRequest::Runner(runner.clone()), target) {
                     Ok((_source, mut segments)) => {
                         route.append(&mut segments);
                         Some(VerifyRouteResult {
-                            using_node: target.abs_moniker().clone(),
+                            using_node: target.node_path(),
                             capability: runner.clone(),
                             result: Ok(route.into()),
                         })
                     }
                     Err(err) => Some(VerifyRouteResult {
-                        using_node: target.abs_moniker().clone(),
+                        using_node: target.node_path(),
                         capability: runner.clone(),
                         result: Err(AnalyzerModelError::from(err).into()),
                     }),
@@ -775,7 +783,7 @@
         let url = Url::parse(target.url()).expect("failed to parse target URL");
         let scheme = url.scheme();
         let mut route = vec![RouteSegment::RequireResolver {
-            abs_moniker: target.abs_moniker().clone(),
+            node_path: target.node_path(),
             scheme: scheme.to_string(),
         }];
 
@@ -786,12 +794,12 @@
                     &instance,
                 ) {
                     Ok((_source, route)) => VerifyRouteResult {
-                        using_node: target.abs_moniker().clone(),
+                        using_node: target.node_path(),
                         capability: resolver.resolver,
                         result: Ok(route.into()),
                     },
                     Err(err) => VerifyRouteResult {
-                        using_node: target.abs_moniker().clone(),
+                        using_node: target.node_path(),
                         capability: resolver.resolver,
                         result: Err(AnalyzerModelError::from(err).into()),
                     },
@@ -803,25 +811,25 @@
                         let mut route = RouteMap::new();
                         route.push(RouteSegment::ProvideAsBuiltin { capability: decl });
                         VerifyRouteResult {
-                            using_node: target.abs_moniker().clone(),
+                            using_node: target.node_path(),
                             capability: resolver.resolver,
                             result: Ok(route.into()),
                         }
                     }
                     Err(err) => VerifyRouteResult {
-                        using_node: target.abs_moniker().clone(),
+                        using_node: target.node_path(),
                         capability: resolver.resolver,
                         result: Err(err.into()),
                     },
                 }
             }
             Ok(None) => VerifyRouteResult {
-                using_node: target.abs_moniker().clone(),
+                using_node: target.node_path(),
                 capability: "".into(),
                 result: Err(AnalyzerModelError::MissingResolverForScheme(scheme.to_string()).into()),
             },
             Err(err) => VerifyRouteResult {
-                using_node: target.abs_moniker().clone(),
+                using_node: target.node_path(),
                 capability: "".into(),
                 result: Err(AnalyzerModelError::from(err).into()),
             },
@@ -835,7 +843,7 @@
             Err(err) => Err(err.into()),
         };
         VerifyRouteResult {
-            using_node: target.abs_moniker().clone(),
+            using_node: target.node_path(),
             capability: check_route.capability,
             result: check_result,
         }
@@ -1088,7 +1096,7 @@
 mod tests {
     use {
         super::ModelBuilderForAnalyzer,
-        crate::{environment::BOOT_SCHEME, ComponentModelForAnalyzer},
+        crate::{environment::BOOT_SCHEME, node_path::NodePath, ComponentModelForAnalyzer},
         anyhow::Result,
         cm_moniker::InstancedAbsoluteMoniker,
         cm_rust::{
@@ -1155,17 +1163,18 @@
         let model = build_model_result.model.unwrap();
         assert_eq!(model.len(), 2);
 
-        let root_instance = model.get_instance(&AbsoluteMoniker::root()).expect("root instance");
+        let root_instance =
+            model.get_instance(&NodePath::absolute_from_vec(vec![])).expect("root instance");
         let child_instance = model
-            .get_instance(&AbsoluteMoniker::parse_str("/child").unwrap())
+            .get_instance(&NodePath::absolute_from_vec(vec!["child"]))
             .expect("child instance");
 
-        let other_moniker = AbsoluteMoniker::parse_str("/other").unwrap();
-        let get_other_result = model.get_instance(&other_moniker);
+        let other_id = NodePath::absolute_from_vec(vec!["other"]);
+        let get_other_result = model.get_instance(&other_id);
         assert_eq!(
             get_other_result.err().unwrap().to_string(),
             ComponentInstanceError::instance_not_found(
-                AbsoluteMoniker::parse_str(&other_moniker.to_string()).unwrap()
+                AbsoluteMoniker::parse_str(&other_id.to_string()).unwrap()
             )
             .to_string()
         );
@@ -1261,7 +1270,7 @@
         assert_eq!(model.len(), 2);
 
         let child_instance = model
-            .get_instance(&AbsoluteMoniker::parse_str("/child").unwrap())
+            .get_instance(&NodePath::absolute_from_vec(vec!["child"]))
             .expect("child instance");
 
         assert_eq!(child_instance.url(), absolute_child_url.as_str());
@@ -1288,7 +1297,8 @@
         let model = build_model_result.model.unwrap();
         assert_eq!(model.len(), 1);
 
-        let root_instance = model.get_instance(&AbsoluteMoniker::root()).expect("root instance");
+        let root_instance =
+            model.get_instance(&NodePath::absolute_from_vec(vec![])).expect("root instance");
 
         // Panics if the future returned by `route_capability` was not ready immediately.
         // If no panic, discard the result.
@@ -1327,7 +1337,8 @@
         let model = build_model_result.model.unwrap();
         assert_eq!(model.len(), 1);
 
-        let root_instance = model.get_instance(&AbsoluteMoniker::root()).expect("root instance");
+        let root_instance =
+            model.get_instance(&NodePath::absolute_from_vec(vec![])).expect("root instance");
 
         // Panics if the future returned by `route_storage_and_backing_directory` was not ready immediately.
         // If no panic, discard the result.
@@ -1405,7 +1416,7 @@
         assert_eq!(model.len(), 2);
 
         let child_instance = model
-            .get_instance(&AbsoluteMoniker::parse_str("/child").unwrap())
+            .get_instance(&NodePath::absolute_from_vec(vec!["child"]))
             .expect("child instance");
 
         let get_child_runner_result = child_instance
diff --git a/tools/lib/cm_fidl_analyzer/src/environment.rs b/tools/lib/cm_fidl_analyzer/src/environment.rs
index 9b05f50..51d1a60 100644
--- a/tools/lib/cm_fidl_analyzer/src/environment.rs
+++ b/tools/lib/cm_fidl_analyzer/src/environment.rs
@@ -6,6 +6,7 @@
     crate::{
         component_instance::{ComponentInstanceForAnalyzer, TopInstanceForAnalyzer},
         component_model::{BuildAnalyzerModelError, Child},
+        node_path::NodePath,
     },
     cm_rust::{EnvironmentDecl, RegistrationSource, ResolverRegistration},
     fidl_fuchsia_component_internal as component_internal,
@@ -151,7 +152,7 @@
                     .ok_or(BuildAnalyzerModelError::EnvironmentNotFound(
                         child_env_name.clone(),
                         child.child_moniker.name.clone(),
-                        parent.abs_moniker().to_string(),
+                        NodePath::from(parent.abs_moniker().clone()).to_string(),
                     ))?;
                 Self::new_from_decl(parent, env_decl)
             }
diff --git a/tools/lib/cm_fidl_analyzer/src/lib.rs b/tools/lib/cm_fidl_analyzer/src/lib.rs
index eeffc33..350effd 100644
--- a/tools/lib/cm_fidl_analyzer/src/lib.rs
+++ b/tools/lib/cm_fidl_analyzer/src/lib.rs
@@ -5,7 +5,9 @@
 pub mod component_instance;
 pub mod component_model;
 pub mod environment;
+pub mod node_path;
 pub mod route;
+pub mod serde_ext;
 
 use {
     crate::{
@@ -223,7 +225,7 @@
         &mut self,
         instance: &Arc<ComponentInstanceForAnalyzer>,
     ) -> Result<(), anyhow::Error> {
-        self.visited.push((instance.abs_moniker().to_string(), instance.url().to_string()));
+        self.visited.push((instance.node_path().to_string(), instance.url().to_string()));
         Ok(())
     }
 }
diff --git a/tools/lib/cm_fidl_analyzer/src/node_path.rs b/tools/lib/cm_fidl_analyzer/src/node_path.rs
new file mode 100644
index 0000000..e3adb79
--- /dev/null
+++ b/tools/lib/cm_fidl_analyzer/src/node_path.rs
@@ -0,0 +1,83 @@
+// Copyright 2021 The Fuchsia Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+use {
+    moniker::{AbsoluteMoniker, AbsoluteMonikerBase, ChildMoniker, ChildMonikerBase},
+    std::{fmt, fmt::Display},
+};
+
+/// A representation of a component's position in the component topology. The last segment of
+/// a component's `NodePath` is its `ChildMoniker` as designated by its parent component,
+/// and the prefix is the parent component's `NodePath`.
+#[derive(Clone, Debug, Default, Eq, Hash, PartialEq)]
+pub struct NodePath(Vec<ChildMoniker>);
+
+impl NodePath {
+    pub fn new(monikers: Vec<ChildMoniker>) -> Self {
+        let mut node_path = NodePath::default();
+        node_path.0 = monikers;
+        node_path
+    }
+
+    /// Construct NodePath from string references that correspond to parsable
+    /// `ChildMoniker` instances.
+    pub fn absolute_from_vec(vec: Vec<&str>) -> Self {
+        let abs_moniker: AbsoluteMoniker = vec.into();
+        Self::new(abs_moniker.path().clone())
+    }
+
+    /// Returns a new `NodePath` which extends `self` by appending `moniker` at the end of the path.
+    pub fn extended(&self, moniker: ChildMoniker) -> Self {
+        let mut node_path = NodePath::new(self.0.clone());
+        node_path.0.push(moniker);
+        node_path
+    }
+
+    /// Construct string references that correspond to underlying
+    /// `ChildMoniker` instances.
+    pub fn as_vec(&self) -> Vec<&str> {
+        self.0.iter().map(|moniker| moniker.as_str()).collect()
+    }
+}
+
+impl Display for NodePath {
+    // Displays a `NodePath` as a slash-separated path.
+    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+        if self.0.is_empty() {
+            return write!(f, "/");
+        }
+        let mut path_string = "".to_owned();
+        for moniker in self.0.iter() {
+            path_string.push('/');
+            path_string.push_str(moniker.as_str());
+        }
+        write!(f, "{}", path_string)
+    }
+}
+
+impl From<AbsoluteMoniker> for NodePath {
+    fn from(moniker: AbsoluteMoniker) -> Self {
+        Self::new(moniker.path().clone())
+    }
+}
+
+#[cfg(test)]
+mod tests {
+    use super::*;
+
+    // Tests the `extended()` and `to_string()` methods of `NodePath`.
+    #[test]
+    fn node_path_operations() {
+        let empty_node_path = NodePath::default();
+        assert_eq!(empty_node_path.to_string(), "/");
+
+        let foo_moniker = ChildMoniker::new("foo".to_string(), None);
+        let foo_node_path = empty_node_path.extended(foo_moniker);
+        assert_eq!(foo_node_path.to_string(), "/foo");
+
+        let bar_moniker = ChildMoniker::new("bar".to_string(), None);
+        let bar_node_path = foo_node_path.extended(bar_moniker);
+        assert_eq!(bar_node_path.to_string(), "/foo/bar");
+    }
+}
diff --git a/tools/lib/cm_fidl_analyzer/src/route.rs b/tools/lib/cm_fidl_analyzer/src/route.rs
index 6e7a0de..5c8c23f 100644
--- a/tools/lib/cm_fidl_analyzer/src/route.rs
+++ b/tools/lib/cm_fidl_analyzer/src/route.rs
@@ -3,7 +3,10 @@
 // found in the LICENSE file.
 
 use {
-    crate::component_model::{AnalyzerModelError, BuildAnalyzerModelError},
+    crate::{
+        component_model::{AnalyzerModelError, BuildAnalyzerModelError},
+        node_path::NodePath,
+    },
     cm_rust::{CapabilityDecl, CapabilityName, ExposeDecl, OfferDecl, UseDecl},
     fuchsia_zircon_status as zx_status,
     moniker::AbsoluteMoniker,
@@ -15,7 +18,7 @@
 /// A summary of a specific capability route and the outcome of verification.
 #[derive(Clone, Debug, PartialEq)]
 pub struct VerifyRouteResult {
-    pub using_node: AbsoluteMoniker,
+    pub using_node: NodePath,
     pub capability: CapabilityName,
     pub result: Result<Vec<RouteSegment>, CapabilityRouteError>,
 }
@@ -25,55 +28,55 @@
 pub enum RouteSegment {
     /// A `ComponentNode` uses the routed capability.
     UseBy {
-        /// The `AbsoluteMoniker` of the using `ComponentNode`.
-        abs_moniker: AbsoluteMoniker,
+        /// The `NodePath` of the using `ComponentNode`.
+        node_path: NodePath,
         /// The use declaration from the `ComponentNode`'s manifest.
         capability: UseDecl,
     },
 
     /// A `ComponentNode` requires a runner in its `ProgramDecl`.
     RequireRunner {
-        /// The `AbsoluteMoniker` of the component instance that requires the runner.
-        abs_moniker: AbsoluteMoniker,
+        /// The `NodePath` of the component instance that requires the runner.
+        node_path: NodePath,
         /// The name of the required runner.
         runner: CapabilityName,
     },
 
     /// A `ComponentNode` requires the resolver capability to resolve a child component URL.
     RequireResolver {
-        /// The `AbsoluteMoniker` of the component node that requires the resolver.
-        abs_moniker: AbsoluteMoniker,
+        /// The `NodePath` of the component node that requires the resolver.
+        node_path: NodePath,
         /// The URL scheme of the resolver.
         scheme: String,
     },
 
     /// A `ComponentNode` offers the routed capability.
     OfferBy {
-        /// The `AbsoluteMoniker` of the offering `ComponentNode`.
-        abs_moniker: AbsoluteMoniker,
+        /// The `NodePath` of the offering `ComponentNode`.
+        node_path: NodePath,
         /// The offer declaration from the `ComponentNode`'s manifest.
         capability: OfferDecl,
     },
 
     /// A `ComponentNode` exposes the routed capability.
     ExposeBy {
-        /// The `AbsoluteMoniker` of the offering `ComponentNode`.
-        abs_moniker: AbsoluteMoniker,
+        /// The `NodePath` of the offering `ComponentNode`.
+        node_path: NodePath,
         /// The expose declaration from the `ComponentNode`'s manifest.
         capability: ExposeDecl,
     },
 
     /// A `ComponentNode` declares the routed capability.
     DeclareBy {
-        /// The `AbsoluteMoniker` of the declaring `ComponentNode`.
-        abs_moniker: AbsoluteMoniker,
+        /// The `NodePath` of the declaring `ComponentNode`.
+        node_path: NodePath,
         /// The capability declaration from the `ComponentNode`'s manifest.
         capability: CapabilityDecl,
     },
 
     RegisterBy {
-        /// The `AbsoluteMoniker` of the `ComponentNode` that registered the capability.
-        abs_moniker: AbsoluteMoniker,
+        /// The `NodePath` of the `ComponentNode` that registered the capability.
+        node_path: NodePath,
         /// The registration declaration. For runner and resolver registrations, this
         /// appears directly in the `ComponentNode`'s manifest. For storage-backing
         /// directories, this is derived from the storage capability's `StorageDecl`.
@@ -104,15 +107,15 @@
 }
 
 impl RouteSegment {
-    pub fn abs_moniker<'a>(&'a self) -> Option<&'a AbsoluteMoniker> {
+    pub fn node_path<'a>(&'a self) -> Option<&'a NodePath> {
         match self {
-            Self::UseBy { abs_moniker, .. }
-            | Self::RequireRunner { abs_moniker, .. }
-            | Self::RequireResolver { abs_moniker, .. }
-            | Self::OfferBy { abs_moniker, .. }
-            | Self::ExposeBy { abs_moniker, .. }
-            | Self::DeclareBy { abs_moniker, .. }
-            | Self::RegisterBy { abs_moniker, .. } => Some(abs_moniker),
+            Self::UseBy { node_path, .. }
+            | Self::RequireRunner { node_path, .. }
+            | Self::RequireResolver { node_path, .. }
+            | Self::OfferBy { node_path, .. }
+            | Self::ExposeBy { node_path, .. }
+            | Self::DeclareBy { node_path, .. }
+            | Self::RegisterBy { node_path, .. } => Some(node_path),
             Self::ProvideFromFramework { .. }
             | Self::ProvideAsBuiltin { .. }
             | Self::ProvideFromNamespace { .. }
@@ -217,15 +220,24 @@
     type RouteMap = RouteMap;
 
     fn add_use(&mut self, abs_moniker: AbsoluteMoniker, use_decl: UseDecl) {
-        self.route.push(RouteSegment::UseBy { abs_moniker, capability: use_decl })
+        self.route.push(RouteSegment::UseBy {
+            node_path: NodePath::from(abs_moniker),
+            capability: use_decl,
+        })
     }
 
     fn add_offer(&mut self, abs_moniker: AbsoluteMoniker, offer_decl: OfferDecl) {
-        self.route.push(RouteSegment::OfferBy { abs_moniker, capability: offer_decl })
+        self.route.push(RouteSegment::OfferBy {
+            node_path: NodePath::from(abs_moniker),
+            capability: offer_decl,
+        })
     }
 
     fn add_expose(&mut self, abs_moniker: AbsoluteMoniker, expose_decl: ExposeDecl) {
-        self.route.push(RouteSegment::ExposeBy { abs_moniker, capability: expose_decl })
+        self.route.push(RouteSegment::ExposeBy {
+            node_path: NodePath::from(abs_moniker),
+            capability: expose_decl,
+        })
     }
 
     fn add_registration(
@@ -233,7 +245,10 @@
         abs_moniker: AbsoluteMoniker,
         registration_decl: RegistrationDecl,
     ) {
-        self.route.push(RouteSegment::RegisterBy { abs_moniker, capability: registration_decl })
+        self.route.push(RouteSegment::RegisterBy {
+            node_path: NodePath::from(abs_moniker),
+            capability: registration_decl,
+        })
     }
 
     fn add_component_capability(
@@ -241,7 +256,10 @@
         abs_moniker: AbsoluteMoniker,
         capability_decl: CapabilityDecl,
     ) {
-        self.route.push(RouteSegment::DeclareBy { abs_moniker, capability: capability_decl })
+        self.route.push(RouteSegment::DeclareBy {
+            node_path: NodePath::from(abs_moniker),
+            capability: capability_decl,
+        })
     }
 
     fn add_framework_capability(&mut self, capability_name: CapabilityName) {
diff --git a/tools/lib/cm_fidl_analyzer/src/serde_ext.rs b/tools/lib/cm_fidl_analyzer/src/serde_ext.rs
new file mode 100644
index 0000000..4fcd242
--- /dev/null
+++ b/tools/lib/cm_fidl_analyzer/src/serde_ext.rs
@@ -0,0 +1,105 @@
+// Copyright 2021 The Fuchsia Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+use {
+    crate::node_path::NodePath,
+    serde::{
+        de::{self, Deserializer, Visitor},
+        Deserialize, Serialize, Serializer,
+    },
+    std::{error::Error, fmt},
+};
+
+/// Serialize `NodePath` into a path-like slash-separated a string
+/// representation of the underlying child monikers.
+//
+/// Example: "/alpha:2/beta:0/gamma:1".
+impl Serialize for NodePath {
+    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
+    where
+        S: Serializer,
+    {
+        let path = format!("/{}", self.as_vec().join("/"));
+        serializer.serialize_str(&path)
+    }
+}
+
+struct NodePathVisitor;
+
+/// Deserialize `NodePath` from path-like slash-separated string of child
+/// monikers.
+impl<'de> Visitor<'de> for NodePathVisitor {
+    type Value = NodePath;
+
+    fn expecting(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
+        formatter.write_str("A component tree node path")
+    }
+
+    fn visit_str<E>(self, value: &str) -> Result<Self::Value, E>
+    where
+        E: de::Error,
+    {
+        Ok(NodePath::new(
+            value
+                .split("/")
+                .filter_map(
+                    |component| {
+                        if component.is_empty() {
+                            None
+                        } else {
+                            Some(component.into())
+                        }
+                    },
+                )
+                .collect(),
+        ))
+    }
+}
+
+impl<'de> Deserialize<'de> for NodePath {
+    fn deserialize<D>(deserializer: D) -> Result<NodePath, D::Error>
+    where
+        D: Deserializer<'de>,
+    {
+        deserializer.deserialize_str(NodePathVisitor)
+    }
+}
+
+/// Error for use with serialization: Stores both structured error and message,
+/// and assesses equality using structured error.
+#[derive(Clone, Default, Deserialize, Serialize)]
+pub struct ErrorWithMessage<E: Clone + Error + Serialize> {
+    pub error: E,
+    #[serde(default)]
+    pub message: String,
+}
+
+impl<E: Clone + Error + PartialEq + Serialize> PartialEq<ErrorWithMessage<E>>
+    for ErrorWithMessage<E>
+{
+    fn eq(&self, other: &Self) -> bool {
+        // Ignore `message` when comparing.
+        self.error == other.error
+    }
+}
+
+impl<'de, E> From<E> for ErrorWithMessage<E>
+where
+    E: Clone + Deserialize<'de> + Error + Serialize,
+{
+    fn from(error: E) -> Self {
+        Self::from(&error)
+    }
+}
+
+impl<'de, E> From<&E> for ErrorWithMessage<E>
+where
+    E: Clone + Deserialize<'de> + Error + Serialize,
+{
+    fn from(error: &E) -> Self {
+        let message = error.to_string();
+        let error = error.clone();
+        Self { error, message }
+    }
+}
diff --git a/tools/lib/cm_fidl_analyzer/tests/src/routing.rs b/tools/lib/cm_fidl_analyzer/tests/src/routing.rs
index 94a3cd2..7656052 100644
--- a/tools/lib/cm_fidl_analyzer/tests/src/routing.rs
+++ b/tools/lib/cm_fidl_analyzer/tests/src/routing.rs
@@ -10,6 +10,7 @@
         component_instance::ComponentInstanceForAnalyzer,
         component_model::{AnalyzerModelError, ComponentModelForAnalyzer, ModelBuilderForAnalyzer},
         environment::{BOOT_RESOLVER_NAME, BOOT_SCHEME},
+        node_path::NodePath,
         route::{CapabilityRouteError, RouteSegment, VerifyRouteResult},
     },
     cm_rust::{
@@ -468,14 +469,14 @@
     type C = ComponentInstanceForAnalyzer;
 
     async fn check_use(&self, moniker: AbsoluteMoniker, check: CheckUse) {
-        let target = self.model.get_instance(&moniker).expect("target instance not found");
+        let target_id = NodePath::new(moniker.path().clone());
+        let target = self.model.get_instance(&target_id).expect("target instance not found");
         let scope =
             if let CheckUse::EventStream { path: _, ref scope, name: _, expected_res: _ } = check {
                 Some(scope.clone())
             } else {
                 None
             };
-
         let (find_decl, expected) = self.find_matching_use(check, target.decl_for_testing());
 
         // If `find_decl` is not OK, check that `expected` has a matching error.
@@ -524,7 +525,8 @@
     }
 
     async fn check_use_exposed_dir(&self, moniker: AbsoluteMoniker, check: CheckUse) {
-        let target = self.model.get_instance(&moniker).expect("target instance not found");
+        let target =
+            self.model.get_instance(&NodePath::from(moniker)).expect("target instance not found");
 
         let (find_decl, expected) = self.find_matching_expose(check, target.decl_for_testing());
 
@@ -569,7 +571,7 @@
         &self,
         moniker: &AbsoluteMoniker,
     ) -> Result<Arc<ComponentInstanceForAnalyzer>, anyhow::Error> {
-        self.model.get_instance(&moniker).map_err(|err| anyhow!(err))
+        self.model.get_instance(&NodePath::from(moniker.clone())).map_err(|err| anyhow!(err))
     }
 
     // File and directory operations
@@ -1374,7 +1376,7 @@
 
         let result = test.model.check_resolver(&b_component);
         assert!(result.result.is_ok());
-        assert_eq!(result.using_node, AbsoluteMoniker::parse_str("/b").unwrap());
+        assert_eq!(result.using_node, NodePath::absolute_from_vec(vec!["b"]));
         assert_eq!(result.capability, "base");
     }
 
@@ -1533,15 +1535,15 @@
             route_map,
             vec![
                 RouteSegment::UseBy {
-                    abs_moniker: AbsoluteMoniker::parse_str("/b").unwrap(),
+                    node_path: NodePath::absolute_from_vec(vec!["b"]),
                     capability: use_decl
                 },
                 RouteSegment::OfferBy {
-                    abs_moniker: AbsoluteMoniker::root(),
+                    node_path: NodePath::absolute_from_vec(vec![]),
                     capability: offer_decl
                 },
                 RouteSegment::DeclareBy {
-                    abs_moniker: AbsoluteMoniker::root(),
+                    node_path: NodePath::absolute_from_vec(vec![]),
                     capability: CapabilityDecl::Protocol(protocol_decl)
                 }
             ]
@@ -1589,13 +1591,16 @@
         assert_eq!(
             route_map,
             vec![
-                RouteSegment::UseBy { abs_moniker: AbsoluteMoniker::root(), capability: use_decl },
+                RouteSegment::UseBy {
+                    node_path: NodePath::absolute_from_vec(vec![]),
+                    capability: use_decl
+                },
                 RouteSegment::ExposeBy {
-                    abs_moniker: AbsoluteMoniker::parse_str("/b").unwrap(),
+                    node_path: NodePath::absolute_from_vec(vec!["b"]),
                     capability: expose_decl
                 },
                 RouteSegment::DeclareBy {
-                    abs_moniker: AbsoluteMoniker::parse_str("/b").unwrap(),
+                    node_path: NodePath::absolute_from_vec(vec!["b"]),
                     capability: CapabilityDecl::Protocol(protocol_decl)
                 }
             ]
@@ -1630,9 +1635,12 @@
         assert_eq!(
             route_map,
             vec![
-                RouteSegment::UseBy { abs_moniker: AbsoluteMoniker::root(), capability: use_decl },
+                RouteSegment::UseBy {
+                    node_path: NodePath::absolute_from_vec(vec![]),
+                    capability: use_decl
+                },
                 RouteSegment::DeclareBy {
-                    abs_moniker: AbsoluteMoniker::root(),
+                    node_path: NodePath::absolute_from_vec(vec![]),
                     capability: CapabilityDecl::Protocol(protocol_decl)
                 }
             ]
@@ -1724,23 +1732,23 @@
             route_map,
             vec![
                 RouteSegment::UseBy {
-                    abs_moniker: AbsoluteMoniker::parse_str("/c").unwrap(),
+                    node_path: NodePath::absolute_from_vec(vec!["c"]),
                     capability: use_decl
                 },
                 RouteSegment::OfferBy {
-                    abs_moniker: AbsoluteMoniker::root(),
+                    node_path: NodePath::absolute_from_vec(vec![]),
                     capability: a_offer_decl
                 },
                 RouteSegment::ExposeBy {
-                    abs_moniker: AbsoluteMoniker::parse_str("/b").unwrap(),
+                    node_path: NodePath::absolute_from_vec(vec!["b"]),
                     capability: b_expose_decl
                 },
                 RouteSegment::ExposeBy {
-                    abs_moniker: AbsoluteMoniker::parse_str("/b/d").unwrap(),
+                    node_path: NodePath::absolute_from_vec(vec!["b", "d"]),
                     capability: d_expose_decl
                 },
                 RouteSegment::DeclareBy {
-                    abs_moniker: AbsoluteMoniker::parse_str("/b/d").unwrap(),
+                    node_path: NodePath::absolute_from_vec(vec!["b", "d"]),
                     capability: CapabilityDecl::Directory(directory_decl),
                 }
             ]
@@ -1803,15 +1811,15 @@
             route_map,
             vec![
                 RouteSegment::RequireRunner {
-                    abs_moniker: AbsoluteMoniker::parse_str("/b").unwrap(),
+                    node_path: NodePath::absolute_from_vec(vec!["b"]),
                     runner: "hobbit".into(),
                 },
                 RouteSegment::RegisterBy {
-                    abs_moniker: AbsoluteMoniker::root(),
+                    node_path: NodePath::absolute_from_vec(vec![]),
                     capability: RegistrationDecl::Runner(runner_reg)
                 },
                 RouteSegment::DeclareBy {
-                    abs_moniker: AbsoluteMoniker::root(),
+                    node_path: NodePath::absolute_from_vec(vec![]),
                     capability: CapabilityDecl::Runner(runner_decl)
                 },
             ]
@@ -1881,15 +1889,15 @@
             storage_route_map,
             vec![
                 RouteSegment::UseBy {
-                    abs_moniker: AbsoluteMoniker::parse_str("/b").unwrap(),
+                    node_path: NodePath::absolute_from_vec(vec!["b"]),
                     capability: use_storage_decl
                 },
                 RouteSegment::OfferBy {
-                    abs_moniker: AbsoluteMoniker::root(),
+                    node_path: NodePath::absolute_from_vec(vec![]),
                     capability: offer_storage_decl
                 },
                 RouteSegment::DeclareBy {
-                    abs_moniker: AbsoluteMoniker::root(),
+                    node_path: NodePath::absolute_from_vec(vec![]),
                     capability: CapabilityDecl::Storage(storage_decl.clone())
                 }
             ]
@@ -1898,11 +1906,11 @@
             backing_directory_route_map,
             vec![
                 RouteSegment::RegisterBy {
-                    abs_moniker: AbsoluteMoniker::root(),
+                    node_path: NodePath::absolute_from_vec(vec![]),
                     capability: RegistrationDecl::Storage(storage_decl.into())
                 },
                 RouteSegment::DeclareBy {
-                    abs_moniker: AbsoluteMoniker::root(),
+                    node_path: NodePath::absolute_from_vec(vec![]),
                     capability: CapabilityDecl::Directory(directory_decl)
                 }
             ]
@@ -1987,11 +1995,11 @@
             event_route_map,
             vec![
                 RouteSegment::UseBy {
-                    abs_moniker: AbsoluteMoniker::parse_str("/b").unwrap(),
+                    node_path: NodePath::absolute_from_vec(vec!["b"]),
                     capability: use_event_decl
                 },
                 RouteSegment::OfferBy {
-                    abs_moniker: AbsoluteMoniker::root(),
+                    node_path: NodePath::absolute_from_vec(vec![]),
                     capability: offer_event_decl
                 },
                 RouteSegment::ProvideFromFramework { capability: "started".into() }
@@ -2008,11 +2016,11 @@
             event_source_route_map,
             vec![
                 RouteSegment::UseBy {
-                    abs_moniker: AbsoluteMoniker::parse_str("/b").unwrap(),
+                    node_path: NodePath::absolute_from_vec(vec!["b"]),
                     capability: use_event_source_decl
                 },
                 RouteSegment::OfferBy {
-                    abs_moniker: AbsoluteMoniker::root(),
+                    node_path: NodePath::absolute_from_vec(vec![]),
                     capability: offer_event_source_decl
                 },
                 RouteSegment::ProvideAsBuiltin { capability: event_source_decl }
@@ -2071,11 +2079,11 @@
             route_map,
             vec![
                 RouteSegment::UseBy {
-                    abs_moniker: AbsoluteMoniker::parse_str("/b").unwrap(),
+                    node_path: NodePath::absolute_from_vec(vec!["b"]),
                     capability: use_decl
                 },
                 RouteSegment::OfferBy {
-                    abs_moniker: AbsoluteMoniker::root(),
+                    node_path: NodePath::absolute_from_vec(vec![]),
                     capability: offer_decl
                 },
                 RouteSegment::ProvideFromNamespace { capability: capability_decl }
@@ -2142,25 +2150,25 @@
 
         let route_map = test.model.check_resolver(&b_component);
 
-        assert_eq!(route_map.using_node, AbsoluteMoniker::parse_str("/b").unwrap(),);
+        assert_eq!(route_map.using_node, NodePath::absolute_from_vec(vec!["b"]));
         assert_eq!(route_map.capability, "base");
         assert_eq!(
             route_map.result.clone().expect("expected OK route"),
             vec![
                 RouteSegment::RequireResolver {
-                    abs_moniker: AbsoluteMoniker::parse_str("/b").unwrap(),
+                    node_path: NodePath::absolute_from_vec(vec!["b"]),
                     scheme: "base".to_string(),
                 },
                 RouteSegment::RegisterBy {
-                    abs_moniker: AbsoluteMoniker::root(),
+                    node_path: NodePath::absolute_from_vec(vec![]),
                     capability: RegistrationDecl::Resolver(registration_decl)
                 },
                 RouteSegment::ExposeBy {
-                    abs_moniker: AbsoluteMoniker::parse_str("/c").unwrap(),
+                    node_path: NodePath::absolute_from_vec(vec!["c"]),
                     capability: expose_decl
                 },
                 RouteSegment::DeclareBy {
-                    abs_moniker: AbsoluteMoniker::parse_str("/c").unwrap(),
+                    node_path: NodePath::absolute_from_vec(vec!["c"]),
                     capability: CapabilityDecl::Resolver(resolver_decl)
                 }
             ]
@@ -2220,21 +2228,21 @@
 
         let route_map = test.model.check_resolver(&c_component);
 
-        assert_eq!(route_map.using_node, AbsoluteMoniker::parse_str("/b/c").unwrap());
+        assert_eq!(route_map.using_node, NodePath::absolute_from_vec(vec!["b", "c"]));
         assert_eq!(route_map.capability, "base");
         assert_eq!(
             route_map.result.clone().expect("expected OK route"),
             vec![
                 RouteSegment::RequireResolver {
-                    abs_moniker: AbsoluteMoniker::parse_str("/b/c").unwrap(),
+                    node_path: NodePath::absolute_from_vec(vec!["b", "c"]),
                     scheme: "base".to_string(),
                 },
                 RouteSegment::RegisterBy {
-                    abs_moniker: AbsoluteMoniker::root(),
+                    node_path: NodePath::absolute_from_vec(vec![]),
                     capability: RegistrationDecl::Resolver(registration_decl)
                 },
                 RouteSegment::DeclareBy {
-                    abs_moniker: AbsoluteMoniker::root(),
+                    node_path: NodePath::absolute_from_vec(vec![]),
                     capability: CapabilityDecl::Resolver(resolver_decl)
                 }
             ]
@@ -2275,13 +2283,13 @@
 
         let route_map = test.model.check_resolver(&b_component);
 
-        assert_eq!(route_map.using_node, AbsoluteMoniker::parse_str("/b").unwrap());
+        assert_eq!(route_map.using_node, NodePath::absolute_from_vec(vec!["b"]));
         assert_eq!(route_map.capability, BOOT_RESOLVER_NAME);
         assert_eq!(
             route_map.result.clone().expect("expected OK route"),
             vec![
                 RouteSegment::RequireResolver {
-                    abs_moniker: AbsoluteMoniker::parse_str("/b").unwrap(),
+                    node_path: NodePath::absolute_from_vec(vec!["b"]),
                     scheme: BOOT_SCHEME.to_string(),
                 },
                 RouteSegment::ProvideAsBuiltin { capability: boot_resolver_decl }
@@ -2333,21 +2341,21 @@
 
         let route_map = test.model.check_resolver(&b_component);
 
-        assert_eq!(route_map.using_node, AbsoluteMoniker::parse_str("/b").unwrap());
+        assert_eq!(route_map.using_node, NodePath::absolute_from_vec(vec!["b"]));
         assert_eq!(route_map.capability, "test");
         assert_eq!(
             route_map.result.clone().expect("expected OK route"),
             vec![
                 RouteSegment::RequireResolver {
-                    abs_moniker: AbsoluteMoniker::parse_str("/b").unwrap(),
+                    node_path: NodePath::absolute_from_vec(vec!["b"]),
                     scheme: "test".to_string(),
                 },
                 RouteSegment::RegisterBy {
-                    abs_moniker: AbsoluteMoniker::root(),
+                    node_path: NodePath::absolute_from_vec(vec![]),
                     capability: RegistrationDecl::Resolver(resolver_registration)
                 },
                 RouteSegment::DeclareBy {
-                    abs_moniker: AbsoluteMoniker::root(),
+                    node_path: NodePath::absolute_from_vec(vec![]),
                     capability: CapabilityDecl::Resolver(resolver_decl)
                 }
             ]
@@ -2386,7 +2394,7 @@
             route_map,
             vec![
                 RouteSegment::RequireRunner {
-                    abs_moniker: AbsoluteMoniker::root(),
+                    node_path: NodePath::absolute_from_vec(vec![]),
                     runner: "elf".into(),
                 },
                 RouteSegment::ProvideAsBuiltin { capability: elf_runner_decl },
@@ -2522,19 +2530,19 @@
         assert_eq!(
             directories,
             &vec![VerifyRouteResult {
-                using_node: AbsoluteMoniker::parse_str("/b").unwrap(),
+                using_node: NodePath::absolute_from_vec(vec!["b"]),
                 capability: "bar_data".into(),
                 result: Ok(vec![
                     RouteSegment::UseBy {
-                        abs_moniker: AbsoluteMoniker::parse_str("/b").unwrap(),
+                        node_path: NodePath::absolute_from_vec(vec!["b"]),
                         capability: use_directory_decl,
                     },
                     RouteSegment::OfferBy {
-                        abs_moniker: AbsoluteMoniker::root(),
+                        node_path: NodePath::absolute_from_vec(vec![]),
                         capability: offer_directory_decl,
                     },
                     RouteSegment::DeclareBy {
-                        abs_moniker: AbsoluteMoniker::root(),
+                        node_path: NodePath::absolute_from_vec(vec![]),
                         capability: CapabilityDecl::Directory(directory_decl),
                     }
                 ])
@@ -2545,19 +2553,19 @@
         assert_eq!(
             runners,
             &vec![VerifyRouteResult {
-                using_node: AbsoluteMoniker::parse_str("/b").unwrap(),
+                using_node: NodePath::absolute_from_vec(vec!["b"]),
                 capability: "dwarf".into(),
                 result: Ok(vec![
                     RouteSegment::RequireRunner {
-                        abs_moniker: AbsoluteMoniker::parse_str("/b").unwrap(),
+                        node_path: NodePath::absolute_from_vec(vec!["b"]),
                         runner: "dwarf".into(),
                     },
                     RouteSegment::RegisterBy {
-                        abs_moniker: AbsoluteMoniker::root(),
+                        node_path: NodePath::absolute_from_vec(vec![]),
                         capability: RegistrationDecl::Runner(runner_registration_decl)
                     },
                     RouteSegment::DeclareBy {
-                        abs_moniker: AbsoluteMoniker::root(),
+                        node_path: NodePath::absolute_from_vec(vec![]),
                         capability: CapabilityDecl::Runner(runner_decl)
                     }
                 ])
@@ -2569,19 +2577,19 @@
         assert_eq!(
             resolvers,
             &vec![VerifyRouteResult {
-                using_node: AbsoluteMoniker::parse_str("/b").unwrap(),
+                using_node: NodePath::absolute_from_vec(vec!["b"]),
                 capability: "base_resolver".into(),
                 result: Ok(vec![
                     RouteSegment::RequireResolver {
-                        abs_moniker: AbsoluteMoniker::parse_str("/b").unwrap(),
+                        node_path: NodePath::absolute_from_vec(vec!["b"]),
                         scheme: "base".to_string(),
                     },
                     RouteSegment::RegisterBy {
-                        abs_moniker: AbsoluteMoniker::root(),
+                        node_path: NodePath::absolute_from_vec(vec![]),
                         capability: RegistrationDecl::Resolver(resolver_registration_decl)
                     },
                     RouteSegment::DeclareBy {
-                        abs_moniker: AbsoluteMoniker::root(),
+                        node_path: NodePath::absolute_from_vec(vec![]),
                         capability: CapabilityDecl::Resolver(resolver_decl)
                     }
                 ])
@@ -2593,7 +2601,7 @@
         assert_eq!(
             protocols,
             &vec![VerifyRouteResult {
-                using_node: AbsoluteMoniker::parse_str("/b").unwrap(),
+                using_node: NodePath::absolute_from_vec(vec!["b"]),
                 capability: "bad_protocol".into(),
                 result: Err(CapabilityRouteError::AnalyzerModelError(
                     AnalyzerModelError::RoutingError(