[pkg_resolver] GN target that disables dynamic rewrite rules

Bug: 37520
Change-Id: Ie1768e7c978d749b9289ca3624c983ff2374a95c
diff --git a/garnet/bin/pkg_resolver/BUILD.gn b/garnet/bin/pkg_resolver/BUILD.gn
index 5741ff2..5d98102 100644
--- a/garnet/bin/pkg_resolver/BUILD.gn
+++ b/garnet/bin/pkg_resolver/BUILD.gn
@@ -6,6 +6,7 @@
 import("//build/rust/rustc_binary.gni")
 import("//build/test/test_package.gni")
 import("//build/testing/environments.gni")
+import("//garnet/bin/pkg_resolver/pkg_resolver_config.gni")
 
 rustc_binary("bin") {
   name = "pkg_resolver"
@@ -39,9 +40,11 @@
     "//third_party/rust_crates:hyper-rustls",
     "//third_party/rust_crates:log",
     "//third_party/rust_crates:maplit",
+    "//third_party/rust_crates:matches",
     "//third_party/rust_crates:parking_lot",
     "//third_party/rust_crates:pin-utils",
     "//third_party/rust_crates:serde",
+    "//third_party/rust_crates:serde_derive",
     "//third_party/rust_crates:serde_json",
     "//third_party/rust_crates:tempfile",
     "//third_party/rust_crates:url",
@@ -82,3 +85,7 @@
     },
   ]
 }
