[amberctl] Mirror source edits to pkg_resolver

This reverts commit 34034708ba7ec01b7e4fccbcd72c6ca578bd0497, which was
a revert of this original change.

amberctl generated the repo config's repo_url field by appending the
source config's ID to "fuchsia-pkg://", but when the source config
contained characters that are not part of a host name (like "http://"),
the repository manager would reject the repo config, causing amberctl to
bail out.

In addition to the original change, this change sanitizes
source config ID to contain only valid characters and adds test cases to
verify the functionality.

Test: amberctl-tests, add-update-source without -n parameter

Change-Id: I2493a91d8806f30c55fd1996d306471c2ef9b1ff
diff --git a/garnet/go/src/amber/BUILD.gn b/garnet/go/src/amber/BUILD.gn
index 916a5f2..bcce302 100644
--- a/garnet/go/src/amber/BUILD.gn
+++ b/garnet/go/src/amber/BUILD.gn
@@ -36,6 +36,7 @@
     "//garnet/public/lib/syslog/go/src/syslog",
     "//sdk/fidl/fuchsia.amber($go_toolchain)",
     "//sdk/fidl/fuchsia.pkg($go_toolchain)",
+    "//sdk/fidl/fuchsia.pkg.rewrite($go_toolchain)",
     "//sdk/fidl/fuchsia.sys($go_toolchain)",
     "//zircon/public/fidl/fuchsia-cobalt($go_toolchain)",
     "//zircon/public/fidl/fuchsia-mem($go_toolchain)",
diff --git a/garnet/go/src/amber/amberctl/amberctl.go b/garnet/go/src/amber/amberctl/amberctl.go
index 7b301e9..b831bbf 100644
--- a/garnet/go/src/amber/amberctl/amberctl.go
+++ b/garnet/go/src/amber/amberctl/amberctl.go
@@ -18,6 +18,7 @@
 	"net/url"
 	"os"
 	"path/filepath"
+	"regexp"
 	"strings"
 	"syscall/zx"
 	"time"
@@ -28,6 +29,7 @@
 	"fidl/fuchsia/amber"
 	fuchsiaio "fidl/fuchsia/io"
 	"fidl/fuchsia/pkg"