+
+pkg_resolver_config("disable_dynamic_configuration") {
+  disable_dynamic_configuration = true
+}
diff --git a/garnet/bin/pkg_resolver/meta/pkg_resolver_integration_test.cmx b/garnet/bin/pkg_resolver/meta/pkg_resolver_integration_test.cmx
index 69c0e54..05cee1f 100644
--- a/garnet/bin/pkg_resolver/meta/pkg_resolver_integration_test.cmx
+++ b/garnet/bin/pkg_resolver/meta/pkg_resolver_integration_test.cmx
@@ -4,7 +4,6 @@
     },
     "sandbox": {
         "features": [
-            "isolated-persistent-storage",
             "root-ssl-certificates"
         ],
         "services": [
diff --git a/garnet/bin/pkg_resolver/pkg_resolver_config.gni b/garnet/bin/pkg_resolver/pkg_resolver_config.gni
new file mode 100644
index 0000000..85dc37d
--- /dev/null
+++ b/garnet/bin/pkg_resolver/pkg_resolver_config.gni
@@ -0,0 +1,33 @@
+# Copyright 2019 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.
+
+import("//build/config.gni")
+
+# Define pkg_resolver configuration data to be included in the
+# config-data package.
+#
+#   disable_dynamic_configuration
+#     [bool] Disable editing package URL rewrite rules.
+#
+template("pkg_resolver_config") {
+  cfg = {
+    forward_variables_from(invoker, [ "disable_dynamic_configuration" ])
+    assert(defined(disable_dynamic_configuration),
+           "disable_dynamic_configuration must be defined")
+  }
+
+  config_path = "$target_gen_dir/pkg_resolver_config.json"
+
+  write_file(config_path, cfg, "json")
+
+  config_data(target_name) {
+    for_pkg = "pkg_resolver"
+    outputs = [
+      "config.json",
+    ]
+    sources = [
+      config_path,
+    ]
+  }
+}
diff --git a/garnet/bin/pkg_resolver/src/config.rs b/garnet/bin/pkg_resolver/src/config.rs
new file mode 100644
index 0000000..7e4559d9
--- /dev/null
+++ b/garnet/bin/pkg_resolver/src/config.rs
@@ -0,0 +1,113 @@
+// Copyright 2019 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 {
+    failure::Fail,
+    fuchsia_syslog::{fx_log_err, fx_log_info},
+    serde_derive::Deserialize,
+    std::{
+        fs::File,
+        io::{BufReader, Read},
+    },
+};
+
+/// Static service configuration options.
+#[derive(Debug, Default, PartialEq, Eq)]
+pub struct Config {
+    disable_dynamic_configuration: bool,
+}
+
+impl Config {
+    pub fn disable_dynamic_configuration(&self) -> bool {
+        self.disable_dynamic_configuration
+    }
+
+    pub fn load_from_config_data_or_default() -> Config {
+        let f = match File::open("/config/data/config.json") {
+            Ok(f) => f,
+            Err(e) => {
+                fx_log_info!("no config found, using defaults: {:?}", e.kind());
+                return Config::default();
+            }
+        };
+
+        Self::load(BufReader::new(f)).unwrap_or_else(|e| {
+            fx_log_err!("unable to load config, using defaults: {:?}", e);
+            Config::default()
+        })
+    }
+
+    fn load(r: impl Read) -> Result<Config, ConfigLoadError> {
+        #[derive(Debug, Deserialize)]
+        #[serde(deny_unknown_fields)]
+        struct ParseConfig {
+            disable_dynamic_configuration: bool,
+        }
+
+        let parse_config = serde_json::from_reader::<_, ParseConfig>(r)?;
+
+        Ok(Config { disable_dynamic_configuration: parse_config.disable_dynamic_configuration })
+    }
+}
+
+#[derive(Debug, Fail)]
+enum ConfigLoadError {
+    #[fail(display = "parse error: {}", _0)]
+    Parse(#[cause] serde_json::Error),
+}
+
+impl From<serde_json::Error> for ConfigLoadError {
+    fn from(e: serde_json::Error) -> Self {
+        Self::Parse(e)
+    }
+}
+
+#[cfg(test)]
+mod tests {
+    use {super::*, matches::assert_matches, serde_json::json};
+
+    fn verify_load(input: serde_json::Value, expected: Config) {
+        assert_eq!(
+            Config::load(input.to_string().as_bytes()).expect("json value to be valid"),
+            expected
+        );
+    }
+
+    #[test]
+    fn test_load_valid_configs() {
+        for val in [true, false].iter() {
+            verify_load(
+                json!({
+                    "disable_dynamic_configuration": *val,
+                }),
+                Config { disable_dynamic_configuration: *val },
+            );
+        }
+    }
+
+    #[test]
+    fn test_load_errors_on_unknown_field() {
+        assert_matches!(
+            Config::load(
+                json!({
+                    "disable_dynamic_configuration": false,
+                    "unknown_field": 3
+                })
+                .to_string()
+                .as_bytes()
+            ),
+            Err(ConfigLoadError::Parse(_))
+        );
+    }
+
+    #[test]
+    fn test_no_config_data_is_default() {
+        assert_eq!(Config::load_from_config_data_or_default(), Config::default());
+    }
+
+    #[test]
+    fn test_default_does_not_disable_dynamic_configuraiton() {
+        assert_eq!(Config::default().disable_dynamic_configuration, false);
+    }
+}
diff --git a/garnet/bin/pkg_resolver/src/main.rs b/garnet/bin/pkg_resolver/src/main.rs
index 7587454..68550257 100644
--- a/garnet/bin/pkg_resolver/src/main.rs
+++ b/garnet/bin/pkg_resolver/src/main.rs
@@ -19,6 +19,7 @@
 
 mod amber_connector;
 mod cache;
+mod config;
 mod experiment;
 mod font_package_manager;
 mod repository_manager;
@@ -58,6 +59,8 @@
 
     let mut executor = fasync::Executor::new().context("error creating executor")?;
 
+    let config = config::Config::load_from_config_data_or_default();
+
     let pkg_cache =
         connect_to_service::<PackageCacheMarker>().context("error connecting to package cache")?;
     let pkgfs_install = connect_to_pkgfs("install").context("error connecting to pkgfs/install")?;
@@ -75,8 +78,11 @@
 
     let font_package_manager = Arc::new(load_font_package_manager());
     let repo_manager = Arc::new(RwLock::new(load_repo_manager(amber_connector, experiments)));
-    let rewrite_manager =
-        Arc::new(RwLock::new(load_rewrite_manager(rewrite_inspect_node, &repo_manager.read())));
+    let rewrite_manager = Arc::new(RwLock::new(load_rewrite_manager(
+        rewrite_inspect_node,
+        &repo_manager.read(),
+        config.disable_dynamic_configuration(),
+    )));
 
     let resolver_cb = {
         // Capture a clone of repo and rewrite manager's Arc so the new client callback has a copy
@@ -200,8 +206,11 @@
 fn load_rewrite_manager(
     node: inspect::Node,
     repo_manager: &RepositoryManager<AmberConnector>,
+    disable_dynamic_configuration: bool,
 ) -> RewriteManager {
-    let builder = RewriteManagerBuilder::new(DYNAMIC_RULES_PATH)
+    let dynamic_rules_path =
+        if disable_dynamic_configuration { None } else { Some(DYNAMIC_RULES_PATH) };
+    let builder = RewriteManagerBuilder::new(dynamic_rules_path)
         .unwrap_or_else(|(builder, err)| {
             if err.kind() != io::ErrorKind::NotFound {
                 fx_log_err!(
diff --git a/garnet/bin/pkg_resolver/src/resolver_service.rs b/garnet/bin/pkg_resolver/src/resolver_service.rs
index 7dd817b..0acb78d 100644
--- a/garnet/bin/pkg_resolver/src/resolver_service.rs
+++ b/garnet/bin/pkg_resolver/src/resolver_service.rs
@@ -452,7 +452,7 @@
             let cache = PackageCache::new(cache_proxy, pkgfs_install, pkgfs_needs);
 
             let dynamic_rule_config = make_rule_config(self.dynamic_rewrite_rules);
-            let rewrite_manager = RewriteManagerBuilder::new(&dynamic_rule_config)
+            let rewrite_manager = RewriteManagerBuilder::new(Some(&dynamic_rule_config))
                 .unwrap()
                 .static_rules(self.static_rewrite_rules)
                 .build();
diff --git a/garnet/bin/pkg_resolver/src/rewrite_manager.rs b/garnet/bin/pkg_resolver/src/rewrite_manager.rs
index 7db50d8..1a0982f 100644
--- a/garnet/bin/pkg_resolver/src/rewrite_manager.rs
+++ b/garnet/bin/pkg_resolver/src/rewrite_manager.rs
@@ -27,7 +27,7 @@
     static_rules: Vec<Rule>,
     dynamic_rules: Vec<Rule>,
     generation: u32,
-    dynamic_rules_path: PathBuf,
+    dynamic_rules_path: Option<PathBuf>,
     inspect: RewriteManagerInspectState,
 }
 
@@ -46,6 +46,8 @@
 pub enum CommitError {
     #[fail(display = "the provided rule set is based on an older generation")]
     TooLate,
+    #[fail(display = "editing rewrite rules is permanently disabled")]
+    DynamicConfigurationDisabled,
 }
 
 impl RewriteManager {
@@ -67,22 +69,22 @@
         url
     }
 
-    fn save(&mut self) -> io::Result<()> {
-        let config = RuleConfig::Version1(std::mem::replace(&mut self.dynamic_rules, vec![]));
+    fn save(dynamic_rules: &mut Vec<Rule>, dynamic_rules_path: &Path) -> io::Result<()> {
+        let config = RuleConfig::Version1(std::mem::replace(dynamic_rules, vec![]));
 
         let result = (|| {
-            let mut temp_path = self.dynamic_rules_path.clone().into_os_string();
+            let mut temp_path = dynamic_rules_path.to_owned().into_os_string();
             temp_path.push(".new");
             let temp_path = PathBuf::from(temp_path);
             {
                 let f = File::create(&temp_path)?;
                 serde_json::to_writer(io::BufWriter::new(f), &config)?;
             };
-            fs::rename(temp_path, &self.dynamic_rules_path)
+            fs::rename(temp_path, dynamic_rules_path)
         })();
 
         let RuleConfig::Version1(rules) = config;
-        self.dynamic_rules = rules;
+        *dynamic_rules = rules;
 
         result
     }
@@ -100,17 +102,21 @@
     /// [RewriteRuleStates] have been applied since `transaction` was cloned from this
     /// [RewriteManager].
     pub fn apply(&mut self, transaction: Transaction) -> Result<(), CommitError> {
-        if self.generation != transaction.generation {
-            Err(CommitError::TooLate)
-        } else {
-            self.dynamic_rules = transaction.dynamic_rules.into();
-            self.generation += 1;
-            // FIXME(kevinwells) synchronous I/O in an async context
-            if let Err(err) = self.save() {
-                fx_log_err!("error while saving dynamic rewrite rules: {}", err);
+        if let Some(ref dynamic_rules_path) = self.dynamic_rules_path {
+            if self.generation != transaction.generation {
+                Err(CommitError::TooLate)
+            } else {
+                self.dynamic_rules = transaction.dynamic_rules.into();
+                self.generation += 1;
+                // FIXME(kevinwells) synchronous I/O in an async context
+                if let Err(err) = Self::save(&mut self.dynamic_rules, dynamic_rules_path) {
+                    fx_log_err!("error while saving dynamic rewrite rules: {}", err);
+                }
+                self.update_inspect_objects();
+                Ok(())
             }
-            self.update_inspect_objects();
-            Ok(())
+        } else {
+            Err(CommitError::DynamicConfigurationDisabled)
         }
     }
 
@@ -187,7 +193,7 @@
 pub struct RewriteManagerBuilder<N> {
     static_rules: Vec<Rule>,
     dynamic_rules: Vec<Rule>,
-    dynamic_rules_path: PathBuf,
+    dynamic_rules_path: Option<PathBuf>,
     inspect_node: N,
 }
 
@@ -196,23 +202,27 @@
     /// provided path. If the provided dynamic rule config file does not exist or is corrupt, this
     /// method returns an [RewriteManagerBuilder] initialized with no rules and configured with the
     /// given dynamic config path.
-    pub fn new<T>(dynamic_rules_path: T) -> Result<Self, (Self, io::Error)>
+    pub fn new<T>(dynamic_rules_path: Option<T>) -> Result<Self, (Self, io::Error)>
     where
         T: Into<PathBuf>,
     {
         let mut builder = RewriteManagerBuilder {
             static_rules: vec![],
             dynamic_rules: vec![],
-            dynamic_rules_path: dynamic_rules_path.into(),
+            dynamic_rules_path: dynamic_rules_path.map(|p| p.into()),
             inspect_node: UnsetInspectNode,
         };
 
-        match Self::load_rules(&builder.dynamic_rules_path) {
-            Ok(rules) => {
-                builder.dynamic_rules = rules;
-                Ok(builder)
+        if let Some(ref path) = builder.dynamic_rules_path {
+            match Self::load_rules(path) {
+                Ok(rules) => {
+                    builder.dynamic_rules = rules;
+                    Ok(builder)
+                }
+                Err(err) => Err((builder, err)),
             }
-            Err(err) => Err((builder, err)),
+        } else {
+            Ok(builder)
         }
     }
 
@@ -292,10 +302,9 @@
             dynamic_rules_node: self.inspect_node.create_child("dynamic_rules"),
             dynamic_rules_states: vec![],
             generation_property: self.inspect_node.create_uint("generation", 0),
-            dynamic_rules_path_property: self.inspect_node.create_string(
-                "dynamic_rules_path",
-                &self.dynamic_rules_path.display().to_string(),
-            ),
+            dynamic_rules_path_property: self
+                .inspect_node
+                .create_string("dynamic_rules_path", &format!("{:?}", self.dynamic_rules_path)),
             node: self.inspect_node,
         };
 
@@ -318,7 +327,10 @@
 
 #[cfg(test)]
 pub(crate) mod tests {
-    use {super::*, failure::Error, fuchsia_inspect::assert_inspect_tree, serde_json::json};
+    use {
+        super::*, failure::Error, fuchsia_inspect::assert_inspect_tree, matches::assert_matches,
+        serde_json::json,
+    };
 
     macro_rules! rule {
         ($host_match:expr => $host_replacement:expr,
@@ -352,7 +364,7 @@
     fn test_empty_configs() {
         let config = make_rule_config(vec![]);
 
-        let manager = RewriteManagerBuilder::new(&config)
+        let manager = RewriteManagerBuilder::new(Some(&config))
             .unwrap()
             .static_rules_path(&config)
             .unwrap()
@@ -368,7 +380,7 @@
 
         let dynamic_config = make_rule_config(vec![]);
         let static_config = make_rule_config(rules.clone());
-        let manager = RewriteManagerBuilder::new(&dynamic_config)
+        let manager = RewriteManagerBuilder::new(Some(&dynamic_config))
             .unwrap()
             .static_rules_path(&static_config)
             .unwrap()
@@ -383,7 +395,7 @@
         let rules = vec![rule!("fuchsia.com" => "fuchsia.com", "/rolldice" => "/rolldice")];
 
         let dynamic_config = make_rule_config(rules.clone());
-        let manager = RewriteManagerBuilder::new(&dynamic_config).unwrap().build();
+        let manager = RewriteManagerBuilder::new(Some(&dynamic_config)).unwrap().build();
 
         assert_eq!(manager.list_static().cloned().collect::<Vec<_>>(), vec![]);
         assert_eq!(manager.list().cloned().collect::<Vec<_>>(), rules);
@@ -403,7 +415,7 @@
                 })
             )
         });
-        let (builder, _) = RewriteManagerBuilder::new(&dynamic_config)
+        let (builder, _) = RewriteManagerBuilder::new(Some(&dynamic_config))
             .unwrap()
             .static_rules_path(&static_config)
             .unwrap_err();
@@ -419,7 +431,7 @@
         let rule = rule!("test.com" => "test.com", "/a" => "/b");
 
         {
-            let (builder, _) = RewriteManagerBuilder::new(&dynamic_config).unwrap_err();
+            let (builder, _) = RewriteManagerBuilder::new(Some(&dynamic_config)).unwrap_err();
             let mut manager = builder.build();
 
             assert_eq!(manager.list().cloned().collect::<Vec<_>>(), vec![]);
@@ -432,7 +444,7 @@
         }
 
         // Verify the dynamic config file is no longer corrupt and contains the newly added rule.
-        let manager = RewriteManagerBuilder::new(&dynamic_config).unwrap().build();
+        let manager = RewriteManagerBuilder::new(Some(&dynamic_config)).unwrap().build();
         assert_eq!(manager.list().cloned().collect::<Vec<_>>(), vec![rule]);
     }
 
@@ -444,7 +456,7 @@
         ];
 
         let dynamic_config = make_rule_config(rules);
-        let manager = RewriteManagerBuilder::new(&dynamic_config).unwrap().build();
+        let manager = RewriteManagerBuilder::new(Some(&dynamic_config)).unwrap().build();
 
         let url: PkgUrl = "fuchsia-pkg://fuchsia.com/c".parse().unwrap();
         assert_eq!(manager.rewrite(url.clone()), url);
@@ -458,7 +470,7 @@
         ];
 
         let dynamic_config = make_rule_config(rules);
-        let manager = RewriteManagerBuilder::new(&dynamic_config).unwrap().build();
+        let manager = RewriteManagerBuilder::new(Some(&dynamic_config)).unwrap().build();
 
         let url = "fuchsia-pkg://fuchsia.com/package".parse().unwrap();
         assert_eq!(manager.rewrite(url), "fuchsia-pkg://fuchsia.com/remapped".parse().unwrap());
@@ -472,7 +484,7 @@
         let static_config = make_rule_config(vec![
             rule!("fuchsia.com" => "fuchsia.com", "/package" => "/incorrect"),
         ]);
-        let manager = RewriteManagerBuilder::new(&dynamic_config)
+        let manager = RewriteManagerBuilder::new(Some(&dynamic_config))
             .unwrap()
             .static_rules_path(&static_config)
             .unwrap()
@@ -491,7 +503,7 @@
         let static_config = make_rule_config(static_rules);
         let dynamic_config = make_rule_config(dynamic_rules);
 
-        let manager = RewriteManagerBuilder::new(&dynamic_config)
+        let manager = RewriteManagerBuilder::new(Some(&dynamic_config))
             .unwrap()
             .static_rules_path(&static_config)
             .unwrap()
@@ -507,7 +519,7 @@
         let override_rule = rule!("fuchsia.com" => "fuchsia.com", "/a" => "/c");
         let dynamic_config =
             make_rule_config(vec![rule!("fuchsia.com" => "fuchsia.com", "/a" => "/b")]);
-        let mut manager = RewriteManagerBuilder::new(&dynamic_config).unwrap().build();
+        let mut manager = RewriteManagerBuilder::new(Some(&dynamic_config)).unwrap().build();
 
         let mut transaction = manager.transaction();
         transaction.add(override_rule.clone());
@@ -529,7 +541,7 @@
 
         let rules = vec![existing_rule.clone()];
         let dynamic_config = make_rule_config(rules.clone());
-        let mut manager = RewriteManagerBuilder::new(&dynamic_config).unwrap().build();
+        let mut manager = RewriteManagerBuilder::new(Some(&dynamic_config)).unwrap().build();
         assert_eq!(manager.list().cloned().collect::<Vec<_>>(), rules);
 
         // Fork the existing state, add a rule, and verify both instances are distinct
@@ -544,7 +556,7 @@
         assert_eq!(manager.list().cloned().collect::<Vec<_>>(), new_rules);
 
         // Ensure new rules are persisted to the dynamic config file
-        let manager = RewriteManagerBuilder::new(&dynamic_config).unwrap().build();
+        let manager = RewriteManagerBuilder::new(Some(&dynamic_config)).unwrap().build();
         assert_eq!(manager.list().cloned().collect::<Vec<_>>(), new_rules);
     }
 
@@ -556,7 +568,7 @@
         ];
 
         let dynamic_config = make_rule_config(rules.clone());
-        let mut manager = RewriteManagerBuilder::new(&dynamic_config).unwrap().build();
+        let mut manager = RewriteManagerBuilder::new(Some(&dynamic_config)).unwrap().build();
         assert_eq!(manager.list().cloned().collect::<Vec<_>>(), rules);
 
         let mut transaction = manager.transaction();
@@ -567,7 +579,7 @@
         assert_eq!(manager.apply(transaction).unwrap(), ());
         assert_eq!(manager.list().cloned().collect::<Vec<_>>(), vec![]);
 
-        let manager = RewriteManagerBuilder::new(&dynamic_config).unwrap().build();
+        let manager = RewriteManagerBuilder::new(Some(&dynamic_config)).unwrap().build();
         assert_eq!(manager.list().cloned().collect::<Vec<_>>(), vec![]);
     }
 
@@ -583,7 +595,7 @@
             vec![rule!("example.com" => "example.org", "/this_throwdice" => "/that_throwdice")];
         let static_config = make_rule_config(static_rules.clone());
 
-        let _manager = RewriteManagerBuilder::new(&dynamic_config)
+        let _manager = RewriteManagerBuilder::new(Some(&dynamic_config))
             .unwrap()
             .static_rules_path(&static_config)
             .unwrap()
@@ -602,7 +614,7 @@
                             path_prefix_replacement: "/that_rolldice",
                         },
                     },
-                    dynamic_rules_path: dynamic_config.to_str().unwrap().to_string(),
+                    dynamic_rules_path: format!("{:?}", Some(&*dynamic_config)),
                     static_rules: {
                         "0": {
                             host_match: "example.com",
@@ -618,18 +630,39 @@
     }
 
     #[test]
-    fn test_transaction_updates_inspect() {
+    fn test_inspect_rewrite_manager_no_dynamic_rules_path() {
         let inspector = fuchsia_inspect::Inspector::new();
         let node = inspector.root().create_child("rewrite_manager");
-        let dynamic_config = make_rule_config(vec![]);
-        let mut manager =
-            RewriteManagerBuilder::new(&dynamic_config).unwrap().inspect_node(node).build();
+
+        let _manager =
+            RewriteManagerBuilder::new(Option::<&Path>::None).unwrap().inspect_node(node).build();
+
         assert_inspect_tree!(
             inspector,
             root: {
                 rewrite_manager: {
                     dynamic_rules: {},
-                    dynamic_rules_path: dynamic_config.to_str().unwrap().to_string(),
+                    dynamic_rules_path: format!("{:?}", Option::<&Path>::None),
+                    static_rules: {},
+                    generation: 0u64,
+                }
+            }
+        );
+    }
+
+    #[test]
+    fn test_transaction_updates_inspect() {
+        let inspector = fuchsia_inspect::Inspector::new();
+        let node = inspector.root().create_child("rewrite_manager");
+        let dynamic_config = make_rule_config(vec![]);
+        let mut manager =
+            RewriteManagerBuilder::new(Some(&dynamic_config)).unwrap().inspect_node(node).build();
+        assert_inspect_tree!(
+            inspector,
+            root: {
+                rewrite_manager: {
+                    dynamic_rules: {},
+                    dynamic_rules_path: format!("{:?}", Some(&*dynamic_config)),
                     static_rules: {},
                     generation: 0u64,
                 }
@@ -653,11 +686,56 @@
                             path_prefix_replacement: "/that_rolldice/",
                         },
                     },
-                    dynamic_rules_path: dynamic_config.to_str().unwrap().to_string(),
+                    dynamic_rules_path: format!("{:?}", Some(&*dynamic_config)),
                     static_rules: {},
                     generation: 1u64,
                 }
             }
         );
     }
+
+    #[test]
+    fn test_no_dynamic_rules_if_no_dynamic_rules_path() {
+        let manager = RewriteManagerBuilder::new(Option::<&Path>::None).unwrap().build();
+
+        assert_eq!(manager.list().cloned().collect::<Vec<_>>(), vec![]);
+    }
+
+    #[test]
+    fn test_same_dynamic_rules_if_apply_fails() {
+        let dynamic_config = make_rule_config(vec![]);
+        let rule0 = rule!("test0.com" => "test0.com", "/a" => "/b");
+        let rule1 = rule!("test1.com" => "test1.com", "/a" => "/b");
+        let mut manager = RewriteManagerBuilder::new(Some(&dynamic_config)).unwrap().build();
+        assert_eq!(manager.list().collect::<Vec<_>>(), Vec::<&Rule>::new());
+
+        // transaction0 adds a dynamic rule
+        let mut transaction0 = manager.transaction();
+        let mut transaction1 = manager.transaction();
+        transaction0.add(rule0.clone());
+        manager.apply(transaction0).unwrap();
+        assert_eq!(manager.list().cloned().collect::<Vec<_>>(), vec![rule0.clone()]);
+
+        // transaction1 fails to apply b/c it was created before transaction0 was applied
+        // the dynamic rewrite rules should be unchanged
+        transaction1.add(rule1.clone());
+        assert_matches!(manager.apply(transaction1), Err(CommitError::TooLate));
+        assert_eq!(manager.list().cloned().collect::<Vec<_>>(), vec![rule0.clone()]);
+
+        // transaction2 applies the rule from transaction1
+        let mut transaction2 = manager.transaction();
+        transaction2.add(rule1.clone());
+        manager.apply(transaction2).unwrap();
+        assert_eq!(manager.list().cloned().collect::<Vec<_>>(), vec![rule1.clone(), rule0.clone()]);
+    }
+
+    #[test]
+    fn test_apply_fails_with_no_change_if_no_dynamic_rules_path() {
+        let mut manager = RewriteManagerBuilder::new(Option::<&Path>::None).unwrap().build();
+        let mut transaction = manager.transaction();
+        transaction.add(rule!("test0.com" => "test0.com", "/a" => "/b"));
+
+        assert_matches!(manager.apply(transaction), Err(CommitError::DynamicConfigurationDisabled));
+        assert_eq!(manager.list().cloned().collect::<Vec<_>>(), vec![]);
+    }
 }
diff --git a/garnet/bin/pkg_resolver/src/rewrite_service.rs b/garnet/bin/pkg_resolver/src/rewrite_service.rs
index 9b99237..f84cb23 100644
--- a/garnet/bin/pkg_resolver/src/rewrite_service.rs
+++ b/garnet/bin/pkg_resolver/src/rewrite_service.rs
@@ -118,6 +118,9 @@
                             let status = match state.write().apply(transaction) {
                                 Ok(()) => Status::OK,
                                 Err(CommitError::TooLate) => Status::UNAVAILABLE,
+                                Err(CommitError::DynamicConfigurationDisabled) => {
+                                    Status::ACCESS_DENIED
+                                }
                             };
                             responder.send(status.into_raw())?;
                             return Ok(());
@@ -237,7 +240,7 @@
             rule!("fuchsia.com" => "static.fuchsia.com", "/4" => "/4"),
         ];
         let state = Arc::new(RwLock::new(
-            RewriteManagerBuilder::new(&dynamic_config)
+            RewriteManagerBuilder::new(Some(&dynamic_config))
                 .unwrap()
                 .static_rules(static_rules.clone())
                 .build(),
@@ -259,7 +262,7 @@
             rule!("fuchsia.com" => "static.fuchsia.com", "/4" => "/4"),
         ];
         let state = Arc::new(RwLock::new(
-            RewriteManagerBuilder::new(&dynamic_config)
+            RewriteManagerBuilder::new(Some(&dynamic_config))
                 .unwrap()
                 .static_rules(static_rules.clone())
                 .build(),
@@ -275,8 +278,9 @@
             rule!("fuchsia.com" => "fuchsia.com", "/rolldice/" => "/rolldice/"),
         ];
         let dynamic_config = make_rule_config(rules.clone());
-        let state =
-            Arc::new(RwLock::new(RewriteManagerBuilder::new(&dynamic_config).unwrap().build()));
+        let state = Arc::new(RwLock::new(
+            RewriteManagerBuilder::new(Some(&dynamic_config)).unwrap().build(),
+        ));
         let mut service = RewriteService::new(state.clone());
 
         let (client, request_stream) = create_proxy_and_stream::<EditTransactionMarker>().unwrap();
@@ -297,7 +301,10 @@
         let dynamic_config = make_rule_config(dynamic_rules.clone());
         let static_rules = vec![rule!("fuchsia.com" => "static.fuchsia.com", "/" => "/")];
         let state = Arc::new(RwLock::new(
-            RewriteManagerBuilder::new(&dynamic_config).unwrap().static_rules(static_rules).build(),
+            RewriteManagerBuilder::new(Some(&dynamic_config))
+                .unwrap()
+                .static_rules(static_rules)
+                .build(),
         ));
         let mut service = RewriteService::new(state.clone());
 
@@ -328,8 +335,9 @@
             rule!("fuchsia.com" => "fuchsia.com", "/rolldice/" => "/rolldice/"),
         ];
         let dynamic_config = make_rule_config(rules.clone());
-        let state =
-            Arc::new(RwLock::new(RewriteManagerBuilder::new(&dynamic_config).unwrap().build()));
+        let state = Arc::new(RwLock::new(
+            RewriteManagerBuilder::new(Some(&dynamic_config)).unwrap().build(),
+        ));
         let mut service = RewriteService::new(state.clone());
 
         let client1 = {
@@ -371,8 +379,9 @@
     #[fasync::run_until_stalled(test)]
     async fn test_concurrent_list_and_edit() {
         let dynamic_config = make_rule_config(vec![]);
-        let state =
-            Arc::new(RwLock::new(RewriteManagerBuilder::new(&dynamic_config).unwrap().build()));
+        let state = Arc::new(RwLock::new(
+            RewriteManagerBuilder::new(Some(&dynamic_config)).unwrap().build(),
+        ));
         let mut service = RewriteService::new(state.clone());
 
         assert_eq!(list_rules(state.clone()).await, vec![]);
@@ -412,7 +421,7 @@
             rule!("fuchsia.com" => "fuchsia.com", "/identity" => "/identity"),
         ];
         let state = Arc::new(RwLock::new(
-            RewriteManagerBuilder::new(&dynamic_config)
+            RewriteManagerBuilder::new(Some(&dynamic_config))
                 .unwrap()
                 .static_rules(static_rules.clone())
                 .build(),
@@ -439,8 +448,9 @@
     #[fasync::run_until_stalled(test)]
     async fn test_rewrite_rejects_invalid_inputs() {
         let dynamic_config = make_rule_config(vec![]);
-        let state =
-            Arc::new(RwLock::new(RewriteManagerBuilder::new(&dynamic_config).unwrap().build()));
+        let state = Arc::new(RwLock::new(
+            RewriteManagerBuilder::new(Some(&dynamic_config)).unwrap().build(),
+        ));
         let service = RewriteService::new(state.clone());
 
         for url in &["not-fuchsia-pkg://fuchsia.com/test", "fuchsia-pkg://fuchsia.com/a*"] {
@@ -452,8 +462,9 @@
     async fn test_concurrent_rewrite_and_edit() {
         let dynamic_config =
             make_rule_config(vec![rule!("fuchsia.com" => "fuchsia.com", "/a" => "/b")]);
-        let state =
-            Arc::new(RwLock::new(RewriteManagerBuilder::new(&dynamic_config).unwrap().build()));
+        let state = Arc::new(RwLock::new(
+            RewriteManagerBuilder::new(Some(&dynamic_config)).unwrap().build(),
+        ));
         let mut service = RewriteService::new(state.clone());
 
         let (edit_client, request_stream) =
@@ -483,8 +494,9 @@
     #[fasync::run_until_stalled(test)]
     async fn test_enables_amber_source() {
         let dynamic_config = make_rule_config(vec![]);
-        let state =
-            Arc::new(RwLock::new(RewriteManagerBuilder::new(&dynamic_config).unwrap().build()));
+        let state = Arc::new(RwLock::new(
+            RewriteManagerBuilder::new(Some(&dynamic_config)).unwrap().build(),
+        ));
         let mut service = RewriteService::new(state.clone());
 
         let (client, request_stream) = create_proxy_and_stream::<EditTransactionMarker>().unwrap();
@@ -522,8 +534,9 @@
         let rules = vec![rule!("fuchsia.com" => "enabled.fuchsia.com", "/" => "/")];
 
         let dynamic_config = make_rule_config(rules);
-        let state =
-            Arc::new(RwLock::new(RewriteManagerBuilder::new(&dynamic_config).unwrap().build()));
+        let state = Arc::new(RwLock::new(
+            RewriteManagerBuilder::new(Some(&dynamic_config)).unwrap().build(),
+        ));
         let mut service = RewriteService::new(state.clone());
 
         let (client, request_stream) = create_proxy_and_stream::<EditTransactionMarker>().unwrap();
@@ -537,4 +550,19 @@
         client.reset_all().unwrap();
         assert_yields_status!(client.commit(), Status::OK);
     }
+
+    #[fasync::run_until_stalled(test)]
+    async fn test_transaction_commit_fails_if_no_dynamic_rules_path() {
+        let state = Arc::new(RwLock::new(
+            RewriteManagerBuilder::new(Option::<&std::path::Path>::None).unwrap().build(),
+        ));
+        let mut service = RewriteService::new(state);
+
+        let (client, request_stream) = create_proxy_and_stream::<EditTransactionMarker>().unwrap();
+        service.serve_edit_transaction(request_stream);
+
+        let status = Status::from_raw(client.commit().await.unwrap());
+
+        assert_eq!(status, Status::ACCESS_DENIED);
+    }
 }
diff --git a/garnet/tests/pkg-resolver/BUILD.gn b/garnet/tests/pkg-resolver/BUILD.gn
index c4c1eda..d108979 100644
--- a/garnet/tests/pkg-resolver/BUILD.gn
+++ b/garnet/tests/pkg-resolver/BUILD.gn
@@ -25,13 +25,18 @@
     "//garnet/public/rust/fuchsia-zircon",
     "//sdk/fidl/fuchsia.amber:fuchsia.amber-rustc",
     "//sdk/fidl/fuchsia.pkg:fuchsia.pkg-rustc",
+    "//sdk/fidl/fuchsia.pkg.rewrite:fuchsia.pkg.rewrite-rustc",
     "//sdk/fidl/fuchsia.sys:fuchsia.sys-rustc",
+    "//src/sys/lib/fuchsia_url_rewrite",
     "//third_party/rust_crates:failure",
     "//third_party/rust_crates:futures-preview",
     "//third_party/rust_crates:hyper",
     "//third_party/rust_crates:matches",
     "//third_party/rust_crates:openat",
     "//third_party/rust_crates:parking_lot",
+    "//third_party/rust_crates:serde",
+    "//third_party/rust_crates:serde_derive",
+    "//third_party/rust_crates:serde_json",
     "//third_party/rust_crates:tempfile",
     "//third_party/rust_crates:void",
     "//zircon/system/fidl/fuchsia-io:fuchsia-io-rustc",
diff --git a/garnet/tests/pkg-resolver/src/dynamic_rewrite_disabled.rs b/garnet/tests/pkg-resolver/src/dynamic_rewrite_disabled.rs
new file mode 100644
index 0000000..14c1265
--- /dev/null
+++ b/garnet/tests/pkg-resolver/src/dynamic_rewrite_disabled.rs
@@ -0,0 +1,215 @@
+// Copyright 2019 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.
+
+/// This module tests pkg_resolver's RewriteManager when
+/// dynamic rewrite rules have been disabled.
+use {
+    crate::{DirOrProxy, Mounts, TestEnv},
+    fidl::endpoints::RequestStream,
+    fidl_fuchsia_io::{
+        DirectoryObject, DirectoryProxy, DirectoryRequest, DirectoryRequestStream,
+        OPEN_FLAG_DESCRIBE,
+    },
+    fidl_fuchsia_pkg_rewrite::EngineProxy as RewriteEngineProxy,
+    fuchsia_async as fasync,
+    fuchsia_url_rewrite::{Rule, RuleConfig},
+    fuchsia_zircon::Status,
+    futures::{
+        future::{BoxFuture, FutureExt},
+        stream::StreamExt,
+    },
+    parking_lot::Mutex,
+    serde_derive::Serialize,
+    std::{collections::HashMap, convert::TryInto, fs::File, io::BufWriter, sync::Arc},
+};
+
+#[derive(Serialize)]
+struct Config {
+    disable_dynamic_configuration: bool,
+}
+
+impl Mounts {
+    fn add_dynamic_rewrite_rules(&self, rule_config: &RuleConfig) {
+        if let DirOrProxy::Dir(ref d) = self.pkg_resolver_data {
+            let f = File::create(d.path().join("rewrites.json")).unwrap();
+            serde_json::to_writer(BufWriter::new(f), rule_config).unwrap();
+        } else {
+            panic!("not supported");
+        }
+    }
+    fn add_config(&self, config: &Config) {
+        if let DirOrProxy::Dir(ref d) = self.pkg_resolver_config_data {
+            let f = File::create(d.path().join("config.json")).unwrap();
+            serde_json::to_writer(BufWriter::new(f), &config).unwrap();
+        } else {
+            panic!("not supported");
+        }
+    }
+}
+
+fn make_rule_config(rule: &Rule) -> RuleConfig {
+    RuleConfig::Version1(vec![rule.clone()])
+}
+
+fn make_rule() -> Rule {
+    Rule::new("example.com", "example.com", "/", "/").unwrap()
+}
+
+async fn get_rules(rewrite_engine: &RewriteEngineProxy) -> Vec<Rule> {
+    let (rule_iterator, rule_iterator_server) =
+        fidl::endpoints::create_proxy().expect("create rule iterator proxy");
+    rewrite_engine.list(rule_iterator_server).expect("list rules");
+    let mut ret = vec![];
+    loop {
+        let rules = rule_iterator.next().await.expect("advance rule iterator");
+        if rules.is_empty() {
+            return ret;
+        }
+        ret.extend(rules.into_iter().map(|r| r.try_into().unwrap()))
+    }
+}
+
+#[fasync::run_singlethreaded(test)]
+async fn load_dynamic_rules() {
+    let mounts = Mounts::new();
+    let rule = make_rule();
+    mounts.add_dynamic_rewrite_rules(&make_rule_config(&rule));
+    mounts.add_config(&Config { disable_dynamic_configuration: false });
+    let env = TestEnv::new_with_mounts(mounts);
+
+    assert_eq!(get_rules(&env.proxies.rewrite_engine).await, vec![rule]);
+
+    env.stop().await;
+}
+
+#[fasync::run_singlethreaded(test)]
+async fn no_load_dynamic_rules_if_disabled() {
+    let mounts = Mounts::new();
+    mounts.add_dynamic_rewrite_rules(&make_rule_config(&make_rule()));
+    mounts.add_config(&Config { disable_dynamic_configuration: true });
+    let env = TestEnv::new_with_mounts(mounts);
+
+    assert_eq!(get_rules(&env.proxies.rewrite_engine).await, vec![]);
+
+    env.stop().await;
+}
+
+#[fasync::run_singlethreaded(test)]
+async fn commit_transaction_succeeds() {
+    let env = TestEnv::new();
+
+    let (edit_transaction, edit_transaction_server) = fidl::endpoints::create_proxy().unwrap();
+    env.proxies.rewrite_engine.start_edit_transaction(edit_transaction_server).unwrap();
+    let rule = make_rule();
+    Status::ok(edit_transaction.add(&mut rule.clone().into()).await.unwrap()).unwrap();
+
+    assert_eq!(Status::from_raw(edit_transaction.commit().await.unwrap()), Status::OK);
+    assert_eq!(get_rules(&env.proxies.rewrite_engine).await, vec![rule]);
+
+    env.stop().await;
+}
+
+#[fasync::run_singlethreaded(test)]
+async fn commit_transaction_fails_if_disabled() {
+    let mounts = Mounts::new();
+    mounts.add_config(&Config { disable_dynamic_configuration: true });
+    let env = TestEnv::new_with_mounts(mounts);
+
+    let (edit_transaction, edit_transaction_server) = fidl::endpoints::create_proxy().unwrap();
+    env.proxies.rewrite_engine.start_edit_transaction(edit_transaction_server).unwrap();
+    Status::ok(edit_transaction.add(&mut make_rule().into()).await.unwrap()).unwrap();
+
+    assert_eq!(Status::from_raw(edit_transaction.commit().await.unwrap()), Status::ACCESS_DENIED);
+    assert_eq!(get_rules(&env.proxies.rewrite_engine).await, vec![]);
+
+    env.stop().await;
+}
+
+type OpenCounter = Arc<Mutex<HashMap<String, u64>>>;
+
+fn handle_directory_request_stream(
+    mut stream: DirectoryRequestStream,
+    open_counts: OpenCounter,
+) -> BoxFuture<'static, ()> {
+    async move {
+        while let Some(req) = stream.next().await {
+            handle_directory_request(req.unwrap(), Arc::clone(&open_counts)).await;
+        }
+    }
+        .boxed()
+}
+
+async fn handle_directory_request(req: DirectoryRequest, open_counts: OpenCounter) {
+    match req {
+        DirectoryRequest::Clone { flags, object, control_handle: _control_handle } => {
+            let stream = DirectoryRequestStream::from_channel(
+                fasync::Channel::from_channel(object.into_channel()).unwrap(),
+            );
+            describe_dir(flags, &stream);
+            fasync::spawn(handle_directory_request_stream(stream, Arc::clone(&open_counts)));
+        }
+        DirectoryRequest::Open {
+            flags: _flags,
+            mode: _mode,
+            path,
+            object: _object,
+            control_handle: _control_handle,
+        } => {
+            *open_counts.lock().entry(path).or_insert(0) += 1;
+        }
+        other => panic!("unhandled request type: {:?}", other),
+    }
+}
+
+fn describe_dir(flags: u32, stream: &DirectoryRequestStream) {
+    let ch = stream.control_handle();
+    if flags & OPEN_FLAG_DESCRIBE != 0 {
+        let mut ni = fidl_fuchsia_io::NodeInfo::Directory(DirectoryObject);
+        ch.send_on_open_(Status::OK.into_raw(), Some(fidl::encoding::OutOfLine(&mut ni)))
+            .expect("send_on_open");
+    }
+}
+
+fn spawn_directory_handler() -> (DirectoryProxy, OpenCounter) {
+    let (proxy, stream) =
+        fidl::endpoints::create_proxy_and_stream::<fidl_fuchsia_io::DirectoryMarker>().unwrap();
+    let open_counts = Arc::new(Mutex::new(HashMap::<String, u64>::new()));
+    fasync::spawn(handle_directory_request_stream(stream, Arc::clone(&open_counts)));
+    (proxy, open_counts)
+}
+
+#[fasync::run_singlethreaded(test)]
+async fn attempt_to_open_persisted_dynamic_rules() {
+    let (proxy, open_counts) = spawn_directory_handler();
+    let mounts = Mounts {
+        pkg_resolver_data: DirOrProxy::Proxy(proxy),
+        pkg_resolver_config_data: DirOrProxy::Dir(tempfile::tempdir().expect("/tmp to exist")),
+    };
+    let env = TestEnv::new_with_mounts(mounts);
+
+    // Waits for pkg_resolver to be initialized
+    get_rules(&env.proxies.rewrite_engine).await;
+
+    assert_eq!(open_counts.lock().get("rewrites.json"), Some(&1));
+
+    env.stop().await;
+}
+
+#[fasync::run_singlethreaded(test)]
+async fn no_attempt_to_open_persisted_dynamic_rules_if_disabled() {
+    let (proxy, open_counts) = spawn_directory_handler();
+    let mounts = Mounts {
+        pkg_resolver_data: DirOrProxy::Proxy(proxy),
+        pkg_resolver_config_data: DirOrProxy::Dir(tempfile::tempdir().expect("/tmp to exist")),
+    };
+    mounts.add_config(&Config { disable_dynamic_configuration: true });
+    let env = TestEnv::new_with_mounts(mounts);
+
+    // Waits for pkg_resolver to be initialized
+    get_rules(&env.proxies.rewrite_engine).await;
+
+    assert_eq!(open_counts.lock().get("rewrites.json"), None);
+
+    env.stop().await;
+}
diff --git a/garnet/tests/pkg-resolver/src/lib.rs b/garnet/tests/pkg-resolver/src/lib.rs
index 0364bb9..9c0f664 100644
--- a/garnet/tests/pkg-resolver/src/lib.rs
+++ b/garnet/tests/pkg-resolver/src/lib.rs
@@ -8,12 +8,15 @@
     failure::Error,
     fidl::endpoints::ClientEnd,
     fidl_fuchsia_amber::ControlMarker as AmberMarker,
-    fidl_fuchsia_io::{DirectoryMarker, DirectoryProxy},
+    fidl_fuchsia_io::{DirectoryMarker, DirectoryProxy, CLONE_FLAG_SAME_RIGHTS},
     fidl_fuchsia_pkg::{
         ExperimentToggle as Experiment, PackageCacheMarker, PackageResolverAdminMarker,
         PackageResolverAdminProxy, PackageResolverMarker, PackageResolverProxy,
         RepositoryManagerMarker, RepositoryManagerProxy, UpdatePolicy,
     },
+    fidl_fuchsia_pkg_rewrite::{
+        EngineMarker as RewriteEngineMarker, EngineProxy as RewriteEngineProxy,
+    },
     fidl_fuchsia_sys::LauncherProxy,
     fuchsia_async as fasync,
     fuchsia_component::{
@@ -21,10 +24,13 @@
         server::{NestedEnvironment, ServiceFs},
     },
     fuchsia_pkg_testing::{pkgfs::TestPkgFs, Package, PackageBuilder},
-    fuchsia_zircon::Status,
+    fuchsia_zircon::{self as zx, Status},
     futures::prelude::*,
+    std::fs::File,
+    tempfile::TempDir,
 };
 
+mod dynamic_rewrite_disabled;
 mod resolve_propagates_pkgfs_failure;
 mod resolve_recovers_from_http_errors;
 mod resolve_succeeds;
@@ -39,6 +45,56 @@
     }
 }
 
+struct Mounts {
+    pkg_resolver_data: DirOrProxy,
+    pkg_resolver_config_data: DirOrProxy,
+}
+
+enum DirOrProxy {
+    Dir(TempDir),
+    Proxy(DirectoryProxy),
+}
+
+trait AppBuilderExt {
+    fn add_dir_or_proxy_to_namespace(
+        self,
+        path: impl Into<String>,
+        dir_or_proxy: &DirOrProxy,
+    ) -> Self;
+}
+
+impl AppBuilderExt for AppBuilder {
+    fn add_dir_or_proxy_to_namespace(
+        self,
+        path: impl Into<String>,
+        dir_or_proxy: &DirOrProxy,
+    ) -> Self {
+        match dir_or_proxy {
+            DirOrProxy::Dir(d) => {
+                self.add_dir_to_namespace(path.into(), File::open(d.path()).unwrap()).unwrap()
+            }
+            DirOrProxy::Proxy(p) => {
+                self.add_handle_to_namespace(path.into(), clone_directory_proxy(p))
+            }
+        }
+    }
+}
+
+fn clone_directory_proxy(proxy: &DirectoryProxy) -> zx::Handle {
+    let (client, server) = fidl::endpoints::create_endpoints().unwrap();
+    proxy.clone(CLONE_FLAG_SAME_RIGHTS, server).unwrap();
+    client.into()
+}
+
+impl Mounts {
+    fn new() -> Self {
+        Self {
+            pkg_resolver_data: DirOrProxy::Dir(tempfile::tempdir().expect("/tmp to exist")),
+            pkg_resolver_config_data: DirOrProxy::Dir(tempfile::tempdir().expect("/tmp to exist")),
+        }
+    }
+}
+
 struct Apps {
     _amber: App,
     _pkg_cache: App,
@@ -49,6 +105,7 @@
     resolver_admin: PackageResolverAdminProxy,
     resolver: PackageResolverProxy,
     repo_manager: RepositoryManagerProxy,
+    rewrite_engine: RewriteEngineProxy,
 }
 
 struct TestEnv<P = TestPkgFs> {
@@ -56,6 +113,7 @@
     env: NestedEnvironment,
     apps: Apps,
     proxies: Proxies,
+    _mounts: Mounts,
 }
 
 impl TestEnv<TestPkgFs> {
@@ -63,6 +121,10 @@
         Self::new_with_pkg_fs(TestPkgFs::start(None).expect("pkgfs to start"))
     }
 
+    fn new_with_mounts(mounts: Mounts) -> Self {
+        Self::new_with_pkg_fs_and_mounts(TestPkgFs::start(None).expect("pkgfs to start"), mounts)
+    }
+
     async fn stop(self) {
         // Tear down the environment in reverse order, ending with the storage.
         drop(self.proxies);
@@ -74,6 +136,10 @@
 
 impl<P: PkgFs> TestEnv<P> {
     fn new_with_pkg_fs(pkgfs: P) -> Self {
+        Self::new_with_pkg_fs_and_mounts(pkgfs, Mounts::new())
+    }
+
+    fn new_with_pkg_fs_and_mounts(pkgfs: P, mounts: Mounts) -> Self {
         let mut amber =
             AppBuilder::new("fuchsia-pkg://fuchsia.com/pkg-resolver-tests#meta/amber.cmx")
                 .add_handle_to_namespace(
@@ -95,7 +161,9 @@
         .add_handle_to_namespace(
             "/pkgfs".to_owned(),
             pkgfs.root_dir_client_end().expect("pkgfs dir to open").into(),
-        );
+        )
+        .add_dir_or_proxy_to_namespace("/data", &mounts.pkg_resolver_data)
+        .add_dir_or_proxy_to_namespace("/config/data", &mounts.pkg_resolver_config_data);
 
         let mut fs = ServiceFs::new();
         fs.add_proxy_service::<fidl_fuchsia_net::NameLookupMarker, _>()
@@ -131,6 +199,9 @@
         let repo_manager_proxy = env
             .connect_to_service::<RepositoryManagerMarker>()
             .expect("connect to repository manager");
+        let rewrite_engine_proxy = pkg_resolver
+            .connect_to_service::<RewriteEngineMarker>()
+            .expect("connect to rewrite engine");
 
         Self {
             env,
@@ -140,7 +211,9 @@
                 resolver: resolver_proxy,
                 resolver_admin: resolver_admin_proxy,
                 repo_manager: repo_manager_proxy,
+                rewrite_engine: rewrite_engine_proxy,
             },
+            _mounts: mounts,
         }
     }
 
diff --git a/sdk/fidl/fuchsia.pkg.rewrite/rewrite.fidl b/sdk/fidl/fuchsia.pkg.rewrite/rewrite.fidl
index bdcf490..649f75c 100644
--- a/sdk/fidl/fuchsia.pkg.rewrite/rewrite.fidl
+++ b/sdk/fidl/fuchsia.pkg.rewrite/rewrite.fidl
@@ -156,8 +156,8 @@
     ///
     /// * status `ZX_OK` the staged edits were successfully committed.
     /// * status `ZX_ERR_UNAVAILABLE` another transaction committed before this one.
+    /// * status `ZX_ERR_ACCESS_DENIED` editing dynamic rewrite rules is permanently disabled.
     Commit() -> (zx.status status);
-
 };
 
 /// The iterator over all the rewrite rules defined in a [`Engine`].