+	"fidl/fuchsia/pkg/rewrite"
 )
 
 const usage = `usage: %s <command> [opts]
@@ -101,27 +103,191 @@
 	return nil
 }
 
-func connectToAmber(ctx *context.Context) (*amber.ControlInterface, amber.ControlInterfaceRequest) {
+type Services struct {
+	amber         *amber.ControlInterface
+	resolver      *pkg.PackageResolverInterface
+	repoMgr       *pkg.RepositoryManagerInterface
+	rewriteEngine *rewrite.EngineInterface
+}
+
+func connectToAmber(ctx *context.Context) *amber.ControlInterface {
 	req, pxy, err := amber.NewControlInterfaceRequest()
 	if err != nil {
 		panic(err)
 	}
 	ctx.ConnectToEnvService(req)
-	return pxy, req
+	return pxy
 }
 
-func connectToPackageResolver(ctx *context.Context) (*pkg.PackageResolverInterface, pkg.PackageResolverInterfaceRequest) {
+func connectToPackageResolver(ctx *context.Context) *pkg.PackageResolverInterface {
 	req, pxy, err := pkg.NewPackageResolverInterfaceRequest()
 	if err != nil {
 		panic(err)
 	}
 	ctx.ConnectToEnvService(req)
-	return pxy, req
+	return pxy
 }
 
-func addSource(a *amber.ControlInterface) error {
-	var cfg amber.SourceConfig
+func connectToRepositoryManager(ctx *context.Context) *pkg.RepositoryManagerInterface {
+	req, pxy, err := pkg.NewRepositoryManagerInterfaceRequest()
+	if err != nil {
+		panic(err)
+	}
+	ctx.ConnectToEnvService(req)
+	return pxy
+}
 
+func connectToRewriteEngine(ctx *context.Context) *rewrite.EngineInterface {
+	req, pxy, err := rewrite.NewEngineInterfaceRequest()
+	if err != nil {
+		panic(err)
+	}
+	ctx.ConnectToEnvService(req)
+	return pxy
+}
+
+// upgradeSourceConfig attempts to upgrade an amber.SourceConfig into a pkg.RepositoryConfig
+//
+// The two config formats are incompatible in various ways:
+//
+// * repo configs cannot be disabled. amberctl will attempt to preserve a config's disabled bit by
+// not configuring a rewrite rule for the source.
+//
+// * repo configs do not support oauth, network client config options, or polling frequency
+// overrides. If present, these options are discarded.
+//
+// * repo config mirrors do not accept different URLs for the TUF repo and the blobs. Any custom
+// blob URL is discarded.
+func upgradeSourceConfig(cfg amber.SourceConfig) pkg.RepositoryConfig {
+	repoCfg := pkg.RepositoryConfig{
+		RepoUrl:        repoUrlForId(cfg.Id),
+		RepoUrlPresent: true,
+	}
+
+	mirror := pkg.MirrorConfig{
+		MirrorUrl:        cfg.RepoUrl,
+		MirrorUrlPresent: true,
+		Subscribe:        cfg.Auto,
+		SubscribePresent: true,
+	}
+	if cfg.BlobKey != nil {
+		var blobKey pkg.RepositoryBlobKey
+		blobKey.SetAesKey(cfg.BlobKey.Data[:])
+		mirror.SetBlobKey(blobKey)
+	}
+	repoCfg.SetMirrors([]pkg.MirrorConfig{mirror})
+
+	for _, key := range cfg.RootKeys {
+		if key.Type != "ed25519" {
+			continue
+		}
+
+		var rootKey pkg.RepositoryKeyConfig
+		bytes, err := hex.DecodeString(key.Value)
+		if err != nil {
+			continue
+		}
+		rootKey.SetEd25519Key(bytes)
+
+		repoCfg.RootKeys = append(repoCfg.RootKeys, rootKey)
+		repoCfg.RootKeysPresent = true
+	}
+
+	return repoCfg
+}
+
+var invalidHostnameCharsPattern = regexp.MustCompile("[^a-zA-Z0-9_-]")
+
+func sanitizeId(id string) string {
+	return invalidHostnameCharsPattern.ReplaceAllString(id, "_")
+}
+
+func repoUrlForId(id string) string {
+	return fmt.Sprintf("fuchsia-pkg://%s", sanitizeId(id))
+}
+
+func rewriteRuleForId(id string) rewrite.Rule {
+	var rule rewrite.Rule
+	rule.SetLiteral(rewrite.LiteralRule{
+		HostMatch:             "fuchsia.com",
+		HostReplacement:       sanitizeId(id),
+		PathPrefixMatch:       "/",
+		PathPrefixReplacement: "/",
+	})
+	return rule
+}
+
+func replaceDynamicRewriteRules(rewriteEngine *rewrite.EngineInterface, rule rewrite.Rule) error {
+	return doRewriteRuleEditTransaction(rewriteEngine, func(transaction *rewrite.EditTransactionInterface) error {
+		if err := transaction.ResetAll(); err != nil {
+			return fmt.Errorf("fuchsia.pkg.rewrite.EditTransaction.ResetAll IPC encountered an error: %s", err)
+		}
+
+		s, err := transaction.Add(rule)
+		if err != nil {
+			return fmt.Errorf("fuchsia.pkg.rewrite.EditTransaction.Add IPC encountered an error: %s", err)
+		}
+		status := zx.Status(s)
+		if status != zx.ErrOk {
+			return fmt.Errorf("unable to add rewrite rule: %s", status)
+		}
+
+		return nil
+	})
+}
+
+func removeAllDynamicRewriteRules(rewriteEngine *rewrite.EngineInterface) error {
+	return doRewriteRuleEditTransaction(rewriteEngine, func(transaction *rewrite.EditTransactionInterface) error {
+		if err := transaction.ResetAll(); err != nil {
+			return fmt.Errorf("fuchsia.pkg.rewrite.EditTransaction.ResetAll IPC encountered an error: %s", err)
+		}
+
+		return nil
+	})
+}
+
+// doRewriteRuleEditTransaction executes a rewrite rule edit transaction using
+// the provided callback, retrying on data races a few times before giving up.
+func doRewriteRuleEditTransaction(rewriteEngine *rewrite.EngineInterface, cb func(*rewrite.EditTransactionInterface) error) error {
+	for i := 0; i < 10; i++ {
+		err, status := func() (error, zx.Status) {
+			var status zx.Status
+			req, transaction, err := rewrite.NewEditTransactionInterfaceRequest()
+			if err != nil {
+				return fmt.Errorf("creating edit transaction: %s", err), status
+			}
+			defer transaction.Close()
+			if err := rewriteEngine.StartEditTransaction(req); err != nil {
+				return fmt.Errorf("fuchsia.pkg.rewrite.Engine IPC encountered an error: %s", err), status
+			}
+
+			if err := cb(transaction); err != nil {
+				return err, status
+			}
+
+			s, err := transaction.Commit()
+			if err != nil {
+				return fmt.Errorf("fuchsia.pkg.rewrite.EditTransaction.Commit IPC encountered an error: %s", err), status
+			}
+			return nil, zx.Status(s)
+		}()
+		if err != nil {
+			return err
+		}
+		switch status {
+		case zx.ErrOk:
+			return nil
+		case zx.ErrUnavailable:
+			continue
+		default:
+			return fmt.Errorf("unexpected error while committing rewrite rule transaction: %s", status)
+		}
+	}
+
+	return fmt.Errorf("unable to commit rewrite rule changes")
+}
+
+func addSource(services Services) error {
 	if len(*pkgFile) == 0 {
 		return fmt.Errorf("a url or file path (via -f) are required")
 	}
@@ -179,6 +345,7 @@
 		source = f
 	}
 
+	var cfg amber.SourceConfig
 	if err := json.NewDecoder(source).Decode(&cfg); err != nil {
 		return fmt.Errorf("failed to parse source config: %v", err)
 	}
@@ -206,16 +373,38 @@
 		cfg.BlobRepoUrl = filepath.Join(cfg.RepoUrl, "blobs")
 	}
 
-	added, err := a.AddSrc(cfg)
+	added, err := services.amber.AddSrc(cfg)
 	if err != nil {
-		return fmt.Errorf("IPC encountered an error: %s", err)
+		return fmt.Errorf("fuchsia.amber.Control IPC encountered an error: %s", err)
 	}
 	if !added {
 		return fmt.Errorf("request arguments properly formatted, but possibly otherwise invalid")
 	}
 
 	if isSourceConfigEnabled(&cfg) && !*nonExclusive {
-		if err := disableAllSources(a, cfg.Id); err != nil {
+		if err := disableAllSources(services.amber, cfg.Id); err != nil {
+			return err
+		}
+	}
+
+	repoCfg := upgradeSourceConfig(cfg)
+	s, err := services.repoMgr.Add(repoCfg)
+	if err != nil {
+		return fmt.Errorf("fuchsia.pkg.RepositoryManager IPC encountered an error: %s", err)
+	}
+	status := zx.Status(s)
+	if !(status == zx.ErrOk || status == zx.ErrAlreadyExists) {
+		return fmt.Errorf("unable to register source with RepositoryManager: %s", status)
+	}
+
+	// Nothing currently registers sources in a disabled state, but make a best effort attempt
+	// to try to prevent the source from being used anyway by only configuring a mapping of
+	// fuchsia.com to this source if it is enabled. Note that this doesn't prevent resolving a
+	// package using this config's id explicitly or calling an amber source config
+	// "fuchsia.com".
+	if isSourceConfigEnabled(&cfg) {
+		rule := rewriteRuleForId(cfg.Id)
+		if err := replaceDynamicRewriteRules(services.rewriteEngine, rule); err != nil {
 			return err
 		}
 	}
@@ -223,19 +412,19 @@
 	return nil
 }
 
-func rmSource(a *amber.ControlInterface) error {
+func rmSource(services Services) error {
 	name := strings.TrimSpace(*name)
 	if name == "" {
 		return fmt.Errorf("no source id provided")
 	}
 
-	status, err := a.RemoveSrc(name)
+	status, err := services.amber.RemoveSrc(name)
 	if err != nil {
-		return fmt.Errorf("IPC encountered an error: %s", err)
+		return fmt.Errorf("fuchsia.amber.Control IPC encountered an error: %s", err)
 	}
 	switch status {
 	case amber.StatusOk:
-		return nil
+		break
 	case amber.StatusErrNotFound:
 		return fmt.Errorf("Source not found")
 	case amber.StatusErr:
@@ -243,6 +432,25 @@
 	default:
 		return fmt.Errorf("Unexpected status: %v", status)
 	}
+
+	// Since modifications to amber.Control, RepositoryManager, and rewrite.Engine aren't
+	// atomic and amberctl could be interrupted or encounter an error during any step,
+	// unregister the rewrite rule before removing the repo config to prevent a dangling
+	// rewrite rule to a repo that no longer exists.
+	if err := removeAllDynamicRewriteRules(services.rewriteEngine); err != nil {
+		return err
+	}
+
+	s, err := services.repoMgr.Remove(repoUrlForId(name))
+	if err != nil {
+		return fmt.Errorf("fuchsia.pkg.RepositoryManager IPC encountered an error: %s", err)
+	}
+	zxStatus := zx.Status(s)
+	if !(zxStatus == zx.ErrOk || zxStatus == zx.ErrNotFound) {
+		return fmt.Errorf("unable to remove source from RepositoryManager: %s", zxStatus)
+	}
+
+	return nil
 }
 
 func getUp(r *pkg.PackageResolverInterface) error {
@@ -322,10 +530,10 @@
 	return nil
 }
 
-func do(amberProxy *amber.ControlInterface, resolverProxy *pkg.PackageResolverInterface) int {
+func do(services Services) int {
 	switch os.Args[1] {
 	case "get_up":
-		if err := getUp(resolverProxy); err != nil {
+		if err := getUp(services.resolver); err != nil {
 			log.Printf("error getting an update: %s", err)
 			return 1
 		}
@@ -334,12 +542,12 @@
 			log.Printf("no blob id provided")
 			return 1
 		}
-		if err := amberProxy.GetBlob(*blobID); err != nil {
+		if err := services.amber.GetBlob(*blobID); err != nil {
 			log.Printf("error requesting blob fetch: %s", err)
 			return 1
 		}
 	case "add_src":
-		if err := addSource(amberProxy); err != nil {
+		if err := addSource(services); err != nil {
 			log.Printf("error adding source: %s", err)
 			if _, ok := err.(ErrGetFile); ok {
 				return 2
@@ -348,12 +556,12 @@
 			}
 		}
 	case "rm_src":
-		if err := rmSource(amberProxy); err != nil {
+		if err := rmSource(services); err != nil {
 			log.Printf("error removing source: %s", err)
 			return 1
 		}
 	case "list_srcs":
-		if err := listSources(amberProxy); err != nil {
+		if err := listSources(services.amber); err != nil {
 			log.Printf("error listing sources: %s", err)
 			return 1
 		}
@@ -361,12 +569,12 @@
 		log.Printf("%q not yet supported\n", os.Args[1])
 		return 1
 	case "test":
-		if err := doTest(amberProxy); err != nil {
+		if err := doTest(services.amber); err != nil {
 			log.Printf("error testing connection to amber: %s", err)
 			return 1
 		}
 	case "system_update":
-		configured, err := amberProxy.CheckForSystemUpdate()
+		configured, err := services.amber.CheckForSystemUpdate()
 		if err != nil {
 			log.Printf("error checking for system update: %s", err)
 			return 1
@@ -382,14 +590,19 @@
 			log.Printf("Error enabling source: no source id provided")
 			return 1
 		}
-		err := setSourceEnablement(amberProxy, *name, true)
+		err := setSourceEnablement(services.amber, *name, true)
 		if err != nil {
 			log.Printf("Error enabling source: %s", err)
 			return 1
 		}
+		err = replaceDynamicRewriteRules(services.rewriteEngine, rewriteRuleForId(*name))
+		if err != nil {
+			log.Printf("Error configuring rewrite rules: %s", err)
+			return 1
+		}
 		fmt.Printf("Source %q enabled\n", *name)
 		if !*nonExclusive {
-			if err := disableAllSources(amberProxy, *name); err != nil {
+			if err := disableAllSources(services.amber, *name); err != nil {
 				log.Printf("Error disabling sources: %s", err)
 				return 1
 			}
@@ -399,14 +612,19 @@
 			log.Printf("Error disabling source: no source id provided")
 			return 1
 		}
-		err := setSourceEnablement(amberProxy, *name, false)
+		err := setSourceEnablement(services.amber, *name, false)
 		if err != nil {
 			log.Printf("Error disabling source: %s", err)
 			return 1
 		}
+		err = removeAllDynamicRewriteRules(services.rewriteEngine)
+		if err != nil {
+			log.Printf("Error configuring rewrite rules: %s", err)
+			return 1
+		}
 		fmt.Printf("Source %q disabled\n", *name)
 	case "gc":
-		err := amberProxy.Gc()
+		err := services.amber.Gc()
 		if err != nil {
 			log.Printf("Error collecting garbage: %s", err)
 			return 1
@@ -462,13 +680,21 @@
 
 	ctx := context.CreateFromStartupInfo()
 
-	amberProxy, _ := connectToAmber(ctx)
-	defer amberProxy.Close()
+	var services Services
 
-	resolverProxy, _ := connectToPackageResolver(ctx)
-	defer resolverProxy.Close()
+	services.amber = connectToAmber(ctx)
+	defer services.amber.Close()
 
-	os.Exit(do(amberProxy, resolverProxy))
+	services.resolver = connectToPackageResolver(ctx)
+	defer services.resolver.Close()
+
+	services.repoMgr = connectToRepositoryManager(ctx)
+	defer services.repoMgr.Close()
+
+	services.rewriteEngine = connectToRewriteEngine(ctx)
+	defer services.rewriteEngine.Close()
+
+	os.Exit(do(services))
 }
 
 type ErrDaemon string
diff --git a/garnet/go/src/amber/meta/amberctl.cmx b/garnet/go/src/amber/meta/amberctl.cmx
index ba9393c..4bc953c 100644
--- a/garnet/go/src/amber/meta/amberctl.cmx
+++ b/garnet/go/src/amber/meta/amberctl.cmx
@@ -7,7 +7,9 @@
             "fuchsia.amber.Control",
             "fuchsia.logger.LogSink",
             "fuchsia.net.SocketProvider",
-            "fuchsia.pkg.PackageResolver"
+            "fuchsia.pkg.PackageResolver",
+            "fuchsia.pkg.RepositoryManager",
+            "fuchsia.pkg.rewrite.Engine"
         ]
     }
 }
diff --git a/garnet/lib/rust/fidl_fuchsia_pkg_ext/src/repo.rs b/garnet/lib/rust/fidl_fuchsia_pkg_ext/src/repo.rs
index 83b7832..59ce888 100644
--- a/garnet/lib/rust/fidl_fuchsia_pkg_ext/src/repo.rs
+++ b/garnet/lib/rust/fidl_fuchsia_pkg_ext/src/repo.rs
@@ -8,18 +8,18 @@
     fuchsia_uri::pkg_uri::{PkgUri, RepoUri},
     serde_derive::{Deserialize, Serialize},
     std::convert::TryFrom,
-    std::mem,
+    std::{fmt, mem},
 };
 
 /// Convenience wrapper for the FIDL RepositoryKeyConfig type
-#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
+#[derive(Clone, PartialEq, Eq, Serialize, Deserialize)]
 #[serde(rename_all = "lowercase", tag = "type", content = "value", deny_unknown_fields)]
 pub enum RepositoryKey {
     Ed25519(#[serde(with = "hex_serde")] Vec<u8>),
 }
 
 /// Convenience wrapper for the FIDL RepositoryBlobConfig type
-#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
+#[derive(Clone, PartialEq, Eq, Serialize, Deserialize)]
 #[serde(rename_all = "lowercase", tag = "type", content = "value", deny_unknown_fields)]
 pub enum RepositoryBlobKey {
     Aes(#[serde(with = "hex_serde")] Vec<u8>),
@@ -40,17 +40,27 @@
 }
 
 /// Convenience wrapper for generating [MirrorConfig] values.
+#[derive(Clone, Debug)]
 pub struct MirrorConfigBuilder {
     config: MirrorConfig,
 }
 
 impl MirrorConfigBuilder {
-    pub fn new(mirror_url: String) -> Self {
+    pub fn new(mirror_url: impl Into<String>) -> Self {
         MirrorConfigBuilder {
-            config: MirrorConfig { mirror_url: mirror_url, subscribe: false, blob_key: None },
+            config: MirrorConfig {
+                mirror_url: mirror_url.into(),
+                subscribe: false,
+                blob_key: None,
+            },
         }
     }
 
+    pub fn mirror_url(mut self, mirror_url: impl Into<String>) -> Self {
+        self.config.mirror_url = mirror_url.into();
+        self
+    }
+
     pub fn subscribe(mut self, subscribe: bool) -> Self {
         self.config.subscribe = subscribe;
         self
@@ -66,6 +76,12 @@
     }
 }
 
+impl Into<MirrorConfig> for MirrorConfigBuilder {
+    fn into(self) -> MirrorConfig {
+        self.build()
+    }
+}
+
 impl TryFrom<fidl::MirrorConfig> for MirrorConfig {
     type Error = RepositoryParseError;
     fn try_from(other: fidl::MirrorConfig) -> Result<Self, RepositoryParseError> {
@@ -169,6 +185,7 @@
 }
 
 /// Convenience wrapper for generating [RepositoryConfig] values.
+#[derive(Clone, Debug)]
 pub struct RepositoryConfigBuilder {
     config: RepositoryConfig,
 }
@@ -177,7 +194,7 @@
     pub fn new(repo_url: RepoUri) -> Self {
         RepositoryConfigBuilder {
             config: RepositoryConfig {
-                repo_url: repo_url,
+                repo_url,
                 root_keys: vec![],
                 mirrors: vec![],
                 update_package_uri: None,
@@ -185,13 +202,18 @@
         }
     }
 
+    pub fn repo_url(mut self, repo_url: RepoUri) -> Self {
+        self.config.repo_url = repo_url;
+        self
+    }
+
     pub fn add_root_key(mut self, key: RepositoryKey) -> Self {
         self.config.root_keys.push(key);
         self
     }
 
-    pub fn add_mirror(mut self, mirror: MirrorConfig) -> Self {
-        self.config.mirrors.push(mirror);
+    pub fn add_mirror(mut self, mirror: impl Into<MirrorConfig>) -> Self {
+        self.config.mirrors.push(mirror.into());
         self
     }
 
@@ -205,6 +227,12 @@
     }
 }
 
+impl Into<RepositoryConfig> for RepositoryConfigBuilder {
+    fn into(self) -> RepositoryConfig {
+        self.build()
+    }
+}
+
 /// Wraper for serializing repository configs to the on-disk JSON format.
 #[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize)]
 #[serde(tag = "version", content = "content", deny_unknown_fields)]
@@ -231,6 +259,13 @@
     }
 }
 
+impl fmt::Debug for RepositoryKey {
+    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
+        let RepositoryKey::Ed25519(ref value) = self;
+        f.debug_tuple("Ed25519").field(&hex::encode(value)).finish()
+    }
+}
+
 impl TryFrom<fidl::RepositoryBlobKey> for RepositoryBlobKey {
     type Error = RepositoryParseError;
     fn try_from(id: fidl::RepositoryBlobKey) -> Result<Self, RepositoryParseError> {
@@ -249,6 +284,13 @@
     }
 }
 
+impl fmt::Debug for RepositoryBlobKey {
+    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
+        let RepositoryBlobKey::Aes(ref value) = self;
+        f.debug_tuple("Aes").field(&hex::encode(value)).finish()
+    }
+}
+
 #[cfg(test)]
 mod tests {
     use super::*;
diff --git a/garnet/lib/rust/fuchsia_uri_rewrite/src/rule.rs b/garnet/lib/rust/fuchsia_uri_rewrite/src/rule.rs
index 3a17a32..e61d14c 100644
--- a/garnet/lib/rust/fuchsia_uri_rewrite/src/rule.rs
+++ b/garnet/lib/rust/fuchsia_uri_rewrite/src/rule.rs
@@ -30,11 +30,16 @@
 impl Rule {
     /// Creates a new `Rule`.
     pub fn new(
-        host_match: String,
-        host_replacement: String,
-        path_prefix_match: String,
-        path_prefix_replacement: String,
+        host_match: impl Into<String>,
+        host_replacement: impl Into<String>,
+        path_prefix_match: impl Into<String>,
+        path_prefix_replacement: impl Into<String>,
     ) -> Result<Self, RuleParseError> {
+        let host_match = host_match.into();
+        let host_replacement = host_replacement.into();
+        let path_prefix_match = path_prefix_match.into();
+        let path_prefix_replacement = path_prefix_replacement.into();
+
         fn validate_host(s: &str) -> Result<(), RuleParseError> {
             PkgUri::new_repository(s.to_owned()).map_err(|_err| RuleParseError::InvalidHost)?;
             Ok(())
@@ -165,13 +170,8 @@
     macro_rules! rule {
         ($host_match:expr => $host_replacement:expr,
          $path_prefix_match:expr => $path_prefix_replacement:expr) => {
-            Rule::new(
-                $host_match.to_owned(),
-                $host_replacement.to_owned(),
-                $path_prefix_match.to_owned(),
-                $path_prefix_replacement.to_owned(),
-            )
-            .unwrap()
+            Rule::new($host_match, $host_replacement, $path_prefix_match, $path_prefix_replacement)
+                .unwrap()
         };
     }
 
@@ -322,19 +322,19 @@
                 #[test]
                 fn $test_name() {
                     let error = Rule::new(
-                        $host_match.to_owned(),
-                        $host_replacement.to_owned(),
-                        $path_prefix_match.to_owned(),
-                        $path_prefix_replacement.to_owned()
+                        $host_match,
+                        $host_replacement,
+                        $path_prefix_match,
+                        $path_prefix_replacement,
                     )
                     .expect_err("should have failed to parse");
                     assert_eq!(error, $error);
 
                     let error = Rule::new(
-                        $host_replacement.to_owned(),
-                        $host_match.to_owned(),
-                        $path_prefix_replacement.to_owned(),
-                        $path_prefix_match.to_owned()
+                        $host_replacement,
+                        $host_match,
+                        $path_prefix_replacement,
+                        $path_prefix_match,
                     )
                     .expect_err("should have failed to parse");
                     assert_eq!(error, $error);
diff --git a/garnet/tests/amberctl/BUILD.gn b/garnet/tests/amberctl/BUILD.gn
index 0faf5ec..59df86f 100644
--- a/garnet/tests/amberctl/BUILD.gn
+++ b/garnet/tests/amberctl/BUILD.gn
@@ -13,10 +13,15 @@
   with_unit_tests = true
 
   deps = [
+    "//garnet/lib/rust/fidl_fuchsia_pkg_ext",
+    "//garnet/lib/rust/fuchsia_uri",
+    "//garnet/lib/rust/fuchsia_uri_rewrite",
     "//garnet/public/lib/fidl/rust/fidl",
     "//garnet/public/rust/fuchsia-async",
     "//garnet/public/rust/fuchsia-component",
     "//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",
     "//third_party/rust_crates:failure",
     "//third_party/rust_crates:hex",
diff --git a/garnet/tests/amberctl/src/lib.rs b/garnet/tests/amberctl/src/lib.rs
index c23983c..8c39964 100644
--- a/garnet/tests/amberctl/src/lib.rs
+++ b/garnet/tests/amberctl/src/lib.rs
@@ -6,13 +6,23 @@
 #![cfg(test)]
 
 use {
+    failure::Error,
     fidl_fuchsia_amber::{ControlMarker as AmberMarker, ControlProxy as AmberProxy},
+    fidl_fuchsia_pkg::{RepositoryManagerMarker, RepositoryManagerProxy},
+    fidl_fuchsia_pkg_ext::{
+        MirrorConfigBuilder, RepositoryConfig, RepositoryConfigBuilder, RepositoryKey,
+    },
+    fidl_fuchsia_pkg_rewrite::{
+        EngineMarker as RewriteEngineMarker, EngineProxy as RewriteEngineProxy,
+    },
     fidl_fuchsia_sys::TerminationReason,
     fuchsia_async as fasync,
     fuchsia_component::{
         client::{App, AppBuilder, Stdio},
         server::{NestedEnvironment, ServiceFs},
     },
+    fuchsia_uri::pkg_uri::RepoUri,
+    fuchsia_uri_rewrite::Rule,
     futures::prelude::*,
     std::{convert::TryInto, fs::File},
 };
@@ -23,6 +33,10 @@
 const ROOT_KEY_1: &str = "be0b983f7396da675c40c6b93e47fced7c1e9ea8a32a1fe952ba8f519760b307";
 const ROOT_KEY_2: &str = "00112233445566778899aabbccddeeffffeeddccbbaa99887766554433221100";
 
+fn amberctl() -> AppBuilder {
+    AppBuilder::new("fuchsia-pkg://fuchsia.com/amberctl-tests#meta/amberctl.cmx".to_owned())
+}
+
 struct Mounts {
     misc: tempfile::TempDir,
     data_amber: tempfile::TempDir,
@@ -39,11 +53,14 @@
 
 struct Proxies {
     amber: AmberProxy,
+    repo_manager: RepositoryManagerProxy,
+    rewrite_engine: RewriteEngineProxy,
 }
 
 struct TestEnv {
     _amber: App,
-    mounts: Mounts,
+    _pkg_resolver: App,
+    _mounts: Mounts,
     env: NestedEnvironment,
     proxies: Proxies,
 }
@@ -69,43 +86,67 @@
         )
         .expect("/data/amber to mount");
 
+        let mut pkg_resolver = AppBuilder::new(
+            "fuchsia-pkg://fuchsia.com/pkg_resolver#meta/pkg_resolver.cmx".to_owned(),
+        );
+
         let mut fs = ServiceFs::new();
-        fs.add_proxy_service_to::<AmberMarker, _>(amber.directory_request().unwrap().clone());
+        fs.add_proxy_service_to::<AmberMarker, _>(amber.directory_request().unwrap().clone())
+            .add_proxy_service_to::<RepositoryManagerMarker, _>(
+                pkg_resolver.directory_request().unwrap().clone(),
+            )
+            .add_proxy_service_to::<RewriteEngineMarker, _>(
+                pkg_resolver.directory_request().unwrap().clone(),
+            );
+
         let env = fs
             .create_salted_nested_environment("amberctl_env")
             .expect("nested environment to create successfully");
         fasync::spawn(fs.collect());
 
         let amber = amber.spawn(env.launcher()).expect("amber to launch");
+        let pkg_resolver = pkg_resolver.spawn(env.launcher()).expect("amber to launch");
 
         let amber_proxy = env.connect_to_service::<AmberMarker>().expect("connect to amber");
+        let repo_manager_proxy = env
+            .connect_to_service::<RepositoryManagerMarker>()
+            .expect("connect to repository manager");
+        let rewrite_engine_proxy =
+            env.connect_to_service::<RewriteEngineMarker>().expect("connect to rewrite engine");
 
-        Self { _amber: amber, mounts, env, proxies: Proxies { amber: amber_proxy } }
+        Self {
+            _amber: amber,
+            _pkg_resolver: pkg_resolver,
+            _mounts: mounts,
+            env,
+            proxies: Proxies {
+                amber: amber_proxy,
+                repo_manager: repo_manager_proxy,
+                rewrite_engine: rewrite_engine_proxy,
+            },
+        }
     }
 
-    /// Tear down the test environment, retaining the state directories.
-    fn into_mounts(self) -> Mounts {
-        self.mounts
+    async fn _run_amberctl(&self, builder: AppBuilder) {
+        let fut =
+            builder.stderr(Stdio::Inherit).output(self.env.launcher()).expect("amberctl to launch");
+        let output = await!(fut).expect("amberctl to run");
+
+        assert_eq!(output.exit_status.reason(), TerminationReason::Exited);
+        assert!(
+            output.exit_status.success(),
+            "amberctl exited with {}\nSTDOUT\n{}\nSTDOUT",
+            output.exit_status.code(),
+            String::from_utf8_lossy(&output.stdout),
+        );
     }
 
-    /// Re-create the test environment, re-using the existing temporary state directories.
-    fn restart(self) -> Self {
-        Self::new_with_mounts(self.into_mounts())
-    }
-
-    async fn run_amberctl<'a>(&'a self, args: &'a [&'a str]) {
-        let fut = AppBuilder::new(
-            "fuchsia-pkg://fuchsia.com/amberctl-tests#meta/amberctl.cmx".to_owned(),
-        )
-        .args(args.into_iter().map(|s| *s))
-        .add_dir_to_namespace(
-            "/sources".to_string(),
-            File::open("/pkg/data/sources").expect("/pkg/data/sources to exist"),
-        )
-        .expect("/sources to mount")
-        .stderr(Stdio::Inherit)
-        .output(self.env.launcher())
-        .expect("amberctl to launch");
+    async fn run_amberctl<'a>(&'a self, args: &'a [impl std::fmt::Debug + AsRef<str>]) {
+        let fut = amberctl()
+            .args(args.into_iter().map(|s| s.as_ref()))
+            .stderr(Stdio::Inherit)
+            .output(self.env.launcher())
+            .expect("amberctl to launch");
         let output = await!(fut).expect("amberctl to run");
 
         assert_eq!(output.exit_status.reason(), TerminationReason::Exited);
@@ -118,6 +159,32 @@
         );
     }
 
+    async fn run_amberctl_add_static_src(&self, name: &'static str) {
+        await!(self._run_amberctl(
+            amberctl()
+                .add_dir_to_namespace(
+                    "/configs".to_string(),
+                    File::open("/pkg/data/sources").expect("/pkg/data/sources to exist"),
+                )
+                .expect("static /configs to mount")
+                .args(["add_src", "-f"].into_iter().cloned())
+                .arg(format!("/configs/{}", name))
+        ));
+    }
+
+    async fn run_amberctl_add_src(&self, source: types::SourceConfig) {
+        let mut config_file = tempfile::tempfile().expect("temp config file to create");
+        serde_json::to_writer(&mut config_file, &source).expect("source config to serialize");
+
+        await!(self._run_amberctl(
+            amberctl()
+                .add_dir_to_namespace("/configs/test.json".to_string(), config_file)
+                .expect("static /configs to mount")
+                // Run amberctl in non-exclusive mode so it doesn't disable existing source configs
+                .args(["add_src", "-x", "-f", "/configs/test.json"].iter().map(|s| *s))
+        ));
+    }
+
     async fn amber_list_sources(&self) -> Vec<types::SourceConfig> {
         let sources = await!(self.proxies.amber.list_srcs()).unwrap();
 
@@ -130,51 +197,90 @@
         sources.sort_unstable();
         sources
     }
+
+    async fn resolver_list_repos(&self) -> Vec<RepositoryConfig> {
+        let (iterator, iterator_server_end) = fidl::endpoints::create_proxy().unwrap();
+        self.proxies.repo_manager.list(iterator_server_end).unwrap();
+        await!(collect_iterator(|| iterator.next())).unwrap()
+    }
+
+    async fn rewrite_engine_list_rules(&self) -> Vec<Rule> {
+        let (iterator, iterator_server_end) = fidl::endpoints::create_proxy().unwrap();
+        self.proxies.rewrite_engine.list(iterator_server_end).unwrap();
+        await!(collect_iterator(|| iterator.next())).unwrap()
+    }
+}
+
+async fn collect_iterator<F, E, I, O>(mut next: impl FnMut() -> F) -> Result<Vec<O>, Error>
+where
+    F: Future<Output = Result<Vec<I>, fidl::Error>>,
+    I: TryInto<O, Error = E>,
+    Error: From<E>,
+{
+    let mut res = Vec::new();
+    loop {
+        let more = await!(next())?;
+        if more.is_empty() {
+            break;
+        }
+        res.extend(more.into_iter().map(|cfg| cfg.try_into()).collect::<Result<Vec<_>, _>>()?);
+    }
+    Ok(res)
 }
 
 struct SourceConfigGenerator {
-    builder: SourceConfigBuilder,
-    root_id: String,
-    root_url: String,
+    id_prefix: String,
     n: usize,
 }
 
 impl SourceConfigGenerator {
-    fn new(builder: SourceConfigBuilder) -> Self {
-        let config = builder.clone().build();
-        Self {
-            root_id: config.id().to_owned(),
-            root_url: config.repo_url().to_owned(),
-            builder,
-            n: 0,
-        }
+    fn new(id_prefix: impl Into<String>) -> Self {
+        Self { id_prefix: id_prefix.into(), n: 0 }
     }
 }
 
 impl Iterator for SourceConfigGenerator {
-    type Item = types::SourceConfigBuilder;
+    type Item = (types::SourceConfigBuilder, RepositoryConfigBuilder);
 
     fn next(&mut self) -> Option<Self::Item> {
-        let id = format!("{}{:02}", &self.root_id, self.n);
-        let url = format!("{}/{:02}", &self.root_url, self.n);
+        let id = format!("{}{:02}", &self.id_prefix, self.n);
+        let repo_url = format!("fuchsia-pkg://{}", &id);
+        let mirror_url = format!("http://example.com/{}", &id);
         self.n += 1;
 
-        Some(self.builder.clone().id(id).repo_url(url))
+        Some((
+            SourceConfigBuilder::new(id)
+                .repo_url(mirror_url.clone())
+                .add_root_key(ROOT_KEY_1)
+                .auto(true),
+            RepositoryConfigBuilder::new(RepoUri::parse(&repo_url).unwrap())
+                .add_root_key(RepositoryKey::Ed25519(hex::decode(ROOT_KEY_1).unwrap()))
+                .add_mirror(MirrorConfigBuilder::new(mirror_url).subscribe(true)),
+        ))
     }
 }
 
+fn make_test_repo_config() -> RepositoryConfig {
+    RepositoryConfigBuilder::new("fuchsia-pkg://test".parse().unwrap())
+        .add_root_key(RepositoryKey::Ed25519(hex::decode(ROOT_KEY_1).unwrap()))
+        .add_mirror(MirrorConfigBuilder::new("http://example.com").subscribe(true))
+        .build()
+}
+
 #[fasync::run_singlethreaded(test)]
-async fn test_amber_starts_with_no_sources() {
+async fn test_services_start_with_no_config() {
     let env = TestEnv::new();
 
     assert_eq!(await!(env.amber_list_sources()), vec![]);
+    assert_eq!(await!(env.resolver_list_repos()), vec![]);
+    assert_eq!(await!(env.rewrite_engine_list_rules()), vec![]);
 }
 
 #[fasync::run_singlethreaded(test)]
 async fn test_add_src() {
     let env = TestEnv::new();
 
-    await!(env.run_amberctl(&["add_src", "-f", "/sources/test.json"]));
+    await!(env.run_amberctl_add_static_src("test.json"));
 
     let cfg_test = SourceConfigBuilder::new("test")
         .repo_url("http://example.com")
@@ -183,51 +289,100 @@
         .add_root_key(ROOT_KEY_1)
         .build();
 
-    assert_eq!(await!(env.amber_list_sources()), vec![cfg_test.clone()]);
-
-    // Ensure source configs persist across service restarts
-    let env = env.restart();
     assert_eq!(await!(env.amber_list_sources()), vec![cfg_test]);
+    assert_eq!(await!(env.resolver_list_repos()), vec![make_test_repo_config()]);
+    assert_eq!(
+        await!(env.rewrite_engine_list_rules()),
+        vec![Rule::new("fuchsia.com", "test", "/", "/").unwrap()]
+    );
+}
+
+#[fasync::run_singlethreaded(test)]
+async fn test_add_src_with_ipv4_id() {
+    let env = TestEnv::new();
+
+    let source = SourceConfigBuilder::new("http://10.0.0.1:8083")
+        .repo_url("http://10.0.0.1:8083")
+        .add_root_key(ROOT_KEY_1)
+        .build();
+
+    let repo = RepositoryConfigBuilder::new("fuchsia-pkg://http___10_0_0_1_8083".parse().unwrap())
+        .add_root_key(RepositoryKey::Ed25519(hex::decode(ROOT_KEY_1).unwrap()))
+        .add_mirror(MirrorConfigBuilder::new("http://10.0.0.1:8083"))
+        .build();
+
+    await!(env.run_amberctl_add_src(source.clone()));
+
+    assert_eq!(await!(env.amber_list_sources()), vec![source]);
+    assert_eq!(await!(env.resolver_list_repos()), vec![repo]);
+    assert_eq!(
+        await!(env.rewrite_engine_list_rules()),
+        vec![Rule::new("fuchsia.com", "http___10_0_0_1_8083", "/", "/").unwrap()]
+    );
+}
+
+#[fasync::run_singlethreaded(test)]
+async fn test_add_src_with_ipv6_id() {
+    let env = TestEnv::new();
+
+    let source = SourceConfigBuilder::new("http://[fe80::1122:3344]:8083")
+        .repo_url("http://[fe80::1122:3344]:8083")
+        .add_root_key(ROOT_KEY_1)
+        .build();
+
+    let repo = RepositoryConfigBuilder::new(
+        "fuchsia-pkg://http____fe80__1122_3344__8083".parse().unwrap(),
+    )
+    .add_root_key(RepositoryKey::Ed25519(hex::decode(ROOT_KEY_1).unwrap()))
+    .add_mirror(MirrorConfigBuilder::new("http://[fe80::1122:3344]:8083"))
+    .build();
+
+    await!(env.run_amberctl_add_src(source.clone()));
+
+    assert_eq!(await!(env.amber_list_sources()), vec![source]);
+    assert_eq!(await!(env.resolver_list_repos()), vec![repo]);
+    assert_eq!(
+        await!(env.rewrite_engine_list_rules()),
+        vec![Rule::new("fuchsia.com", "http____fe80__1122_3344__8083", "/", "/").unwrap()]
+    );
 }
 
 #[fasync::run_singlethreaded(test)]
 async fn test_add_src_disables_other_sources() {
     let env = TestEnv::new();
 
-    let configs = SourceConfigGenerator::new(
-        SourceConfigBuilder::new("test")
-            .repo_url("http://example.com")
-            .rate_period(60)
-            .auto(true)
-            .add_root_key(ROOT_KEY_1),
-    )
-    .take(3)
-    .collect::<Vec<_>>();
+    let configs = SourceConfigGenerator::new("testgen").take(3).collect::<Vec<_>>();
 
-    for config in &configs {
-        assert_eq!(
-            await!(env.proxies.amber.add_src(&mut config.clone().build().into())).unwrap(),
-            true
-        );
+    for (config, _) in &configs {
+        await!(env.run_amberctl_add_src(config.clone().build().into()));
     }
 
-    await!(env.run_amberctl(&["add_src", "-f", "/sources/test.json"]));
+    await!(env.run_amberctl_add_static_src("test.json"));
 
-    let mut configs =
-        configs.into_iter().map(|builder| builder.enabled(false).build()).collect::<Vec<_>>();
+    let mut source_configs = vec![];
+    let mut repo_configs = vec![make_test_repo_config()];
+    for (source_config, repo_config) in configs {
+        source_configs.push(source_config.enabled(false).build());
+        repo_configs.push(repo_config.build());
+    }
     let test_config =
         serde_json::from_reader(File::open("/pkg/data/sources/test.json").unwrap()).unwrap();
-    configs.push(test_config);
-    configs.sort_unstable();
+    source_configs.push(test_config);
+    source_configs.sort_unstable();
 
-    assert_eq!(await!(env.amber_list_sources()), configs);
+    assert_eq!(await!(env.amber_list_sources()), source_configs);
+    assert_eq!(await!(env.resolver_list_repos()), repo_configs);
+    assert_eq!(
+        await!(env.rewrite_engine_list_rules()),
+        vec![Rule::new("fuchsia.com", "test", "/", "/").unwrap()]
+    );
 }
 
 #[fasync::run_singlethreaded(test)]
 async fn test_rm_src() {
     let env = TestEnv::new();
 
-    let cfg_a = SourceConfigBuilder::new("a")
+    let cfg_a = SourceConfigBuilder::new("http://[fe80::1122:3344]:8083")
         .repo_url("http://example.com/a")
         .rate_period(60)
         .add_root_key(ROOT_KEY_1)
@@ -239,30 +394,55 @@
         .add_root_key(ROOT_KEY_2)
         .build();
 
-    assert_eq!(await!(env.proxies.amber.add_src(&mut cfg_a.clone().into())).unwrap(), true);
-    assert_eq!(await!(env.proxies.amber.add_src(&mut cfg_b.clone().into())).unwrap(), true);
+    await!(env.run_amberctl_add_src(cfg_a.clone().into()));
+    await!(env.run_amberctl_add_src(cfg_b.clone().into()));
+
+    await!(env.run_amberctl(&["rm_src", "-n", "http://[fe80::1122:3344]:8083"]));
+    assert_eq!(await!(env.amber_list_sources()), vec![cfg_b]);
+    assert_eq!(
+        await!(env.resolver_list_repos()),
+        vec![RepositoryConfigBuilder::new("fuchsia-pkg://b".parse().unwrap())
+            .add_root_key(RepositoryKey::Ed25519(hex::decode(ROOT_KEY_2).unwrap()))
+            .add_mirror(MirrorConfigBuilder::new("http://example.com/b"))
+            .build()]
+    );
+    // rm_src removes all rules, so no source remains enabled.
+    assert_eq!(await!(env.rewrite_engine_list_rules()), vec![]);
 
     await!(env.run_amberctl(&["rm_src", "-n", "b"]));
-    assert_eq!(await!(env.amber_list_sources()), vec![cfg_a]);
-
-    await!(env.run_amberctl(&["rm_src", "-n", "a"]));
     assert_eq!(await!(env.amber_list_sources()), vec![]);
+    assert_eq!(await!(env.resolver_list_repos()), vec![]);
+    assert_eq!(await!(env.rewrite_engine_list_rules()), vec![]);
 }
 
 #[fasync::run_singlethreaded(test)]
 async fn test_enable_src() {
     let env = TestEnv::new();
 
-    let cfg = SourceConfigBuilder::new("test")
+    let source = SourceConfigBuilder::new("test")
         .repo_url("http://example.com")
         .enabled(false)
         .add_root_key(ROOT_KEY_1);
 
-    assert_eq!(await!(env.proxies.amber.add_src(&mut cfg.clone().build().into())).unwrap(), true);
+    let repo = RepositoryConfigBuilder::new("fuchsia-pkg://test".parse().unwrap())
+        .add_root_key(RepositoryKey::Ed25519(hex::decode(ROOT_KEY_1).unwrap()))
+        .add_mirror(MirrorConfigBuilder::new("http://example.com"))
+        .build();
+
+    await!(env.run_amberctl_add_src(source.clone().build().into()));
+
+    assert_eq!(await!(env.resolver_list_repos()), vec![repo.clone()]);
+    // Adding a disabled source does not add a rewrite rule for it.
+    assert_eq!(await!(env.rewrite_engine_list_rules()), vec![]);
 
     await!(env.run_amberctl(&["enable_src", "-n", "test"]));
 
-    assert_eq!(await!(env.amber_list_sources()), vec![cfg.enabled(true).build()]);
+    assert_eq!(await!(env.amber_list_sources()), vec![source.enabled(true).build()]);
+    assert_eq!(await!(env.resolver_list_repos()), vec![repo]);
+    assert_eq!(
+        await!(env.rewrite_engine_list_rules()),
+        vec![Rule::new("fuchsia.com", "test", "/", "/").unwrap()]
+    );
 }
 
 #[fasync::run_singlethreaded(test)]
@@ -270,33 +450,39 @@
     let env = TestEnv::new();
 
     // add some enabled sources
-    let mut gen = SourceConfigGenerator::new(
-        SourceConfigBuilder::new("test").repo_url("http://example.com").add_root_key(ROOT_KEY_1),
-    );
+    let mut gen = SourceConfigGenerator::new("test");
     let configs = gen.by_ref().take(3).collect::<Vec<_>>();
-    for config in &configs {
-        assert_eq!(
-            await!(env.proxies.amber.add_src(&mut config.clone().build().into())).unwrap(),
-            true
-        );
+    for (config, _) in &configs {
+        await!(env.run_amberctl_add_src(config.clone().build().into()));
     }
 
-    // add an initially disabled source.
-    let config = gen.next().unwrap().enabled(false);
+    // add an initially disabled source
+    let (config, repo) = gen.next().unwrap();
+    let config = config.enabled(false);
     let c = config.clone().build();
     let id = c.id().to_owned();
-    assert_eq!(await!(env.proxies.amber.add_src(&mut c.into())).unwrap(), true);
+    await!(env.run_amberctl_add_src(c.into()));
 
     // enable that source
     let args = ["enable_src", "-n", &id];
     await!(env.run_amberctl(&args));
 
     // verify the enabled sources are now disabled and the disabled source is now enabled
-    let mut configs =
-        configs.into_iter().map(|builder| builder.enabled(false).build()).collect::<Vec<_>>();
-    configs.push(config.enabled(true).build());
-    configs.sort_unstable();
-    assert_eq!(await!(env.amber_list_sources()), configs);
+    let mut source_configs = vec![];
+    let mut repo_configs = vec![];
+    for (source_config, repo_config) in configs {
+        source_configs.push(source_config.enabled(false).build());
+        repo_configs.push(repo_config.build());
+    }
+    source_configs.push(config.enabled(true).build());
+    repo_configs.push(repo.build());
+    source_configs.sort_unstable();
+    assert_eq!(await!(env.amber_list_sources()), source_configs);
+    assert_eq!(await!(env.resolver_list_repos()), repo_configs);
+    assert_eq!(
+        await!(env.rewrite_engine_list_rules()),
+        vec![Rule::new("fuchsia.com", id, "/", "/").unwrap()]
+    );
 }
 
 #[fasync::run_singlethreaded(test)]
@@ -313,8 +499,8 @@
         .rate_period(60)
         .add_root_key(ROOT_KEY_2);
 
-    assert_eq!(await!(env.proxies.amber.add_src(&mut cfg_a.clone().build().into())).unwrap(), true);
-    assert_eq!(await!(env.proxies.amber.add_src(&mut cfg_b.clone().build().into())).unwrap(), true);
+    await!(env.run_amberctl_add_src(cfg_a.clone().build().into()));
+    await!(env.run_amberctl_add_src(cfg_b.clone().build().into()));
 
     await!(env.run_amberctl(&["disable_src", "-n", "a"]));
 
@@ -322,4 +508,19 @@
         await!(env.amber_list_sources()),
         vec![cfg_a.enabled(false).build(), cfg_b.enabled(true).build().into(),]
     );
+    assert_eq!(
+        await!(env.resolver_list_repos()),
+        vec![
+            RepositoryConfigBuilder::new("fuchsia-pkg://a".parse().unwrap())
+                .add_root_key(RepositoryKey::Ed25519(hex::decode(ROOT_KEY_1).unwrap()))
+                .add_mirror(MirrorConfigBuilder::new("http://example.com/a"))
+                .build(),
+            RepositoryConfigBuilder::new("fuchsia-pkg://b".parse().unwrap())
+                .add_root_key(RepositoryKey::Ed25519(hex::decode(ROOT_KEY_2).unwrap()))
+                .add_mirror(MirrorConfigBuilder::new("http://example.com/b"))
+                .build(),
+        ]
+    );
+    // disabling any source clears all rewrite rules.
+    assert_eq!(await!(env.rewrite_engine_list_rules()), vec![]);
 }
diff --git a/garnet/tests/amberctl/src/types.rs b/garnet/tests/amberctl/src/types.rs
index cbf00fa..eedbbf7 100644
--- a/garnet/tests/amberctl/src/types.rs
+++ b/garnet/tests/amberctl/src/types.rs
@@ -35,11 +35,6 @@
         }
     }
 
-    pub fn id(mut self, value: impl Into<String>) -> Self {
-        self.config.id = value.into();
-        self
-    }
-
     pub fn repo_url(mut self, value: impl Into<String>) -> Self {
         self.config.repo_url = value.into();
         self.config.blob_repo_url = format!("{}/blobs", self.config.repo_url);
@@ -98,9 +93,6 @@
     pub fn id(&self) -> &str {
         self.id.as_str()
     }
-    pub fn repo_url(&self) -> &str {
-        self.repo_url.as_str()
-    }
 }
 
 impl Into<fidl::SourceConfig> for SourceConfig {