TBR: ref: namespace_browser: Move v.io/x/ref/tools to v.io/x/ref/cmd.

This is part of the v.io reorganization.  cmd is the standard
go directory for commandline binaries.  We actually have two:
cmd for regular command line tools, and services for servers.

Note, I am temporarily duplicating the vdl tool for ease of transition.

MultiPart: 4/8

Change-Id: I1880ea23f643b60d315b14e2695c63932fe99625
diff --git a/cmd/GO.PACKAGE b/cmd/GO.PACKAGE
new file mode 100644
index 0000000..5100003
--- /dev/null
+++ b/cmd/GO.PACKAGE
@@ -0,0 +1,8 @@
+{
+	"dependencies": {
+		"incoming": [
+			{"allow": "v.io/x/ref/cmd/..."},
+			{"deny": "..."}
+		]
+	}
+}
diff --git a/cmd/application/doc.go b/cmd/application/doc.go
new file mode 100644
index 0000000..26deafa
--- /dev/null
+++ b/cmd/application/doc.go
@@ -0,0 +1,123 @@
+// This file was auto-generated via go generate.
+// DO NOT UPDATE MANUALLY
+
+/*
+The application tool facilitates interaction with the veyron application
+repository.
+
+Usage:
+   application <command>
+
+The application commands are:
+   match       Shows the first matching envelope that matches the given
+               profiles.
+   put         Add the given envelope to the application for the given profiles.
+   remove      removes the application envelope for the given profile.
+   edit        edits the application envelope for the given profile.
+   help        Display help for commands or topics
+Run "application help [command]" for command usage.
+
+The global flags are:
+ -alsologtostderr=true
+   log to standard error as well as files
+ -log_backtrace_at=:0
+   when logging hits line file:N, emit a stack trace
+ -log_dir=
+   if non-empty, write log files to this directory
+ -logtostderr=false
+   log to standard error instead of files
+ -max_stack_buf_size=4292608
+   max size in bytes of the buffer to use for logging stack traces
+ -stderrthreshold=2
+   logs at or above this threshold go to stderr
+ -v=0
+   log level for V logs
+ -vanadium.i18n_catalogue=
+   18n catalogue files to load, comma separated
+ -veyron.credentials=
+   directory to use for storing security credentials
+ -veyron.namespace.root=[/ns.dev.v.io:8101]
+   local namespace root; can be repeated to provided multiple roots
+ -veyron.proxy=
+   object name of proxy service to use to export services across network
+   boundaries
+ -veyron.tcp.address=
+   address to listen on
+ -veyron.tcp.protocol=tcp
+   protocol to listen with
+ -veyron.vtrace.cache_size=1024
+   The number of vtrace traces to store in memory.
+ -veyron.vtrace.collect_regexp=
+   Spans and annotations that match this regular expression will trigger trace
+   collection.
+ -veyron.vtrace.dump_on_shutdown=true
+   If true, dump all stored traces on runtime shutdown.
+ -veyron.vtrace.sample_rate=0
+   Rate (from 0.0 to 1.0) to sample vtrace traces.
+ -vmodule=
+   comma-separated list of pattern=N settings for file-filtered logging
+
+Application Match
+
+Shows the first matching envelope that matches the given profiles.
+
+Usage:
+   application match <application> <profiles>
+
+<application> is the full name of the application. <profiles> is a
+comma-separated list of profiles.
+
+Application Put
+
+Add the given envelope to the application for the given profiles.
+
+Usage:
+   application put <application> <profiles> [<envelope>]
+
+<application> is the full name of the application. <profiles> is a
+comma-separated list of profiles. <envelope> is the file that contains a
+JSON-encoded envelope. If this file is not provided, the user will be prompted
+to enter the data manually.
+
+Application Remove
+
+removes the application envelope for the given profile.
+
+Usage:
+   application remove <application> <profile>
+
+<application> is the full name of the application. <profile> is a profile.
+
+Application Edit
+
+edits the application envelope for the given profile.
+
+Usage:
+   application edit <application> <profile>
+
+<application> is the full name of the application. <profile> is a profile.
+
+Application Help
+
+Help with no args displays the usage of the parent command.
+
+Help with args displays the usage of the specified sub-command or help topic.
+
+"help ..." recursively displays help for all commands and topics.
+
+The output is formatted to a target width in runes.  The target width is
+determined by checking the environment variable CMDLINE_WIDTH, falling back on
+the terminal width from the OS, falling back on 80 chars.  By setting
+CMDLINE_WIDTH=x, if x > 0 the width is x, if x < 0 the width is unlimited, and
+if x == 0 or is unset one of the fallbacks is used.
+
+Usage:
+   application help [flags] [command/topic ...]
+
+[command/topic ...] optionally identifies a specific sub-command or help topic.
+
+The application help flags are:
+ -style=text
+   The formatting style for help output, either "text" or "godoc".
+*/
+package main
diff --git a/cmd/application/impl.go b/cmd/application/impl.go
new file mode 100644
index 0000000..4cae1ee
--- /dev/null
+++ b/cmd/application/impl.go
@@ -0,0 +1,297 @@
+package main
+
+import (
+	"bytes"
+	"encoding/json"
+	"errors"
+	"fmt"
+	"io/ioutil"
+	"os"
+	"os/exec"
+	"reflect"
+	"strings"
+	"time"
+
+	"v.io/v23/context"
+	"v.io/v23/security"
+	"v.io/v23/services/mgmt/application"
+	"v.io/v23/vom"
+	"v.io/x/lib/cmdline"
+	"v.io/x/ref/services/mgmt/repository"
+)
+
+// TODO(ashankar): application.Envelope is no longer JSON friendly
+// (after https://vanadium-review.googlesource.com/#/c/6300/).
+//
+// This mirrored structure is required in order to work around that
+// problem. Figure out what we want to do.
+type appenv struct {
+	Title     string
+	Args      []string
+	Binary    application.SignedFile
+	Publisher security.WireBlessings
+	Env       []string
+	Packages  application.Packages
+}
+
+func (a *appenv) Load(env application.Envelope) {
+	a.Title = env.Title
+	a.Args = env.Args
+	a.Binary = env.Binary
+	a.Publisher = security.MarshalBlessings(env.Publisher)
+	a.Env = env.Env
+	a.Packages = env.Packages
+}
+
+func (a *appenv) Store() (application.Envelope, error) {
+	// Have to roundtrip through vom to convert from WireBlessings to Blessings.
+	// This may seem silly, but this whole appenv type is silly too :).
+	// Figure out how to get rid of it.
+	bytes, err := vom.Encode(a.Publisher)
+	if err != nil {
+		return application.Envelope{}, err
+	}
+	var publisher security.Blessings
+	if err := vom.Decode(bytes, &publisher); err != nil {
+		return application.Envelope{}, err
+	}
+	return application.Envelope{
+		Title:     a.Title,
+		Args:      a.Args,
+		Binary:    a.Binary,
+		Publisher: publisher,
+		Env:       a.Env,
+		Packages:  a.Packages,
+	}, nil
+}
+
+func init() {
+	// Ensure that no fields have been added to application.Envelope,
+	// because if so, then appenv needs to change.
+	if n := reflect.TypeOf(application.Envelope{}).NumField(); n != 6 {
+		panic(fmt.Sprintf("It appears that fields have been added to or removed from application.Envelope before the hack in this file around json-encodeability was removed. Please also update appenv, appenv.Load and appenv.Store in this file"))
+	}
+}
+
+func getEnvelopeJSON(app repository.ApplicationClientMethods, profiles string) ([]byte, error) {
+	ctx, cancel := context.WithTimeout(gctx, time.Minute)
+	defer cancel()
+	env, err := app.Match(ctx, strings.Split(profiles, ","))
+	if err != nil {
+		return nil, err
+	}
+	var appenv appenv
+	appenv.Load(env)
+	j, err := json.MarshalIndent(appenv, "", "  ")
+	if err != nil {
+		return nil, fmt.Errorf("MarshalIndent(%v) failed: %v", env, err)
+	}
+	return j, nil
+}
+
+func putEnvelopeJSON(app repository.ApplicationClientMethods, profiles string, j []byte) error {
+	var appenv appenv
+	if err := json.Unmarshal(j, &appenv); err != nil {
+		return fmt.Errorf("Unmarshal(%v) failed: %v", string(j), err)
+	}
+	env, err := appenv.Store()
+	if err != nil {
+		return err
+	}
+	ctx, cancel := context.WithTimeout(gctx, time.Minute)
+	defer cancel()
+	if err := app.Put(ctx, strings.Split(profiles, ","), env); err != nil {
+		return err
+	}
+	return nil
+}
+
+func promptUser(cmd *cmdline.Command, msg string) string {
+	fmt.Fprint(cmd.Stdout(), msg)
+	var answer string
+	if _, err := fmt.Scanf("%s", &answer); err != nil {
+		return ""
+	}
+	return answer
+}
+
+var cmdMatch = &cmdline.Command{
+	Run:      runMatch,
+	Name:     "match",
+	Short:    "Shows the first matching envelope that matches the given profiles.",
+	Long:     "Shows the first matching envelope that matches the given profiles.",
+	ArgsName: "<application> <profiles>",
+	ArgsLong: `
+<application> is the full name of the application.
+<profiles> is a comma-separated list of profiles.`,
+}
+
+func runMatch(cmd *cmdline.Command, args []string) error {
+	if expected, got := 2, len(args); expected != got {
+		return cmd.UsageErrorf("match: incorrect number of arguments, expected %d, got %d", expected, got)
+	}
+	name, profiles := args[0], args[1]
+	app := repository.ApplicationClient(name)
+	j, err := getEnvelopeJSON(app, profiles)
+	if err != nil {
+		return err
+	}
+	fmt.Fprintln(cmd.Stdout(), string(j))
+	return nil
+}
+
+var cmdPut = &cmdline.Command{
+	Run:      runPut,
+	Name:     "put",
+	Short:    "Add the given envelope to the application for the given profiles.",
+	Long:     "Add the given envelope to the application for the given profiles.",
+	ArgsName: "<application> <profiles> [<envelope>]",
+	ArgsLong: `
+<application> is the full name of the application.
+<profiles> is a comma-separated list of profiles.
+<envelope> is the file that contains a JSON-encoded envelope. If this file is
+not provided, the user will be prompted to enter the data manually.`,
+}
+
+func runPut(cmd *cmdline.Command, args []string) error {
+	if got := len(args); got != 2 && got != 3 {
+		return cmd.UsageErrorf("put: incorrect number of arguments, expected 2 or 3, got %d", got)
+	}
+	name, profiles := args[0], args[1]
+	app := repository.ApplicationClient(name)
+	if len(args) == 3 {
+		envelope := args[2]
+		j, err := ioutil.ReadFile(envelope)
+		if err != nil {
+			return fmt.Errorf("ReadFile(%v): %v", envelope, err)
+		}
+		if err = putEnvelopeJSON(app, profiles, j); err != nil {
+			return err
+		}
+		fmt.Fprintln(cmd.Stdout(), "Application envelope added successfully.")
+		return nil
+	}
+	env := application.Envelope{Args: []string{}, Env: []string{}, Packages: application.Packages{}}
+	j, err := json.MarshalIndent(env, "", "  ")
+	if err != nil {
+		return fmt.Errorf("MarshalIndent() failed: %v", err)
+	}
+	if err := editAndPutEnvelopeJSON(cmd, app, profiles, j); err != nil {
+		return err
+	}
+	return nil
+}
+
+var cmdRemove = &cmdline.Command{
+	Run:      runRemove,
+	Name:     "remove",
+	Short:    "removes the application envelope for the given profile.",
+	Long:     "removes the application envelope for the given profile.",
+	ArgsName: "<application> <profile>",
+	ArgsLong: `
+<application> is the full name of the application.
+<profile> is a profile.`,
+}
+
+func runRemove(cmd *cmdline.Command, args []string) error {
+	if expected, got := 2, len(args); expected != got {
+		return cmd.UsageErrorf("remove: incorrect number of arguments, expected %d, got %d", expected, got)
+	}
+	name, profile := args[0], args[1]
+	app := repository.ApplicationClient(name)
+	ctx, cancel := context.WithTimeout(gctx, time.Minute)
+	defer cancel()
+	if err := app.Remove(ctx, profile); err != nil {
+		return err
+	}
+	fmt.Fprintln(cmd.Stdout(), "Application envelope removed successfully.")
+	return nil
+}
+
+var cmdEdit = &cmdline.Command{
+	Run:      runEdit,
+	Name:     "edit",
+	Short:    "edits the application envelope for the given profile.",
+	Long:     "edits the application envelope for the given profile.",
+	ArgsName: "<application> <profile>",
+	ArgsLong: `
+<application> is the full name of the application.
+<profile> is a profile.`,
+}
+
+func runEdit(cmd *cmdline.Command, args []string) error {
+	if expected, got := 2, len(args); expected != got {
+		return cmd.UsageErrorf("edit: incorrect number of arguments, expected %d, got %d", expected, got)
+	}
+	name, profile := args[0], args[1]
+	app := repository.ApplicationClient(name)
+
+	envData, err := getEnvelopeJSON(app, profile)
+	if err != nil {
+		return err
+	}
+	if err := editAndPutEnvelopeJSON(cmd, app, profile, envData); err != nil {
+		return err
+	}
+	return nil
+}
+
+func editAndPutEnvelopeJSON(cmd *cmdline.Command, app repository.ApplicationClientMethods, profile string, envData []byte) error {
+	f, err := ioutil.TempFile("", "application-edit-")
+	if err != nil {
+		return fmt.Errorf("TempFile() failed: %v", err)
+	}
+	fileName := f.Name()
+	f.Close()
+	defer os.Remove(fileName)
+	if err = ioutil.WriteFile(fileName, envData, os.FileMode(0644)); err != nil {
+		return err
+	}
+	editor := os.Getenv("EDITOR")
+	if len(editor) == 0 {
+		editor = "nano"
+	}
+	for {
+		c := exec.Command("sh", "-c", fmt.Sprintf("%s %s", editor, fileName))
+		c.Stdin = os.Stdin
+		c.Stdout = os.Stdout
+		c.Stderr = os.Stderr
+		if err := c.Run(); err != nil {
+			return fmt.Errorf("failed to run %s %s", editor, fileName)
+		}
+		newData, err := ioutil.ReadFile(fileName)
+		if err != nil {
+			fmt.Fprintf(cmd.Stdout(), "Error: %v\n", err)
+			if ans := promptUser(cmd, "Try again? [y/N] "); strings.ToUpper(ans) == "Y" {
+				continue
+			}
+			return errors.New("aborted")
+		}
+		if bytes.Compare(envData, newData) == 0 {
+			fmt.Fprintln(cmd.Stdout(), "Nothing changed")
+			return nil
+		}
+		if err = putEnvelopeJSON(app, profile, newData); err != nil {
+			fmt.Fprintf(cmd.Stdout(), "Error: %v\n", err)
+			if ans := promptUser(cmd, "Try again? [y/N] "); strings.ToUpper(ans) == "Y" {
+				continue
+			}
+			return errors.New("aborted")
+		}
+		break
+	}
+	fmt.Fprintln(cmd.Stdout(), "Application envelope updated successfully.")
+	return nil
+}
+
+func root() *cmdline.Command {
+	return &cmdline.Command{
+		Name:  "application",
+		Short: "Tool for interacting with the veyron application repository",
+		Long: `
+The application tool facilitates interaction with the veyron application
+repository.
+`,
+		Children: []*cmdline.Command{cmdMatch, cmdPut, cmdRemove, cmdEdit},
+	}
+}
diff --git a/cmd/application/impl_test.go b/cmd/application/impl_test.go
new file mode 100644
index 0000000..9ef3a08
--- /dev/null
+++ b/cmd/application/impl_test.go
@@ -0,0 +1,214 @@
+package main
+
+import (
+	"bytes"
+	"io/ioutil"
+	"os"
+	"strings"
+	"testing"
+
+	"v.io/v23"
+	"v.io/v23/context"
+	"v.io/v23/ipc"
+	"v.io/v23/naming"
+	"v.io/v23/security"
+	"v.io/v23/services/mgmt/application"
+	"v.io/v23/services/security/access"
+	"v.io/x/lib/vlog"
+
+	"v.io/x/ref/lib/testutil"
+	_ "v.io/x/ref/profiles"
+	"v.io/x/ref/services/mgmt/repository"
+)
+
+var (
+	envelope = application.Envelope{
+		Title:  "fifa world cup",
+		Args:   []string{"arg1", "arg2", "arg3"},
+		Binary: application.SignedFile{File: "/path/to/binary"},
+		Env:    []string{"env1", "env2", "env3"},
+		Packages: map[string]application.SignedFile{
+			"pkg1": application.SignedFile{
+				File: "/path/to/package1",
+			},
+		},
+	}
+	jsonEnv = `{
+  "Title": "fifa world cup",
+  "Args": [
+    "arg1",
+    "arg2",
+    "arg3"
+  ],
+  "Binary": {
+    "File": "/path/to/binary",
+    "Signature": {
+      "Purpose": null,
+      "Hash": "",
+      "R": null,
+      "S": null
+    }
+  },
+  "Publisher": {
+    "CertificateChains": null
+  },
+  "Env": [
+    "env1",
+    "env2",
+    "env3"
+  ],
+  "Packages": {
+    "pkg1": {
+      "File": "/path/to/package1",
+      "Signature": {
+        "Purpose": null,
+        "Hash": "",
+        "R": null,
+        "S": null
+      }
+    }
+  }
+}`
+)
+
+type server struct {
+	suffix string
+}
+
+func (s *server) Match(_ ipc.ServerCall, profiles []string) (application.Envelope, error) {
+	vlog.VI(2).Infof("%v.Match(%v) was called", s.suffix, profiles)
+	return envelope, nil
+}
+
+func (s *server) Put(_ ipc.ServerCall, profiles []string, env application.Envelope) error {
+	vlog.VI(2).Infof("%v.Put(%v, %v) was called", s.suffix, profiles, env)
+	return nil
+}
+
+func (s *server) Remove(_ ipc.ServerCall, profile string) error {
+	vlog.VI(2).Infof("%v.Remove(%v) was called", s.suffix, profile)
+	return nil
+}
+
+func (s *server) SetACL(_ ipc.ServerCall, acl access.TaggedACLMap, etag string) error {
+	vlog.VI(2).Infof("%v.SetACL(%v, %v) was called", acl, etag)
+	return nil
+}
+
+func (s *server) GetACL(ipc.ServerCall) (access.TaggedACLMap, string, error) {
+	vlog.VI(2).Infof("%v.GetACL() was called")
+	return nil, "", nil
+}
+
+type dispatcher struct {
+}
+
+func NewDispatcher() ipc.Dispatcher {
+	return &dispatcher{}
+}
+
+func (d *dispatcher) Lookup(suffix string) (interface{}, security.Authorizer, error) {
+	return repository.ApplicationServer(&server{suffix: suffix}), nil, nil
+}
+
+func startServer(t *testing.T, ctx *context.T) (ipc.Server, naming.Endpoint, error) {
+	dispatcher := NewDispatcher()
+	server, err := v23.NewServer(ctx)
+	if err != nil {
+		t.Errorf("NewServer failed: %v", err)
+		return nil, nil, err
+	}
+	endpoints, err := server.Listen(v23.GetListenSpec(ctx))
+	if err != nil {
+		t.Errorf("Listen failed: %v", err)
+		return nil, nil, err
+	}
+	if err := server.ServeDispatcher("", dispatcher); err != nil {
+		t.Errorf("Serve failed: %v", err)
+		return nil, nil, err
+	}
+	return server, endpoints[0], nil
+}
+
+func stopServer(t *testing.T, server ipc.Server) {
+	if err := server.Stop(); err != nil {
+		t.Errorf("server.Stop failed: %v", err)
+	}
+}
+
+func TestApplicationClient(t *testing.T) {
+	var shutdown v23.Shutdown
+	gctx, shutdown = testutil.InitForTest()
+	defer shutdown()
+
+	server, endpoint, err := startServer(t, gctx)
+	if err != nil {
+		return
+	}
+	defer stopServer(t, server)
+	// Setup the command-line.
+	cmd := root()
+	var stdout, stderr bytes.Buffer
+	cmd.Init(nil, &stdout, &stderr)
+	appName := naming.JoinAddressName(endpoint.String(), "myapp/1")
+	profile := "myprofile"
+
+	// Test the 'Match' command.
+	if err := cmd.Execute([]string{"match", appName, profile}); err != nil {
+		t.Fatalf("%v", err)
+	}
+	if expected, got := jsonEnv, strings.TrimSpace(stdout.String()); got != expected {
+		t.Errorf("Unexpected output from match. Got %q, expected %q", got, expected)
+	}
+	stdout.Reset()
+
+	// Test the 'put' command.
+	f, err := ioutil.TempFile("", "test")
+	if err != nil {
+		t.Fatalf("%v", err)
+	}
+	fileName := f.Name()
+	defer os.Remove(fileName)
+	if _, err = f.Write([]byte(jsonEnv)); err != nil {
+		t.Fatalf("%v", err)
+	}
+	if err = f.Close(); err != nil {
+		t.Fatalf("%v", err)
+	}
+	if err := cmd.Execute([]string{"put", appName, profile, fileName}); err != nil {
+		t.Fatalf("%v", err)
+	}
+	if expected, got := "Application envelope added successfully.", strings.TrimSpace(stdout.String()); got != expected {
+		t.Errorf("Unexpected output from put. Got %q, expected %q", got, expected)
+	}
+	stdout.Reset()
+
+	// Test the 'remove' command.
+	if err := cmd.Execute([]string{"remove", appName, profile}); err != nil {
+		t.Fatalf("%v", err)
+	}
+	if expected, got := "Application envelope removed successfully.", strings.TrimSpace(stdout.String()); got != expected {
+		t.Errorf("Unexpected output from remove. Got %q, expected %q", got, expected)
+	}
+	stdout.Reset()
+
+	// Test the 'edit' command. (nothing changed)
+	os.Setenv("EDITOR", "true")
+	if err := cmd.Execute([]string{"edit", appName, profile}); err != nil {
+		t.Fatalf("%v", err)
+	}
+	if expected, got := "Nothing changed", strings.TrimSpace(stdout.String()); got != expected {
+		t.Errorf("Unexpected output from edit. Got %q, expected %q", got, expected)
+	}
+	stdout.Reset()
+
+	// Test the 'edit' command.
+	os.Setenv("EDITOR", "perl -pi -e 's/arg1/arg111/'")
+	if err := cmd.Execute([]string{"edit", appName, profile}); err != nil {
+		t.Fatalf("%v", err)
+	}
+	if expected, got := "Application envelope updated successfully.", strings.TrimSpace(stdout.String()); got != expected {
+		t.Errorf("Unexpected output from edit. Got %q, expected %q", got, expected)
+	}
+	stdout.Reset()
+}
diff --git a/cmd/application/main.go b/cmd/application/main.go
new file mode 100644
index 0000000..03caf91
--- /dev/null
+++ b/cmd/application/main.go
@@ -0,0 +1,23 @@
+// The following enables go generate to generate the doc.go file.
+//go:generate go run $VANADIUM_ROOT/release/go/src/v.io/x/lib/cmdline/testdata/gendoc.go .
+
+package main
+
+import (
+	"os"
+
+	"v.io/v23"
+	"v.io/v23/context"
+
+	_ "v.io/x/ref/profiles"
+)
+
+var gctx *context.T
+
+func main() {
+	var shutdown v23.Shutdown
+	gctx, shutdown = v23.Init()
+	exitCode := root().Main()
+	shutdown()
+	os.Exit(exitCode)
+}
diff --git a/cmd/binary/doc.go b/cmd/binary/doc.go
new file mode 100644
index 0000000..31712d3
--- /dev/null
+++ b/cmd/binary/doc.go
@@ -0,0 +1,123 @@
+// This file was auto-generated via go generate.
+// DO NOT UPDATE MANUALLY
+
+/*
+The binary tool facilitates interaction with the veyron binary repository.
+
+Usage:
+   binary <command>
+
+The binary commands are:
+   delete      Delete a binary
+   download    Download a binary
+   upload      Upload a binary or directory archive
+   url         Fetch a download URL
+   help        Display help for commands or topics
+Run "binary help [command]" for command usage.
+
+The global flags are:
+ -alsologtostderr=true
+   log to standard error as well as files
+ -log_backtrace_at=:0
+   when logging hits line file:N, emit a stack trace
+ -log_dir=
+   if non-empty, write log files to this directory
+ -logtostderr=false
+   log to standard error instead of files
+ -max_stack_buf_size=4292608
+   max size in bytes of the buffer to use for logging stack traces
+ -stderrthreshold=2
+   logs at or above this threshold go to stderr
+ -v=0
+   log level for V logs
+ -vanadium.i18n_catalogue=
+   18n catalogue files to load, comma separated
+ -veyron.credentials=
+   directory to use for storing security credentials
+ -veyron.namespace.root=[/ns.dev.v.io:8101]
+   local namespace root; can be repeated to provided multiple roots
+ -veyron.proxy=
+   object name of proxy service to use to export services across network
+   boundaries
+ -veyron.tcp.address=
+   address to listen on
+ -veyron.tcp.protocol=tcp
+   protocol to listen with
+ -veyron.vtrace.cache_size=1024
+   The number of vtrace traces to store in memory.
+ -veyron.vtrace.collect_regexp=
+   Spans and annotations that match this regular expression will trigger trace
+   collection.
+ -veyron.vtrace.dump_on_shutdown=true
+   If true, dump all stored traces on runtime shutdown.
+ -veyron.vtrace.sample_rate=0
+   Rate (from 0.0 to 1.0) to sample vtrace traces.
+ -vmodule=
+   comma-separated list of pattern=N settings for file-filtered logging
+
+Binary Delete
+
+Delete connects to the binary repository and deletes the specified binary
+
+Usage:
+   binary delete <von>
+
+<von> is the veyron object name of the binary to delete
+
+Binary Download
+
+Download connects to the binary repository, downloads the specified binary, and
+writes it to a file.
+
+Usage:
+   binary download <von> <filename>
+
+<von> is the veyron object name of the binary to download <filename> is the name
+of the file where the binary will be written
+
+Binary Upload
+
+Upload connects to the binary repository and uploads the binary of the specified
+file or archive of the specified directory. When successful, it writes the name
+of the new binary to stdout.
+
+Usage:
+   binary upload <von> <filename>
+
+<von> is the veyron object name of the binary to upload <filename> is the name
+of the file or directory to upload
+
+Binary Url
+
+Connect to the binary repository and fetch the download URL for the given veyron
+object name.
+
+Usage:
+   binary url <von>
+
+<von> is the veyron object name of the binary repository
+
+Binary Help
+
+Help with no args displays the usage of the parent command.
+
+Help with args displays the usage of the specified sub-command or help topic.
+
+"help ..." recursively displays help for all commands and topics.
+
+The output is formatted to a target width in runes.  The target width is
+determined by checking the environment variable CMDLINE_WIDTH, falling back on
+the terminal width from the OS, falling back on 80 chars.  By setting
+CMDLINE_WIDTH=x, if x > 0 the width is x, if x < 0 the width is unlimited, and
+if x == 0 or is unset one of the fallbacks is used.
+
+Usage:
+   binary help [flags] [command/topic ...]
+
+[command/topic ...] optionally identifies a specific sub-command or help topic.
+
+The binary help flags are:
+ -style=text
+   The formatting style for help output, either "text" or "godoc".
+*/
+package main
diff --git a/cmd/binary/impl.go b/cmd/binary/impl.go
new file mode 100644
index 0000000..2739c01
--- /dev/null
+++ b/cmd/binary/impl.go
@@ -0,0 +1,130 @@
+package main
+
+import (
+	"fmt"
+	"os"
+
+	"v.io/x/lib/cmdline"
+	"v.io/x/ref/services/mgmt/lib/binary"
+)
+
+var cmdDelete = &cmdline.Command{
+	Run:      runDelete,
+	Name:     "delete",
+	Short:    "Delete a binary",
+	Long:     "Delete connects to the binary repository and deletes the specified binary",
+	ArgsName: "<von>",
+	ArgsLong: "<von> is the veyron object name of the binary to delete",
+}
+
+func runDelete(cmd *cmdline.Command, args []string) error {
+	if expected, got := 1, len(args); expected != got {
+		return cmd.UsageErrorf("delete: incorrect number of arguments, expected %d, got %d", expected, got)
+	}
+	von := args[0]
+	if err := binary.Delete(gctx, von); err != nil {
+		return err
+	}
+	fmt.Fprintf(cmd.Stdout(), "Binary deleted successfully\n")
+	return nil
+}
+
+var cmdDownload = &cmdline.Command{
+	Run:   runDownload,
+	Name:  "download",
+	Short: "Download a binary",
+	Long: `
+Download connects to the binary repository, downloads the specified binary, and
+writes it to a file.
+`,
+	ArgsName: "<von> <filename>",
+	ArgsLong: `
+<von> is the veyron object name of the binary to download
+<filename> is the name of the file where the binary will be written
+`,
+}
+
+func runDownload(cmd *cmdline.Command, args []string) error {
+	if expected, got := 2, len(args); expected != got {
+		return cmd.UsageErrorf("download: incorrect number of arguments, expected %d, got %d", expected, got)
+	}
+	von, filename := args[0], args[1]
+	if err := binary.DownloadToFile(gctx, von, filename); err != nil {
+		return err
+	}
+	fmt.Fprintf(cmd.Stdout(), "Binary downloaded to file %s\n", filename)
+	return nil
+}
+
+var cmdUpload = &cmdline.Command{
+	Run:   runUpload,
+	Name:  "upload",
+	Short: "Upload a binary or directory archive",
+	Long: `
+Upload connects to the binary repository and uploads the binary of the specified
+file or archive of the specified directory. When successful, it writes the name of the new binary to stdout.
+`,
+	ArgsName: "<von> <filename>",
+	ArgsLong: `
+<von> is the veyron object name of the binary to upload
+<filename> is the name of the file or directory to upload
+`,
+}
+
+func runUpload(cmd *cmdline.Command, args []string) error {
+	if expected, got := 2, len(args); expected != got {
+		return cmd.UsageErrorf("upload: incorrect number of arguments, expected %d, got %d", expected, got)
+	}
+	von, filename := args[0], args[1]
+	fi, err := os.Stat(filename)
+	if err != nil {
+		return err
+	}
+	if fi.IsDir() {
+		sig, err := binary.UploadFromDir(gctx, von, filename)
+		if err != nil {
+			return err
+		}
+		fmt.Fprintf(cmd.Stdout(), "Binary package uploaded from directory %s signature(%v)\n", filename, sig)
+		return nil
+	}
+	sig, err := binary.UploadFromFile(gctx, von, filename)
+	if err != nil {
+		return err
+	}
+	fmt.Fprintf(cmd.Stdout(), "Binary uploaded from file %s signature(%v)\n", filename, sig)
+	return nil
+}
+
+var cmdURL = &cmdline.Command{
+	Run:      runURL,
+	Name:     "url",
+	Short:    "Fetch a download URL",
+	Long:     "Connect to the binary repository and fetch the download URL for the given veyron object name.",
+	ArgsName: "<von>",
+	ArgsLong: "<von> is the veyron object name of the binary repository",
+}
+
+func runURL(cmd *cmdline.Command, args []string) error {
+	if expected, got := 1, len(args); expected != got {
+		return cmd.UsageErrorf("rooturl: incorrect number of arguments, expected %d, got %d", expected, got)
+	}
+	von := args[0]
+	url, _, err := binary.DownloadURL(gctx, von)
+	if err != nil {
+		return err
+	}
+	fmt.Fprintf(cmd.Stdout(), "%v\n", url)
+	return nil
+}
+
+func root() *cmdline.Command {
+	return &cmdline.Command{
+		Name:  "binary",
+		Short: "Tool for interacting with the veyron binary repository",
+		Long: `
+The binary tool facilitates interaction with the veyron binary repository.
+`,
+		Children: []*cmdline.Command{cmdDelete, cmdDownload, cmdUpload, cmdURL},
+	}
+}
diff --git a/cmd/binary/impl_test.go b/cmd/binary/impl_test.go
new file mode 100644
index 0000000..e46f0a3
--- /dev/null
+++ b/cmd/binary/impl_test.go
@@ -0,0 +1,183 @@
+package main
+
+import (
+	"bytes"
+	"crypto/md5"
+	"encoding/hex"
+	"fmt"
+	"io/ioutil"
+	"os"
+	"path"
+	"strings"
+	"testing"
+
+	"v.io/v23"
+	"v.io/v23/context"
+	"v.io/v23/ipc"
+	"v.io/v23/naming"
+	"v.io/v23/security"
+	"v.io/v23/services/mgmt/binary"
+	"v.io/v23/services/mgmt/repository"
+	"v.io/v23/services/security/access"
+	"v.io/x/lib/vlog"
+
+	"v.io/x/ref/lib/testutil"
+	_ "v.io/x/ref/profiles"
+)
+
+type server struct {
+	suffix string
+}
+
+func (s *server) Create(ipc.ServerCall, int32, repository.MediaInfo) error {
+	vlog.Infof("Create() was called. suffix=%v", s.suffix)
+	return nil
+}
+
+func (s *server) Delete(ipc.ServerCall) error {
+	vlog.Infof("Delete() was called. suffix=%v", s.suffix)
+	if s.suffix != "exists" {
+		return fmt.Errorf("binary doesn't exist: %v", s.suffix)
+	}
+	return nil
+}
+
+func (s *server) Download(ctx repository.BinaryDownloadContext, _ int32) error {
+	vlog.Infof("Download() was called. suffix=%v", s.suffix)
+	sender := ctx.SendStream()
+	sender.Send([]byte("Hello"))
+	sender.Send([]byte("World"))
+	return nil
+}
+
+func (s *server) DownloadURL(ipc.ServerCall) (string, int64, error) {
+	vlog.Infof("DownloadURL() was called. suffix=%v", s.suffix)
+	if s.suffix != "" {
+		return "", 0, fmt.Errorf("non-empty suffix: %v", s.suffix)
+	}
+	return "test-download-url", 0, nil
+}
+
+func (s *server) Stat(ipc.ServerCall) ([]binary.PartInfo, repository.MediaInfo, error) {
+	vlog.Infof("Stat() was called. suffix=%v", s.suffix)
+	h := md5.New()
+	text := "HelloWorld"
+	h.Write([]byte(text))
+	part := binary.PartInfo{Checksum: hex.EncodeToString(h.Sum(nil)), Size: int64(len(text))}
+	return []binary.PartInfo{part}, repository.MediaInfo{Type: "text/plain"}, nil
+}
+
+func (s *server) Upload(ctx repository.BinaryUploadContext, _ int32) error {
+	vlog.Infof("Upload() was called. suffix=%v", s.suffix)
+	rStream := ctx.RecvStream()
+	for rStream.Advance() {
+	}
+	return nil
+}
+
+func (s *server) GetACL(ipc.ServerCall) (acl access.TaggedACLMap, etag string, err error) {
+	return nil, "", nil
+}
+
+func (s *server) SetACL(ctx ipc.ServerCall, acl access.TaggedACLMap, etag string) error {
+	return nil
+}
+
+type dispatcher struct {
+}
+
+func NewDispatcher() ipc.Dispatcher {
+	return &dispatcher{}
+}
+
+func (d *dispatcher) Lookup(suffix string) (interface{}, security.Authorizer, error) {
+	return repository.BinaryServer(&server{suffix: suffix}), nil, nil
+}
+
+func startServer(t *testing.T, ctx *context.T) (ipc.Server, naming.Endpoint, error) {
+	dispatcher := NewDispatcher()
+	server, err := v23.NewServer(ctx)
+	if err != nil {
+		t.Errorf("NewServer failed: %v", err)
+		return nil, nil, err
+	}
+	endpoints, err := server.Listen(v23.GetListenSpec(ctx))
+	if err != nil {
+		t.Errorf("Listen failed: %v", err)
+		return nil, nil, err
+	}
+	if err := server.ServeDispatcher("", dispatcher); err != nil {
+		t.Errorf("ServeDispatcher failed: %v", err)
+		return nil, nil, err
+	}
+	return server, endpoints[0], nil
+}
+
+func stopServer(t *testing.T, server ipc.Server) {
+	if err := server.Stop(); err != nil {
+		t.Errorf("server.Stop failed: %v", err)
+	}
+}
+
+func TestBinaryClient(t *testing.T) {
+	var shutdown v23.Shutdown
+	gctx, shutdown = testutil.InitForTest()
+	defer shutdown()
+
+	server, endpoint, err := startServer(t, gctx)
+	if err != nil {
+		return
+	}
+	defer stopServer(t, server)
+
+	// Setup the command-line.
+	cmd := root()
+	var out bytes.Buffer
+	cmd.Init(nil, &out, &out)
+
+	// Test the 'delete' command.
+	if err := cmd.Execute([]string{"delete", naming.JoinAddressName(endpoint.String(), "exists")}); err != nil {
+		t.Fatalf("%v failed: %v\n%v", "delete", err, out.String())
+	}
+	if expected, got := "Binary deleted successfully", strings.TrimSpace(out.String()); got != expected {
+		t.Errorf("Got %q, expected %q", got, expected)
+	}
+	out.Reset()
+
+	// Test the 'download' command.
+	dir, err := ioutil.TempDir("", "binaryimpltest")
+	if err != nil {
+		t.Fatalf("%v", err)
+	}
+	defer os.RemoveAll(dir)
+	file := path.Join(dir, "testfile")
+	defer os.Remove(file)
+	if err := cmd.Execute([]string{"download", naming.JoinAddressName(endpoint.String(), "exists"), file}); err != nil {
+		t.Fatalf("%v failed: %v\n%v", "download", err, out.String())
+	}
+	if expected, got := "Binary downloaded to file "+file, strings.TrimSpace(out.String()); got != expected {
+		t.Errorf("Got %q, expected %q", got, expected)
+	}
+	buf, err := ioutil.ReadFile(file)
+	if err != nil {
+		t.Fatalf("%v", err)
+	}
+	if expected := "HelloWorld"; string(buf) != expected {
+		t.Errorf("Got %q, expected %q", string(buf), expected)
+	}
+	out.Reset()
+
+	// Test the 'upload' command.
+	if err := cmd.Execute([]string{"upload", naming.JoinAddressName(endpoint.String(), "exists"), file}); err != nil {
+		t.Fatalf("%v failed: %v\n%v", "upload", err, out.String())
+	}
+	out.Reset()
+
+	// Test the 'url' command.
+	if err := cmd.Execute([]string{"url", naming.JoinAddressName(endpoint.String(), "")}); err != nil {
+		t.Fatalf("%v failed: %v\n%v", "url", err, out.String())
+	}
+	if expected, got := "test-download-url", strings.TrimSpace(out.String()); got != expected {
+		t.Errorf("Got %q, expected %q", got, expected)
+	}
+}
diff --git a/cmd/binary/main.go b/cmd/binary/main.go
new file mode 100644
index 0000000..03caf91
--- /dev/null
+++ b/cmd/binary/main.go
@@ -0,0 +1,23 @@
+// The following enables go generate to generate the doc.go file.
+//go:generate go run $VANADIUM_ROOT/release/go/src/v.io/x/lib/cmdline/testdata/gendoc.go .
+
+package main
+
+import (
+	"os"
+
+	"v.io/v23"
+	"v.io/v23/context"
+
+	_ "v.io/x/ref/profiles"
+)
+
+var gctx *context.T
+
+func main() {
+	var shutdown v23.Shutdown
+	gctx, shutdown = v23.Init()
+	exitCode := root().Main()
+	shutdown()
+	os.Exit(exitCode)
+}
diff --git a/cmd/build/doc.go b/cmd/build/doc.go
new file mode 100644
index 0000000..40cd568
--- /dev/null
+++ b/cmd/build/doc.go
@@ -0,0 +1,100 @@
+// This file was auto-generated via go generate.
+// DO NOT UPDATE MANUALLY
+
+/*
+The build tool tool facilitates interaction with the veyron build server.
+
+Usage:
+   build <command>
+
+The build commands are:
+   build       Build veyron Go packages
+   help        Display help for commands or topics
+Run "build help [command]" for command usage.
+
+The global flags are:
+ -alsologtostderr=true
+   log to standard error as well as files
+ -log_backtrace_at=:0
+   when logging hits line file:N, emit a stack trace
+ -log_dir=
+   if non-empty, write log files to this directory
+ -logtostderr=false
+   log to standard error instead of files
+ -max_stack_buf_size=4292608
+   max size in bytes of the buffer to use for logging stack traces
+ -stderrthreshold=2
+   logs at or above this threshold go to stderr
+ -v=0
+   log level for V logs
+ -vanadium.i18n_catalogue=
+   18n catalogue files to load, comma separated
+ -veyron.credentials=
+   directory to use for storing security credentials
+ -veyron.namespace.root=[/ns.dev.v.io:8101]
+   local namespace root; can be repeated to provided multiple roots
+ -veyron.proxy=
+   object name of proxy service to use to export services across network
+   boundaries
+ -veyron.tcp.address=
+   address to listen on
+ -veyron.tcp.protocol=tcp
+   protocol to listen with
+ -veyron.vtrace.cache_size=1024
+   The number of vtrace traces to store in memory.
+ -veyron.vtrace.collect_regexp=
+   Spans and annotations that match this regular expression will trigger trace
+   collection.
+ -veyron.vtrace.dump_on_shutdown=true
+   If true, dump all stored traces on runtime shutdown.
+ -veyron.vtrace.sample_rate=0
+   Rate (from 0.0 to 1.0) to sample vtrace traces.
+ -vmodule=
+   comma-separated list of pattern=N settings for file-filtered logging
+
+Build Build
+
+Build veyron Go packages using a remote build server. The command collects all
+source code files that are not part of the Go standard library that the target
+packages depend on, sends them to a build server, and receives the built
+binaries.
+
+Usage:
+   build build [flags] <name> <packages>
+
+<name> is a veyron object name of a build server <packages> is a list of
+packages to build, specified as arguments for each command. The format is
+similar to the go tool.  In its simplest form each package is an import path;
+e.g. "veyron/tools/build". A package that ends with "..." does a wildcard match
+against all packages with that prefix.
+
+The build build flags are:
+ -arch=$GOARCH
+   Target architecture.
+ -os=$GOOS
+   Target operating system.
+
+Build Help
+
+Help with no args displays the usage of the parent command.
+
+Help with args displays the usage of the specified sub-command or help topic.
+
+"help ..." recursively displays help for all commands and topics.
+
+The output is formatted to a target width in runes.  The target width is
+determined by checking the environment variable CMDLINE_WIDTH, falling back on
+the terminal width from the OS, falling back on 80 chars.  By setting
+CMDLINE_WIDTH=x, if x > 0 the width is x, if x < 0 the width is unlimited, and
+if x == 0 or is unset one of the fallbacks is used.
+
+Usage:
+   build help [flags] [command/topic ...]
+
+[command/topic ...] optionally identifies a specific sub-command or help topic.
+
+The build help flags are:
+ -style=text
+   The formatting style for help output, either "text" or "godoc".
+*/
+package main
diff --git a/cmd/build/impl.go b/cmd/build/impl.go
new file mode 100644
index 0000000..f2fc050
--- /dev/null
+++ b/cmd/build/impl.go
@@ -0,0 +1,253 @@
+package main
+
+import (
+	"fmt"
+	"go/build"
+	"io/ioutil"
+	"os"
+	"path/filepath"
+	"runtime"
+	"strings"
+	"time"
+
+	"v.io/v23/context"
+	vbuild "v.io/v23/services/mgmt/build"
+	"v.io/x/lib/cmdline"
+)
+
+const (
+	defaultArch = "$GOARCH"
+	defaultOS   = "$GOOS"
+)
+
+var (
+	flagArch string
+	flagOS   string
+)
+
+func init() {
+	cmdBuild.Flags.StringVar(&flagArch, "arch", defaultArch, "Target architecture.")
+	cmdBuild.Flags.StringVar(&flagOS, "os", defaultOS, "Target operating system.")
+}
+
+// substituteVarsInFlags substitutes environment variables in default
+// values of relevant flags.
+func substituteVarsInFlags() {
+	if flagArch == defaultArch {
+		flagArch = runtime.GOARCH
+	}
+	if flagOS == defaultOS {
+		flagOS = runtime.GOOS
+	}
+}
+
+var cmdRoot = &cmdline.Command{
+	Name:  "build",
+	Short: "Tool for interacting with the veyron build server",
+	Long: `
+The build tool tool facilitates interaction with the veyron build server.
+`,
+	Children: []*cmdline.Command{cmdBuild},
+}
+
+// root returns a command that represents the root of the veyron tool.
+func root() *cmdline.Command {
+	return cmdRoot
+}
+
+var cmdBuild = &cmdline.Command{
+	Run:   runBuild,
+	Name:  "build",
+	Short: "Build veyron Go packages",
+	Long: `
+Build veyron Go packages using a remote build server. The command
+collects all source code files that are not part of the Go standard
+library that the target packages depend on, sends them to a build
+server, and receives the built binaries.
+`,
+	ArgsName: "<name> <packages>",
+	ArgsLong: `
+<name> is a veyron object name of a build server
+<packages> is a list of packages to build, specified as arguments for
+each command. The format is similar to the go tool.  In its simplest
+form each package is an import path; e.g. "veyron/tools/build". A
+package that ends with "..." does a wildcard match against all
+packages with that prefix.
+`,
+}
+
+// TODO(jsimsa): Add support for importing (and remotely building)
+// packages from multiple package source root GOPATH directories with
+// identical names.
+func importPackages(paths []string, pkgMap map[string]*build.Package) error {
+	for _, path := range paths {
+		recurse := false
+		if strings.HasSuffix(path, "...") {
+			recurse = true
+			path = strings.TrimSuffix(path, "...")
+		}
+		if _, exists := pkgMap[path]; !exists {
+			srcDir, mode := "", build.ImportMode(0)
+			pkg, err := build.Import(path, srcDir, mode)
+			if err != nil {
+				// "C" is a pseudo-package for cgo: http://golang.org/cmd/cgo/
+				// Do not attempt recursive imports.
+				if pkg.ImportPath == "C" {
+					continue
+				}
+				return fmt.Errorf("Import(%q,%q,%v) failed: %v", path, srcDir, mode, err)
+			}
+			if pkg.Goroot {
+				continue
+			}
+			pkgMap[path] = pkg
+			if err := importPackages(pkg.Imports, pkgMap); err != nil {
+				return err
+			}
+		}
+		if recurse {
+			pkg := pkgMap[path]
+			fis, err := ioutil.ReadDir(pkg.Dir)
+			if err != nil {
+				return fmt.Errorf("ReadDir(%v) failed: %v", pkg.Dir)
+			}
+			for _, fi := range fis {
+				if fi.IsDir() {
+					subPath := filepath.Join(path, fi.Name(), "...")
+					if err := importPackages([]string{subPath}, pkgMap); err != nil {
+						return err
+					}
+				}
+			}
+		}
+	}
+	return nil
+}
+
+func getSources(ctx *context.T, pkgMap map[string]*build.Package, errchan chan<- error) <-chan vbuild.File {
+	sources := make(chan vbuild.File)
+	go func() {
+		defer close(sources)
+		for _, pkg := range pkgMap {
+			for _, files := range [][]string{pkg.CFiles, pkg.CgoFiles, pkg.GoFiles, pkg.SFiles} {
+				for _, file := range files {
+					path := filepath.Join(pkg.Dir, file)
+					bytes, err := ioutil.ReadFile(path)
+					if err != nil {
+						errchan <- fmt.Errorf("ReadFile(%v) failed: %v", path, err)
+						return
+					}
+					select {
+					case sources <- vbuild.File{Contents: bytes, Name: filepath.Join(pkg.ImportPath, file)}:
+					case <-ctx.Done():
+						errchan <- fmt.Errorf("Get sources failed: %v", ctx.Err())
+						return
+					}
+				}
+			}
+		}
+		errchan <- nil
+	}()
+	return sources
+}
+
+func invokeBuild(ctx *context.T, name string, sources <-chan vbuild.File, errchan chan<- error) <-chan vbuild.File {
+	binaries := make(chan vbuild.File)
+	go func() {
+		defer close(binaries)
+		ctx, cancel := context.WithCancel(ctx)
+		defer cancel()
+
+		client := vbuild.BuilderClient(name)
+		stream, err := client.Build(ctx, vbuild.Architecture(flagArch), vbuild.OperatingSystem(flagOS))
+		if err != nil {
+			errchan <- fmt.Errorf("Build() failed: %v", err)
+			return
+		}
+		sender := stream.SendStream()
+		for source := range sources {
+			if err := sender.Send(source); err != nil {
+				errchan <- fmt.Errorf("Send() failed: %v", err)
+				return
+			}
+		}
+		if err := sender.Close(); err != nil {
+			errchan <- fmt.Errorf("Close() failed: %v", err)
+			return
+		}
+		iterator := stream.RecvStream()
+		for iterator.Advance() {
+			select {
+			case binaries <- iterator.Value():
+			case <-ctx.Done():
+				errchan <- fmt.Errorf("Invoke build failed: %v", ctx.Err())
+				return
+			}
+		}
+		if err := iterator.Err(); err != nil {
+			errchan <- fmt.Errorf("Advance() failed: %v", err)
+			return
+		}
+		if out, err := stream.Finish(); err != nil {
+			errchan <- fmt.Errorf("Finish() failed: (%v, %v)", string(out), err)
+			return
+		}
+		errchan <- nil
+	}()
+	return binaries
+}
+
+func saveBinaries(ctx *context.T, prefix string, binaries <-chan vbuild.File, errchan chan<- error) {
+	go func() {
+		for binary := range binaries {
+			select {
+			case <-ctx.Done():
+				errchan <- fmt.Errorf("Save binaries failed: %v", ctx.Err())
+				return
+			default:
+			}
+			path, perm := filepath.Join(prefix, filepath.Base(binary.Name)), os.FileMode(0755)
+			if err := ioutil.WriteFile(path, binary.Contents, perm); err != nil {
+				errchan <- fmt.Errorf("WriteFile(%v, %v) failed: %v", path, perm, err)
+				return
+			}
+			fmt.Printf("Generated binary %v\n", path)
+		}
+		errchan <- nil
+	}()
+}
+
+// runBuild identifies the source files needed to build the packages
+// specified on command-line and then creates a pipeline that
+// concurrently 1) reads the source files, 2) sends them to the build
+// server and receives binaries from the build server, and 3) writes
+// the binaries out to the disk.
+func runBuild(command *cmdline.Command, args []string) error {
+	name, paths := args[0], args[1:]
+	pkgMap := map[string]*build.Package{}
+	if err := importPackages(paths, pkgMap); err != nil {
+		return err
+	}
+	errchan := make(chan error)
+	defer close(errchan)
+
+	ctx, ctxCancel := context.WithTimeout(gctx, time.Minute)
+	defer ctxCancel()
+
+	// Start all stages of the pipeline.
+	sources := getSources(ctx, pkgMap, errchan)
+	binaries := invokeBuild(ctx, name, sources, errchan)
+	saveBinaries(ctx, os.TempDir(), binaries, errchan)
+	// Wait for all stages of the pipeline to terminate.
+	errors, numStages := []error{}, 3
+	for i := 0; i < numStages; i++ {
+		if err := <-errchan; err != nil {
+			errors = append(errors, err)
+			ctxCancel()
+		}
+	}
+	if len(errors) != 0 {
+		return fmt.Errorf("build failed(%v)", errors)
+	}
+	return nil
+}
diff --git a/cmd/build/impl_test.go b/cmd/build/impl_test.go
new file mode 100644
index 0000000..1213aef
--- /dev/null
+++ b/cmd/build/impl_test.go
@@ -0,0 +1,84 @@
+package main
+
+import (
+	"bytes"
+	"strings"
+	"testing"
+
+	"v.io/v23"
+	"v.io/v23/context"
+	"v.io/v23/ipc"
+	"v.io/v23/naming"
+	"v.io/v23/services/mgmt/binary"
+	"v.io/v23/services/mgmt/build"
+	"v.io/v23/verror"
+	"v.io/x/lib/vlog"
+
+	"v.io/x/ref/lib/testutil"
+	_ "v.io/x/ref/profiles"
+)
+
+type mock struct{}
+
+func (mock) Build(ctx build.BuilderBuildContext, arch build.Architecture, opsys build.OperatingSystem) ([]byte, error) {
+	vlog.VI(2).Infof("Build(%v, %v) was called", arch, opsys)
+	iterator := ctx.RecvStream()
+	for iterator.Advance() {
+	}
+	if err := iterator.Err(); err != nil {
+		vlog.Errorf("Advance() failed: %v", err)
+		return nil, verror.New(verror.ErrInternal, ctx.Context())
+	}
+	return nil, nil
+}
+
+func (mock) Describe(_ ipc.ServerCall, name string) (binary.Description, error) {
+	vlog.VI(2).Infof("Describe(%v) was called", name)
+	return binary.Description{}, nil
+}
+
+type dispatcher struct{}
+
+func startServer(ctx *context.T, t *testing.T) (ipc.Server, naming.Endpoint) {
+	server, err := v23.NewServer(ctx)
+	if err != nil {
+		t.Fatalf("NewServer failed: %v", err)
+	}
+	l := v23.GetListenSpec(ctx)
+	endpoints, err := server.Listen(l)
+	if err != nil {
+		t.Fatalf("Listen(%s) failed: %v", l, err)
+	}
+	unpublished := ""
+	if err := server.Serve(unpublished, build.BuilderServer(&mock{}), nil); err != nil {
+		t.Fatalf("Serve(%v) failed: %v", unpublished, err)
+	}
+	return server, endpoints[0]
+}
+
+func stopServer(t *testing.T, server ipc.Server) {
+	if err := server.Stop(); err != nil {
+		t.Errorf("Stop() failed: %v", err)
+	}
+}
+
+func TestBuildClient(t *testing.T) {
+	var shutdown v23.Shutdown
+	gctx, shutdown = testutil.InitForTest()
+	defer shutdown()
+
+	server, endpoint := startServer(gctx, t)
+	defer stopServer(t, server)
+
+	cmd := root()
+	var stdout, stderr bytes.Buffer
+	cmd.Init(nil, &stdout, &stderr)
+
+	// Test the 'Build' command.
+	if err := cmd.Execute([]string{"build", naming.JoinAddressName(endpoint.String(), ""), "v.io/x/ref/cmd/build"}); err != nil {
+		t.Fatalf("%v", err)
+	}
+	if expected, got := "", strings.TrimSpace(stdout.String()); got != expected {
+		t.Errorf("Unexpected output from build: got %q, expected %q", got, expected)
+	}
+}
diff --git a/cmd/build/main.go b/cmd/build/main.go
new file mode 100644
index 0000000..6d3c698
--- /dev/null
+++ b/cmd/build/main.go
@@ -0,0 +1,24 @@
+// The following enables go generate to generate the doc.go file.
+//go:generate go run $VANADIUM_ROOT/release/go/src/v.io/x/lib/cmdline/testdata/gendoc.go .
+
+package main
+
+import (
+	"os"
+
+	"v.io/v23"
+	"v.io/v23/context"
+
+	_ "v.io/x/ref/profiles"
+)
+
+var gctx *context.T
+
+func main() {
+	var shutdown v23.Shutdown
+	gctx, shutdown = v23.Init()
+	substituteVarsInFlags()
+	exitCode := root().Main()
+	shutdown()
+	os.Exit(exitCode)
+}
diff --git a/cmd/debug/debug_v23_test.go b/cmd/debug/debug_v23_test.go
new file mode 100644
index 0000000..a56051c
--- /dev/null
+++ b/cmd/debug/debug_v23_test.go
@@ -0,0 +1,216 @@
+package main_test
+
+import (
+	"bufio"
+	"fmt"
+	"os"
+	"path/filepath"
+	"regexp"
+	"strconv"
+	"strings"
+	"time"
+
+	"v.io/x/ref/lib/testutil/v23tests"
+)
+
+//go:generate v23 test generate
+
+func V23TestDebugGlob(i *v23tests.T) {
+	v23tests.RunRootMT(i, "--veyron.tcp.address=127.0.0.1:0")
+
+	binary := i.BuildGoPkg("v.io/x/ref/cmd/debug")
+	inv := binary.Start("glob", "__debug/*")
+
+	var want string
+	for _, entry := range []string{"logs", "pprof", "stats", "vtrace"} {
+		want += "__debug/" + entry + "\n"
+	}
+	if got := inv.Output(); got != want {
+		i.Fatalf("unexpected output, want %s, got %s", want, got)
+	}
+}
+
+func V23TestDebugGlobLogs(i *v23tests.T) {
+	v23tests.RunRootMT(i, "--veyron.tcp.address=127.0.0.1:0")
+
+	// Create a temp file before we list the logs.
+	fileName := filepath.Base(i.NewTempFile().Name())
+	binary := i.BuildGoPkg("v.io/x/ref/cmd/debug")
+	output := binary.Start("glob", "__debug/logs/*").Output()
+
+	// The output should contain the filename.
+	want := "/logs/" + fileName
+	if !strings.Contains(output, want) {
+		i.Fatalf("output should contain %s but did not\n%s", want, output)
+	}
+}
+
+func V23TestReadHostname(i *v23tests.T) {
+	v23tests.RunRootMT(i, "--veyron.tcp.address=127.0.0.1:0")
+
+	path := "__debug/stats/system/hostname"
+	binary := i.BuildGoPkg("v.io/x/ref/cmd/debug")
+	got := binary.Start("stats", "read", path).Output()
+	hostname, err := os.Hostname()
+	if err != nil {
+		i.Fatalf("Hostname() failed: %v", err)
+	}
+	if want := path + ": " + hostname + "\n"; got != want {
+		i.Fatalf("unexpected output, want %q, got %q", want, got)
+	}
+}
+
+func createTestLogFile(i *v23tests.T, content string) *os.File {
+	file := i.NewTempFile()
+	_, err := file.Write([]byte(content))
+	if err != nil {
+		i.Fatalf("Write failed: %v", err)
+	}
+	return file
+}
+
+func V23TestLogSize(i *v23tests.T) {
+	v23tests.RunRootMT(i, "--veyron.tcp.address=127.0.0.1:0")
+
+	binary := i.BuildGoPkg("v.io/x/ref/cmd/debug")
+	testLogData := "This is a test log file"
+	file := createTestLogFile(i, testLogData)
+
+	// Check to ensure the file size is accurate
+	str := strings.TrimSpace(binary.Start("logs", "size", "__debug/logs/"+filepath.Base(file.Name())).Output())
+	got, err := strconv.Atoi(str)
+	if err != nil {
+		i.Fatalf("Atoi(\"%q\") failed", str)
+	}
+	want := len(testLogData)
+	if got != want {
+		i.Fatalf("unexpected output, want %d, got %d", got, want)
+	}
+}
+
+func V23TestStatsRead(i *v23tests.T) {
+	v23tests.RunRootMT(i, "--veyron.tcp.address=127.0.0.1:0")
+
+	binary := i.BuildGoPkg("v.io/x/ref/cmd/debug")
+	testLogData := "This is a test log file\n"
+	file := createTestLogFile(i, testLogData)
+	logName := filepath.Base(file.Name())
+	runCount := 12
+	for i := 0; i < runCount; i++ {
+		binary.Start("logs", "read", "__debug/logs/"+logName).WaitOrDie(nil, nil)
+	}
+
+	got := binary.Start("stats", "read", "__debug/stats/ipc/server/routing-id/*/methods/ReadLog/latency-ms").Output()
+
+	want := fmt.Sprintf("Count: %d", runCount)
+	if !strings.Contains(got, want) {
+		i.Fatalf("expected output to contain %s, but did not\n", want, got)
+	}
+}
+
+func V23TestStatsWatch(i *v23tests.T) {
+	v23tests.RunRootMT(i, "--veyron.tcp.address=127.0.0.1:0")
+
+	binary := i.BuildGoPkg("v.io/x/ref/cmd/debug")
+	testLogData := "This is a test log file\n"
+	file := createTestLogFile(i, testLogData)
+	logName := filepath.Base(file.Name())
+	binary.Start("logs", "read", "__debug/logs/"+logName).WaitOrDie(nil, nil)
+
+	inv := binary.Start("stats", "watch", "-raw", "__debug/stats/ipc/server/routing-id/*/methods/ReadLog/latency-ms")
+
+	lineChan := make(chan string)
+	// Go off and read the invocation's stdout.
+	go func() {
+		line, err := bufio.NewReader(inv.Stdout()).ReadString('\n')
+		if err != nil {
+			i.Fatalf("Could not read line from invocation")
+		}
+		lineChan <- line
+	}()
+
+	// Wait up to 10 seconds for some stats output. Either some output
+	// occurs or the timeout expires without any output.
+	select {
+	case <-time.After(10 * time.Second):
+		i.Errorf("Timed out waiting for output")
+	case got := <-lineChan:
+		// Expect one ReadLog call to have occurred.
+		want := "}}{Count: 1"
+		if !strings.Contains(got, want) {
+			i.Errorf("wanted but could not find %q in output\n%s", want, got)
+		}
+	}
+}
+
+func performTracedRead(debugBinary *v23tests.Binary, path string) string {
+	return debugBinary.Start("--veyron.vtrace.sample_rate=1", "logs", "read", path).Output()
+}
+
+func V23TestVTrace(i *v23tests.T) {
+	v23tests.RunRootMT(i, "--veyron.tcp.address=127.0.0.1:0")
+
+	binary := i.BuildGoPkg("v.io/x/ref/cmd/debug")
+	logContent := "Hello, world!\n"
+	logPath := "__debug/logs/" + filepath.Base(createTestLogFile(i, logContent).Name())
+	// Create a log file with tracing, read it and check that the resulting trace exists.
+	got := performTracedRead(binary, logPath)
+	if logContent != got {
+		i.Fatalf("unexpected output: want %s, got %s", logContent, got)
+	}
+
+	// Grab the ID of the first and only trace.
+	want, traceContent := 1, binary.Start("vtrace", "__debug/vtrace").Output()
+	if count := strings.Count(traceContent, "Trace -"); count != want {
+		i.Fatalf("unexpected trace count, want %d, got %d\n%s", want, count, traceContent)
+	}
+	fields := strings.Split(traceContent, " ")
+	if len(fields) < 3 {
+		i.Fatalf("expected at least 3 space-delimited fields, got %d\n", len(fields), traceContent)
+	}
+	traceId := fields[2]
+
+	// Do a sanity check on the trace ID: it should be a 32-character hex ID prefixed with 0x
+	if match, _ := regexp.MatchString("0x[0-9a-f]{32}", traceId); !match {
+		i.Fatalf("wanted a 32-character hex ID prefixed with 0x, got %s", traceId)
+	}
+
+	// Do another traced read, this will generate a new trace entry.
+	performTracedRead(binary, logPath)
+
+	// Read vtrace, we should have 2 traces now.
+	want, output := 2, binary.Start("vtrace", "__debug/vtrace").Output()
+	if count := strings.Count(output, "Trace -"); count != want {
+		i.Fatalf("unexpected trace count, want %d, got %d\n%s", want, count, output)
+	}
+
+	// Now ask for a particular trace. The output should contain exactly
+	// one trace whose ID is equal to the one we asked for.
+	want, got = 1, binary.Start("vtrace", "__debug/vtrace", traceId).Output()
+	if count := strings.Count(got, "Trace -"); count != want {
+		i.Fatalf("unexpected trace count, want %d, got %d\n%s", want, count, got)
+	}
+	fields = strings.Split(got, " ")
+	if len(fields) < 3 {
+		i.Fatalf("expected at least 3 space-delimited fields, got %d\n", len(fields), got)
+	}
+	got = fields[2]
+	if traceId != got {
+		i.Fatalf("unexpected traceId, want %s, got %s", traceId, got)
+	}
+}
+
+func V23TestPprof(i *v23tests.T) {
+	v23tests.RunRootMT(i, "--veyron.tcp.address=127.0.0.1:0")
+
+	binary := i.BuildGoPkg("v.io/x/ref/cmd/debug")
+	inv := binary.Start("pprof", "run", "__debug/pprof", "heap", "--text")
+
+	// Assert that a profile indicating the heap size was written out.
+	want, got := "(.*) of (.*) total", inv.Output()
+	var groups []string
+	if groups = regexp.MustCompile(want).FindStringSubmatch(got); len(groups) < 3 {
+		i.Fatalf("could not find regexp %q in output\n%s", want, got)
+	}
+	i.Logf("got a heap profile showing a heap size of %s", groups[2])
+}
diff --git a/cmd/debug/doc.go b/cmd/debug/doc.go
new file mode 100644
index 0000000..248ac28
--- /dev/null
+++ b/cmd/debug/doc.go
@@ -0,0 +1,221 @@
+// This file was auto-generated via go generate.
+// DO NOT UPDATE MANUALLY
+
+/*
+Command-line tool for interacting with the debug server.
+
+Usage:
+   debug <command>
+
+The debug commands are:
+   glob        Returns all matching entries from the namespace.
+   vtrace      Returns vtrace traces.
+   logs        Accesses log files
+   stats       Accesses stats
+   pprof       Accesses profiling data
+   help        Display help for commands or topics
+Run "debug help [command]" for command usage.
+
+The global flags are:
+ -alsologtostderr=true
+   log to standard error as well as files
+ -log_backtrace_at=:0
+   when logging hits line file:N, emit a stack trace
+ -log_dir=
+   if non-empty, write log files to this directory
+ -logtostderr=false
+   log to standard error instead of files
+ -max_stack_buf_size=4292608
+   max size in bytes of the buffer to use for logging stack traces
+ -stderrthreshold=2
+   logs at or above this threshold go to stderr
+ -v=0
+   log level for V logs
+ -vanadium.i18n_catalogue=
+   18n catalogue files to load, comma separated
+ -veyron.credentials=
+   directory to use for storing security credentials
+ -veyron.namespace.root=[/ns.dev.v.io:8101]
+   local namespace root; can be repeated to provided multiple roots
+ -veyron.proxy=
+   object name of proxy service to use to export services across network
+   boundaries
+ -veyron.tcp.address=
+   address to listen on
+ -veyron.tcp.protocol=tcp
+   protocol to listen with
+ -veyron.vtrace.cache_size=1024
+   The number of vtrace traces to store in memory.
+ -veyron.vtrace.collect_regexp=
+   Spans and annotations that match this regular expression will trigger trace
+   collection.
+ -veyron.vtrace.dump_on_shutdown=true
+   If true, dump all stored traces on runtime shutdown.
+ -veyron.vtrace.sample_rate=0
+   Rate (from 0.0 to 1.0) to sample vtrace traces.
+ -vmodule=
+   comma-separated list of pattern=N settings for file-filtered logging
+
+Debug Glob
+
+Returns all matching entries from the namespace.
+
+Usage:
+   debug glob <pattern> ...
+
+<pattern> is a glob pattern to match.
+
+Debug Vtrace
+
+Returns matching vtrace traces (or all stored traces if no ids are given).
+
+Usage:
+   debug vtrace <name> [id ...]
+
+<name> is the name of a vtrace object. [id] is a vtrace trace id.
+
+Debug Logs
+
+Accesses log files
+
+Usage:
+   debug logs <command>
+
+The debug logs commands are:
+   read        Reads the content of a log file object.
+   size        Returns the size of a log file object.
+
+Debug Logs Read
+
+Reads the content of a log file object.
+
+Usage:
+   debug logs read [flags] <name>
+
+<name> is the name of the log file object.
+
+The debug logs read flags are:
+ -f=false
+   When true, read will wait for new log entries when it reaches the end of the
+   file.
+ -n=-1
+   The number of log entries to read.
+ -o=0
+   The position, in bytes, from which to start reading the log file.
+ -v=false
+   When true, read will be more verbose.
+
+Debug Logs Size
+
+Returns the size of a log file object.
+
+Usage:
+   debug logs size <name>
+
+<name> is the name of the log file object.
+
+Debug Stats
+
+Accesses stats
+
+Usage:
+   debug stats <command>
+
+The debug stats commands are:
+   read        Returns the value of stats objects.
+   watch       Returns a stream of all matching entries and their values as they
+               change.
+
+Debug Stats Read
+
+Returns the value of stats objects.
+
+Usage:
+   debug stats read [flags] <name> ...
+
+<name> is the name of a stats object, or a glob pattern to match against stats
+object names.
+
+The debug stats read flags are:
+ -raw=false
+   When true, the command will display the raw value of the object.
+ -type=false
+   When true, the type of the values will be displayed.
+
+Debug Stats Watch
+
+Returns a stream of all matching entries and their values as they change.
+
+Usage:
+   debug stats watch [flags] <pattern> ...
+
+<pattern> is a glob pattern to match.
+
+The debug stats watch flags are:
+ -raw=false
+   When true, the command will display the raw value of the object.
+ -type=false
+   When true, the type of the values will be displayed.
+
+Debug Pprof
+
+Accesses profiling data
+
+Usage:
+   debug pprof <command>
+
+The debug pprof commands are:
+   run         Runs the pprof tool.
+   proxy       Runs an http proxy to a pprof object.
+
+Debug Pprof Run
+
+Runs the pprof tool.
+
+Usage:
+   debug pprof run [flags] <name> <profile> [passthru args] ...
+
+<name> is the name of the pprof object. <profile> the name of the profile to
+use.
+
+All the [passthru args] are passed to the pprof tool directly, e.g.
+
+$ debug pprof run a/b/c heap --text $ debug pprof run a/b/c profile -gv
+
+The debug pprof run flags are:
+ -pprofcmd=v23 go tool pprof
+   The pprof command to use.
+
+Debug Pprof Proxy
+
+Runs an http proxy to a pprof object.
+
+Usage:
+   debug pprof proxy <name>
+
+<name> is the name of the pprof object.
+
+Debug Help
+
+Help with no args displays the usage of the parent command.
+
+Help with args displays the usage of the specified sub-command or help topic.
+
+"help ..." recursively displays help for all commands and topics.
+
+The output is formatted to a target width in runes.  The target width is
+determined by checking the environment variable CMDLINE_WIDTH, falling back on
+the terminal width from the OS, falling back on 80 chars.  By setting
+CMDLINE_WIDTH=x, if x > 0 the width is x, if x < 0 the width is unlimited, and
+if x == 0 or is unset one of the fallbacks is used.
+
+Usage:
+   debug help [flags] [command/topic ...]
+
+[command/topic ...] optionally identifies a specific sub-command or help topic.
+
+The debug help flags are:
+ -style=text
+   The formatting style for help output, either "text" or "godoc".
+*/
+package main
diff --git a/cmd/debug/impl.go b/cmd/debug/impl.go
new file mode 100644
index 0000000..40c4498
--- /dev/null
+++ b/cmd/debug/impl.go
@@ -0,0 +1,577 @@
+package main
+
+import (
+	"fmt"
+	"os"
+	"os/exec"
+	"regexp"
+	"sort"
+	"strings"
+	"sync"
+	"time"
+
+	"v.io/v23"
+	"v.io/v23/context"
+	"v.io/v23/naming"
+	"v.io/v23/services/mgmt/logreader"
+	logtypes "v.io/v23/services/mgmt/logreader/types"
+	"v.io/v23/services/mgmt/pprof"
+	"v.io/v23/services/mgmt/stats"
+	vtracesvc "v.io/v23/services/mgmt/vtrace"
+	"v.io/v23/services/watch"
+	watchtypes "v.io/v23/services/watch/types"
+	"v.io/v23/uniqueid"
+	"v.io/v23/vdl"
+	"v.io/v23/vtrace"
+	"v.io/x/lib/cmdline"
+	"v.io/x/ref/lib/glob"
+	"v.io/x/ref/lib/signals"
+	"v.io/x/ref/services/mgmt/pprof/client"
+)
+
+var (
+	follow     bool
+	verbose    bool
+	numEntries int
+	startPos   int64
+	raw        bool
+	showType   bool
+	pprofCmd   string
+)
+
+func init() {
+	// logs read flags
+	cmdLogsRead.Flags.BoolVar(&follow, "f", false, "When true, read will wait for new log entries when it reaches the end of the file.")
+	cmdLogsRead.Flags.BoolVar(&verbose, "v", false, "When true, read will be more verbose.")
+	cmdLogsRead.Flags.IntVar(&numEntries, "n", int(logtypes.AllEntries), "The number of log entries to read.")
+	cmdLogsRead.Flags.Int64Var(&startPos, "o", 0, "The position, in bytes, from which to start reading the log file.")
+
+	// stats read flags
+	cmdStatsRead.Flags.BoolVar(&raw, "raw", false, "When true, the command will display the raw value of the object.")
+	cmdStatsRead.Flags.BoolVar(&showType, "type", false, "When true, the type of the values will be displayed.")
+
+	// stats watch flags
+	cmdStatsWatch.Flags.BoolVar(&raw, "raw", false, "When true, the command will display the raw value of the object.")
+	cmdStatsWatch.Flags.BoolVar(&showType, "type", false, "When true, the type of the values will be displayed.")
+
+	// pprof flags
+	cmdPProfRun.Flags.StringVar(&pprofCmd, "pprofcmd", "v23 go tool pprof", "The pprof command to use.")
+}
+
+var cmdVtrace = &cmdline.Command{
+	Run:      runVtrace,
+	Name:     "vtrace",
+	Short:    "Returns vtrace traces.",
+	Long:     "Returns matching vtrace traces (or all stored traces if no ids are given).",
+	ArgsName: "<name> [id ...]",
+	ArgsLong: `
+<name> is the name of a vtrace object.
+[id] is a vtrace trace id.
+`,
+}
+
+func doFetchTrace(ctx *context.T, wg *sync.WaitGroup, client vtracesvc.StoreClientStub,
+	id uniqueid.Id, traces chan *vtrace.TraceRecord, errors chan error) {
+	defer wg.Done()
+
+	trace, err := client.Trace(ctx, id)
+	if err != nil {
+		errors <- err
+	} else {
+		traces <- &trace
+	}
+}
+
+func runVtrace(cmd *cmdline.Command, args []string) error {
+	arglen := len(args)
+	if arglen == 0 {
+		return cmd.UsageErrorf("vtrace: incorrect number of arguments, got %d want >= 1", arglen)
+	}
+
+	name := args[0]
+	client := vtracesvc.StoreClient(name)
+	if arglen == 1 {
+		call, err := client.AllTraces(gctx)
+		if err != nil {
+			return err
+		}
+		stream := call.RecvStream()
+		for stream.Advance() {
+			trace := stream.Value()
+			vtrace.FormatTrace(os.Stdout, &trace, nil)
+		}
+		if err := stream.Err(); err != nil {
+			return err
+		}
+		return call.Finish()
+	}
+
+	ntraces := len(args) - 1
+	traces := make(chan *vtrace.TraceRecord, ntraces)
+	errors := make(chan error, ntraces)
+	var wg sync.WaitGroup
+	wg.Add(ntraces)
+	for _, arg := range args[1:] {
+		id, err := uniqueid.FromHexString(arg)
+		if err != nil {
+			return err
+		}
+		go doFetchTrace(gctx, &wg, client, id, traces, errors)
+	}
+	go func() {
+		wg.Wait()
+		close(traces)
+		close(errors)
+	}()
+
+	for trace := range traces {
+		vtrace.FormatTrace(os.Stdout, trace, nil)
+	}
+
+	// Just return one of the errors.
+	return <-errors
+}
+
+var cmdGlob = &cmdline.Command{
+	Run:      runGlob,
+	Name:     "glob",
+	Short:    "Returns all matching entries from the namespace.",
+	Long:     "Returns all matching entries from the namespace.",
+	ArgsName: "<pattern> ...",
+	ArgsLong: `
+<pattern> is a glob pattern to match.
+`,
+}
+
+func runGlob(cmd *cmdline.Command, args []string) error {
+	if min, got := 1, len(args); got < min {
+		return cmd.UsageErrorf("glob: incorrect number of arguments, got %d, want >=%d", got, min)
+	}
+	results := make(chan interface{})
+	errors := make(chan error)
+	doGlobs(gctx, args, results, errors)
+	var lastErr error
+	for {
+		select {
+		case err := <-errors:
+			lastErr = err
+			fmt.Fprintln(cmd.Stderr(), "Error:", err)
+		case me, ok := <-results:
+			if !ok {
+				return lastErr
+			}
+			switch v := me.(type) {
+			case *naming.MountEntry:
+				fmt.Fprint(cmd.Stdout(), v.Name)
+				for _, s := range v.Servers {
+					fmt.Fprintf(cmd.Stdout(), " %s (Expires %s)", s.Server, s.Expires)
+				}
+				fmt.Fprintln(cmd.Stdout())
+			case *naming.GlobError:
+				fmt.Fprintf(cmd.Stderr(), "Error: %s: %v\n", v.Name, v.Error)
+			}
+		}
+	}
+}
+
+// doGlobs calls Glob on multiple patterns in parallel and sends all the results
+// on the results channel and all the errors on the errors channel. It closes
+// the results channel when all the results have been sent.
+func doGlobs(ctx *context.T, patterns []string, results chan<- interface{}, errors chan<- error) {
+	var wg sync.WaitGroup
+	wg.Add(len(patterns))
+	for _, p := range patterns {
+		go doGlob(ctx, p, results, errors, &wg)
+	}
+	go func() {
+		wg.Wait()
+		close(results)
+	}()
+}
+
+func doGlob(ctx *context.T, pattern string, results chan<- interface{}, errors chan<- error, wg *sync.WaitGroup) {
+	defer wg.Done()
+	ctx, cancel := context.WithTimeout(ctx, time.Minute)
+	defer cancel()
+	c, err := v23.GetNamespace(ctx).Glob(ctx, pattern)
+	if err != nil {
+		errors <- fmt.Errorf("%s: %v", pattern, err)
+		return
+	}
+	for me := range c {
+		results <- me
+	}
+}
+
+var cmdLogsRead = &cmdline.Command{
+	Run:      runLogsRead,
+	Name:     "read",
+	Short:    "Reads the content of a log file object.",
+	Long:     "Reads the content of a log file object.",
+	ArgsName: "<name>",
+	ArgsLong: `
+<name> is the name of the log file object.
+`,
+}
+
+func runLogsRead(cmd *cmdline.Command, args []string) error {
+	if want, got := 1, len(args); want != got {
+		return cmd.UsageErrorf("read: incorrect number of arguments, got %d, want %d", got, want)
+	}
+	name := args[0]
+	lf := logreader.LogFileClient(name)
+	stream, err := lf.ReadLog(gctx, startPos, int32(numEntries), follow)
+	if err != nil {
+		return err
+	}
+	iterator := stream.RecvStream()
+	for iterator.Advance() {
+		entry := iterator.Value()
+		if verbose {
+			fmt.Fprintf(cmd.Stdout(), "[%d] %s\n", entry.Position, entry.Line)
+		} else {
+			fmt.Fprintf(cmd.Stdout(), "%s\n", entry.Line)
+		}
+	}
+	if err = iterator.Err(); err != nil {
+		return err
+	}
+	offset, err := stream.Finish()
+	if err != nil {
+		return err
+	}
+	if verbose {
+		fmt.Fprintf(cmd.Stdout(), "Offset: %d\n", offset)
+	}
+	return nil
+}
+
+var cmdLogsSize = &cmdline.Command{
+	Run:      runLogsSize,
+	Name:     "size",
+	Short:    "Returns the size of a log file object.",
+	Long:     "Returns the size of a log file object.",
+	ArgsName: "<name>",
+	ArgsLong: `
+<name> is the name of the log file object.
+`,
+}
+
+func runLogsSize(cmd *cmdline.Command, args []string) error {
+	if want, got := 1, len(args); want != got {
+		return cmd.UsageErrorf("size: incorrect number of arguments, got %d, want %d", got, want)
+	}
+	name := args[0]
+	lf := logreader.LogFileClient(name)
+	size, err := lf.Size(gctx)
+	if err != nil {
+		return err
+	}
+	fmt.Fprintln(cmd.Stdout(), size)
+	return nil
+}
+
+var cmdStatsRead = &cmdline.Command{
+	Run:      runStatsRead,
+	Name:     "read",
+	Short:    "Returns the value of stats objects.",
+	Long:     "Returns the value of stats objects.",
+	ArgsName: "<name> ...",
+	ArgsLong: `
+<name> is the name of a stats object, or a glob pattern to match against stats
+object names.
+`,
+}
+
+func runStatsRead(cmd *cmdline.Command, args []string) error {
+	if min, got := 1, len(args); got < min {
+		return cmd.UsageErrorf("read: incorrect number of arguments, got %d, want >=%d", got, min)
+	}
+	globResults := make(chan interface{})
+	errors := make(chan error)
+	doGlobs(gctx, args, globResults, errors)
+
+	output := make(chan string)
+	go func() {
+		var wg sync.WaitGroup
+		for me := range globResults {
+			switch v := me.(type) {
+			case *naming.MountEntry:
+				wg.Add(1)
+				go doValue(gctx, v.Name, output, errors, &wg)
+			}
+		}
+		wg.Wait()
+		close(output)
+	}()
+
+	var lastErr error
+	for {
+		select {
+		case err := <-errors:
+			lastErr = err
+			fmt.Fprintln(cmd.Stderr(), err)
+		case out, ok := <-output:
+			if !ok {
+				return lastErr
+			}
+			fmt.Fprintln(cmd.Stdout(), out)
+		}
+	}
+}
+
+func doValue(ctx *context.T, name string, output chan<- string, errors chan<- error, wg *sync.WaitGroup) {
+	defer wg.Done()
+	ctx, cancel := context.WithTimeout(ctx, time.Minute)
+	defer cancel()
+	v, err := stats.StatsClient(name).Value(ctx)
+	if err != nil {
+		errors <- fmt.Errorf("%s: %v", name, err)
+		return
+	}
+	fv, err := formatValue(v)
+	if err != nil {
+		errors <- fmt.Errorf("%s: %v", name, err)
+		// fv is still valid, so dump it out too.
+	}
+	output <- fmt.Sprintf("%s: %v", name, fv)
+}
+
+var cmdStatsWatch = &cmdline.Command{
+	Run:      runStatsWatch,
+	Name:     "watch",
+	Short:    "Returns a stream of all matching entries and their values as they change.",
+	Long:     "Returns a stream of all matching entries and their values as they change.",
+	ArgsName: "<pattern> ...",
+	ArgsLong: `
+<pattern> is a glob pattern to match.
+`,
+}
+
+func runStatsWatch(cmd *cmdline.Command, args []string) error {
+	if want, got := 1, len(args); got < want {
+		return cmd.UsageErrorf("watch: incorrect number of arguments, got %d, want >=%d", got, want)
+	}
+
+	results := make(chan string)
+	errors := make(chan error)
+	var wg sync.WaitGroup
+	wg.Add(len(args))
+	for _, arg := range args {
+		go doWatch(gctx, arg, results, errors, &wg)
+	}
+	go func() {
+		wg.Wait()
+		close(results)
+	}()
+	var lastErr error
+	for {
+		select {
+		case err := <-errors:
+			lastErr = err
+			fmt.Fprintln(cmd.Stderr(), "Error:", err)
+		case r, ok := <-results:
+			if !ok {
+				return lastErr
+			}
+			fmt.Fprintln(cmd.Stdout(), r)
+		}
+	}
+}
+
+func doWatch(ctx *context.T, pattern string, results chan<- string, errors chan<- error, wg *sync.WaitGroup) {
+	defer wg.Done()
+	root, globPattern := naming.SplitAddressName(pattern)
+	g, err := glob.Parse(globPattern)
+	if err != nil {
+		errors <- fmt.Errorf("%s: %v", globPattern, err)
+		return
+	}
+	var prefixElems []string
+	prefixElems, g = g.SplitFixedPrefix()
+	name := naming.Join(prefixElems...)
+	if len(root) != 0 {
+		name = naming.JoinAddressName(root, name)
+	}
+	c := watch.GlobWatcherClient(name)
+	for retry := false; ; retry = true {
+		if retry {
+			time.Sleep(10 * time.Second)
+		}
+		stream, err := c.WatchGlob(ctx, watchtypes.GlobRequest{Pattern: g.String()})
+		if err != nil {
+			errors <- fmt.Errorf("%s: %v", name, err)
+			continue
+		}
+		iterator := stream.RecvStream()
+		for iterator.Advance() {
+			v := iterator.Value()
+			fv, err := formatValue(v.Value)
+			if err != nil {
+				errors <- fmt.Errorf("%s: %v", name, err)
+				// fv is still valid, so dump it out too.
+			}
+			results <- fmt.Sprintf("%s: %s", naming.Join(name, v.Name), fv)
+		}
+		if err = iterator.Err(); err != nil {
+			errors <- fmt.Errorf("%s: %v", name, err)
+			continue
+		}
+		if err = stream.Finish(); err != nil {
+			errors <- fmt.Errorf("%s: %v", name, err)
+		}
+	}
+}
+
+func formatValue(value *vdl.Value) (string, error) {
+	var ret string
+	if showType {
+		ret += value.Type().String() + ": "
+	}
+	if raw {
+		return ret + value.String(), nil
+	}
+	// Convert the *vdl.Value into an interface{}, so that things like histograms
+	// get pretty-printed.
+	var pretty interface{}
+	err := vdl.Convert(&pretty, value)
+	if err != nil {
+		// If we can't convert, just print the raw value, but still return an error.
+		err = fmt.Errorf("couldn't pretty-print, consider setting -raw: %v", err)
+		pretty = value
+	}
+	return ret + fmt.Sprint(pretty), err
+}
+
+var cmdPProfRun = &cmdline.Command{
+	Run:      runPProf,
+	Name:     "run",
+	Short:    "Runs the pprof tool.",
+	Long:     "Runs the pprof tool.",
+	ArgsName: "<name> <profile> [passthru args] ...",
+	ArgsLong: `
+<name> is the name of the pprof object.
+<profile> the name of the profile to use.
+
+All the [passthru args] are passed to the pprof tool directly, e.g.
+
+$ debug pprof run a/b/c heap --text
+$ debug pprof run a/b/c profile -gv
+`,
+}
+
+func runPProf(cmd *cmdline.Command, args []string) error {
+	if min, got := 1, len(args); got < min {
+		return cmd.UsageErrorf("pprof: incorrect number of arguments, got %d, want >=%d", got, min)
+	}
+	name := args[0]
+	if len(args) == 1 {
+		return showPProfProfiles(cmd, name)
+	}
+	profile := args[1]
+	listener, err := client.StartProxy(gctx, name)
+	if err != nil {
+		return err
+	}
+	defer listener.Close()
+
+	// Construct the pprof command line:
+	// <pprofCmd> http://<proxyaddr>/pprof/<profile> [pprof flags]
+	pargs := []string{pprofCmd} // pprofCmd is purposely not escaped.
+	for i := 2; i < len(args); i++ {
+		pargs = append(pargs, shellEscape(args[i]))
+	}
+	pargs = append(pargs, shellEscape(fmt.Sprintf("http://%s/pprof/%s", listener.Addr(), profile)))
+	pcmd := strings.Join(pargs, " ")
+	fmt.Fprintf(cmd.Stdout(), "Running: %s\n", pcmd)
+	c := exec.Command("sh", "-c", pcmd)
+	c.Stdin = os.Stdin
+	c.Stdout = cmd.Stdout()
+	c.Stderr = cmd.Stderr()
+	return c.Run()
+}
+
+func showPProfProfiles(cmd *cmdline.Command, name string) error {
+	v, err := pprof.PProfClient(name).Profiles(gctx)
+	if err != nil {
+		return err
+	}
+	v = append(v, "profile")
+	sort.Strings(v)
+	fmt.Fprintln(cmd.Stdout(), "Available profiles:")
+	for _, p := range v {
+		fmt.Fprintf(cmd.Stdout(), "  %s\n", p)
+	}
+	return nil
+}
+
+func shellEscape(s string) string {
+	if !strings.Contains(s, "'") {
+		return "'" + s + "'"
+	}
+	re := regexp.MustCompile("([\"$`\\\\])")
+	return `"` + re.ReplaceAllString(s, "\\$1") + `"`
+}
+
+var cmdPProfRunProxy = &cmdline.Command{
+	Run:      runPProfProxy,
+	Name:     "proxy",
+	Short:    "Runs an http proxy to a pprof object.",
+	Long:     "Runs an http proxy to a pprof object.",
+	ArgsName: "<name>",
+	ArgsLong: `
+<name> is the name of the pprof object.
+`,
+}
+
+func runPProfProxy(cmd *cmdline.Command, args []string) error {
+	if want, got := 1, len(args); got != want {
+		return cmd.UsageErrorf("proxy: incorrect number of arguments, got %d, want %d", got, want)
+	}
+	name := args[0]
+	listener, err := client.StartProxy(gctx, name)
+	if err != nil {
+		return err
+	}
+	defer listener.Close()
+
+	fmt.Fprintln(cmd.Stdout())
+	fmt.Fprintf(cmd.Stdout(), "The pprof proxy is listening at http://%s/pprof\n", listener.Addr())
+	fmt.Fprintln(cmd.Stdout())
+	fmt.Fprintln(cmd.Stdout(), "Hit CTRL-C to exit")
+
+	<-signals.ShutdownOnSignals(gctx)
+	return nil
+}
+
+var cmdRoot = cmdline.Command{
+	Name:  "debug",
+	Short: "Command-line tool for interacting with the debug server.",
+	Long:  "Command-line tool for interacting with the debug server.",
+	Children: []*cmdline.Command{
+		cmdGlob,
+		cmdVtrace,
+		&cmdline.Command{
+			Name:     "logs",
+			Short:    "Accesses log files",
+			Long:     "Accesses log files",
+			Children: []*cmdline.Command{cmdLogsRead, cmdLogsSize},
+		},
+		&cmdline.Command{
+			Name:     "stats",
+			Short:    "Accesses stats",
+			Long:     "Accesses stats",
+			Children: []*cmdline.Command{cmdStatsRead, cmdStatsWatch},
+		},
+		&cmdline.Command{
+			Name:     "pprof",
+			Short:    "Accesses profiling data",
+			Long:     "Accesses profiling data",
+			Children: []*cmdline.Command{cmdPProfRun, cmdPProfRunProxy},
+		},
+	},
+}
+
+func root() *cmdline.Command {
+	return &cmdRoot
+}
diff --git a/cmd/debug/main.go b/cmd/debug/main.go
new file mode 100644
index 0000000..03caf91
--- /dev/null
+++ b/cmd/debug/main.go
@@ -0,0 +1,23 @@
+// The following enables go generate to generate the doc.go file.
+//go:generate go run $VANADIUM_ROOT/release/go/src/v.io/x/lib/cmdline/testdata/gendoc.go .
+
+package main
+
+import (
+	"os"
+
+	"v.io/v23"
+	"v.io/v23/context"
+
+	_ "v.io/x/ref/profiles"
+)
+
+var gctx *context.T
+
+func main() {
+	var shutdown v23.Shutdown
+	gctx, shutdown = v23.Init()
+	exitCode := root().Main()
+	shutdown()
+	os.Exit(exitCode)
+}
diff --git a/cmd/debug/v23_test.go b/cmd/debug/v23_test.go
new file mode 100644
index 0000000..b3cf055
--- /dev/null
+++ b/cmd/debug/v23_test.go
@@ -0,0 +1,53 @@
+// Copyright 2015 The Vanadium 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 file was auto-generated via go generate.
+// DO NOT UPDATE MANUALLY
+package main_test
+
+import "testing"
+import "os"
+
+import "v.io/x/ref/lib/testutil"
+import "v.io/x/ref/lib/testutil/v23tests"
+
+func TestMain(m *testing.M) {
+	testutil.Init()
+	cleanup := v23tests.UseSharedBinDir()
+	r := m.Run()
+	cleanup()
+	os.Exit(r)
+}
+
+func TestV23DebugGlob(t *testing.T) {
+	v23tests.RunTest(t, V23TestDebugGlob)
+}
+
+func TestV23DebugGlobLogs(t *testing.T) {
+	v23tests.RunTest(t, V23TestDebugGlobLogs)
+}
+
+func TestV23ReadHostname(t *testing.T) {
+	v23tests.RunTest(t, V23TestReadHostname)
+}
+
+func TestV23LogSize(t *testing.T) {
+	v23tests.RunTest(t, V23TestLogSize)
+}
+
+func TestV23StatsRead(t *testing.T) {
+	v23tests.RunTest(t, V23TestStatsRead)
+}
+
+func TestV23StatsWatch(t *testing.T) {
+	v23tests.RunTest(t, V23TestStatsWatch)
+}
+
+func TestV23VTrace(t *testing.T) {
+	v23tests.RunTest(t, V23TestVTrace)
+}
+
+func TestV23Pprof(t *testing.T) {
+	v23tests.RunTest(t, V23TestPprof)
+}
diff --git a/cmd/findunusedport/main.go b/cmd/findunusedport/main.go
new file mode 100644
index 0000000..9d08a3a
--- /dev/null
+++ b/cmd/findunusedport/main.go
@@ -0,0 +1,21 @@
+package main
+
+// findunusedport finds a random unused TCP port in the range 1k to 64k and prints it to standard out.
+
+import (
+	"fmt"
+
+	"v.io/x/lib/vlog"
+	"v.io/x/ref/lib/testutil"
+)
+
+func main() {
+	port, err := testutil.FindUnusedPort()
+	if err != nil {
+		vlog.Fatalf("can't find unused port: %v\n", err)
+	} else if port == 0 {
+		vlog.Fatalf("can't find unused port")
+	} else {
+		fmt.Println(port)
+	}
+}
diff --git a/cmd/gclogs/doc.go b/cmd/gclogs/doc.go
new file mode 100644
index 0000000..302fe98
--- /dev/null
+++ b/cmd/gclogs/doc.go
@@ -0,0 +1,31 @@
+// This file was auto-generated via go generate.
+// DO NOT UPDATE MANUALLY
+
+/*
+gclogs is a utility that safely deletes old log files.
+
+It looks for file names that match the format of files produced by the vlog
+package, and deletes the ones that have not changed in the amount of time
+specified by the --cutoff flag.
+
+Only files produced by the same user as the one running the gclogs command are
+considered for deletion.
+
+Usage:
+   gclogs [flags] <dir> ...
+
+<dir> ... A list of directories where to look for log files.
+
+The gclogs flags are:
+ -cutoff=24h0m0s
+   The age cut-off for a log file to be considered for garbage collection.
+ -n=false
+   If true, log files that would be deleted are shown on stdout, but not
+   actually deleted.
+ -program=.*
+   A regular expression to apply to the program part of the log file name, e.g
+   ".*test".
+ -verbose=false
+   If true, each deleted file is shown on stdout.
+*/
+package main
diff --git a/cmd/gclogs/format.go b/cmd/gclogs/format.go
new file mode 100644
index 0000000..502db25
--- /dev/null
+++ b/cmd/gclogs/format.go
@@ -0,0 +1,69 @@
+package main
+
+import (
+	"errors"
+	"os"
+	"path/filepath"
+	"regexp"
+	"strconv"
+	"syscall"
+	"time"
+)
+
+var (
+	// The format of the log file names is:
+	// program.host.user.log.logger.tag.YYYYMMDD-HHmmss.pid
+	logFileRE = regexp.MustCompile(`^(.*)\.([^.]*)\.([^.]*)\.log\.([^.]*)\.([^.]*)\.(........-......)\.([0-9]*)$`)
+)
+
+type logFile struct {
+	// TODO(rthellend): Some of these fields are not used by anything yet,
+	// but they will be eventually when we need to sort the log files
+	// associated with a given instance.
+	symlink                          bool
+	program, host, user, logger, tag string
+	time                             time.Time
+	pid                              int
+}
+
+func parseFileInfo(dir string, fileInfo os.FileInfo) (*logFile, error) {
+	fileName := fileInfo.Name()
+	if fileInfo.Mode()&os.ModeSymlink != 0 {
+		buf := make([]byte, syscall.NAME_MAX)
+		n, err := syscall.Readlink(filepath.Join(dir, fileName), buf)
+		if err != nil {
+			return nil, err
+		}
+		linkName := string(buf[:n])
+		lf, err := parseFileName(linkName)
+		if err != nil {
+			return nil, err
+		}
+		lf.symlink = true
+		return lf, nil
+	}
+	return parseFileName(fileName)
+}
+
+func parseFileName(fileName string) (*logFile, error) {
+	if m := logFileRE.FindStringSubmatch(fileName); len(m) != 0 {
+		t, err := time.ParseInLocation("20060102-150405", m[6], time.Local)
+		if err != nil {
+			return nil, err
+		}
+		pid, err := strconv.ParseInt(m[7], 10, 32)
+		if err != nil {
+			return nil, err
+		}
+		return &logFile{
+			program: m[1],
+			host:    m[2],
+			user:    m[3],
+			logger:  m[4],
+			tag:     m[5],
+			time:    t,
+			pid:     int(pid),
+		}, nil
+	}
+	return nil, errors.New("not a recognized log file name")
+}
diff --git a/cmd/gclogs/format_test.go b/cmd/gclogs/format_test.go
new file mode 100644
index 0000000..5396894
--- /dev/null
+++ b/cmd/gclogs/format_test.go
@@ -0,0 +1,97 @@
+package main
+
+import (
+	"io/ioutil"
+	"os"
+	"path/filepath"
+	"reflect"
+	"testing"
+	"time"
+)
+
+func TestParseFileNameNoError(t *testing.T) {
+	testcases := []struct {
+		filename string
+		lf       *logFile
+	}{
+		{
+			"program.host.user.log.veyron.INFO.20141204-131502.12345",
+			&logFile{false, "program", "host", "user", "veyron", "INFO", time.Date(2014, 12, 4, 13, 15, 2, 0, time.Local), 12345},
+		},
+		{
+			"prog.test.host-name.user.log.veyron.ERROR.20141204-131502.12345",
+			&logFile{false, "prog.test", "host-name", "user", "veyron", "ERROR", time.Date(2014, 12, 4, 13, 15, 2, 0, time.Local), 12345},
+		},
+	}
+	for _, tc := range testcases {
+		lf, err := parseFileName(tc.filename)
+		if err != nil {
+			t.Errorf("unexpected error for %q: %v", tc.filename, err)
+		}
+		if !reflect.DeepEqual(tc.lf, lf) {
+			t.Errorf("unexpected result: got %+v, expected %+v", lf, tc.lf)
+		}
+	}
+}
+
+func TestParseFileNameError(t *testing.T) {
+	testcases := []string{
+		"program.host.user.log.veyron.INFO.20141204-131502",
+		"prog.test.host-name.user.log.veyron.20141204-131502.12345",
+		"foo.txt",
+	}
+	for _, tc := range testcases {
+		if _, err := parseFileName(tc); err == nil {
+			t.Errorf("unexpected success for %q", tc)
+		}
+	}
+}
+
+func TestParseFileInfo(t *testing.T) {
+	tmpdir, err := ioutil.TempDir("", "parse-file-info-")
+	if err != nil {
+		t.Fatalf("ioutil.TempDir failed: %v", err)
+	}
+	defer os.RemoveAll(tmpdir)
+
+	name := "program.host.user.log.veyron.INFO.20141204-131502.12345"
+	if err := ioutil.WriteFile(filepath.Join(tmpdir, name), []byte{}, 0644); err != nil {
+		t.Fatalf("ioutil.WriteFile failed: %v", err)
+	}
+	link := "program.INFO"
+	if err := os.Symlink(name, filepath.Join(tmpdir, link)); err != nil {
+		t.Fatalf("os.Symlink failed: %v", err)
+	}
+
+	// Test regular file.
+	{
+		fi, err := os.Lstat(filepath.Join(tmpdir, name))
+		if err != nil {
+			t.Fatalf("os.Lstat failed: %v", err)
+		}
+		lf, err := parseFileInfo(tmpdir, fi)
+		if err != nil {
+			t.Errorf("parseFileInfo(%v, %v) failed: %v", tmpdir, fi, err)
+		}
+		expected := &logFile{false, "program", "host", "user", "veyron", "INFO", time.Date(2014, 12, 4, 13, 15, 2, 0, time.Local), 12345}
+		if !reflect.DeepEqual(lf, expected) {
+			t.Errorf("unexpected result: got %+v, expected %+v", lf, expected)
+		}
+	}
+
+	// Test symlink.
+	{
+		fi, err := os.Lstat(filepath.Join(tmpdir, link))
+		if err != nil {
+			t.Fatalf("os.Lstat failed: %v", err)
+		}
+		lf, err := parseFileInfo(tmpdir, fi)
+		if err != nil {
+			t.Errorf("parseFileInfo(%v, %v) failed: %v", tmpdir, fi, err)
+		}
+		expected := &logFile{true, "program", "host", "user", "veyron", "INFO", time.Date(2014, 12, 4, 13, 15, 2, 0, time.Local), 12345}
+		if !reflect.DeepEqual(lf, expected) {
+			t.Errorf("unexpected result: got %+v, expected %+v", lf, expected)
+		}
+	}
+}
diff --git a/cmd/gclogs/gclogs.go b/cmd/gclogs/gclogs.go
new file mode 100644
index 0000000..d0c0f57
--- /dev/null
+++ b/cmd/gclogs/gclogs.go
@@ -0,0 +1,157 @@
+package main
+
+import (
+	"fmt"
+	"io"
+	"os"
+	"os/user"
+	"path/filepath"
+	"regexp"
+	"time"
+
+	"v.io/x/lib/cmdline"
+)
+
+var (
+	flagCutoff   time.Duration
+	flagProgname string
+	flagVerbose  bool
+	flagDryrun   bool
+
+	cmdGCLogs = &cmdline.Command{
+		Run:   garbageCollectLogs,
+		Name:  "gclogs",
+		Short: "gclogs is a utility that safely deletes old log files.",
+		Long: `
+gclogs is a utility that safely deletes old log files.
+
+It looks for file names that match the format of files produced by the vlog
+package, and deletes the ones that have not changed in the amount of time
+specified by the --cutoff flag.
+
+Only files produced by the same user as the one running the gclogs command
+are considered for deletion.
+`,
+		ArgsName: "<dir> ...",
+		ArgsLong: "<dir> ... A list of directories where to look for log files.",
+	}
+)
+
+func init() {
+	cmdGCLogs.Flags.DurationVar(&flagCutoff, "cutoff", 24*time.Hour, "The age cut-off for a log file to be considered for garbage collection.")
+	cmdGCLogs.Flags.StringVar(&flagProgname, "program", ".*", `A regular expression to apply to the program part of the log file name, e.g ".*test".`)
+	cmdGCLogs.Flags.BoolVar(&flagVerbose, "verbose", false, "If true, each deleted file is shown on stdout.")
+	cmdGCLogs.Flags.BoolVar(&flagDryrun, "n", false, "If true, log files that would be deleted are shown on stdout, but not actually deleted.")
+}
+
+func garbageCollectLogs(cmd *cmdline.Command, args []string) error {
+	if len(args) == 0 {
+		cmd.UsageErrorf("gclogs requires at least one argument")
+	}
+	timeCutoff := time.Now().Add(-flagCutoff)
+	currentUser, err := user.Current()
+	if err != nil {
+		return err
+	}
+	programRE, err := regexp.Compile(flagProgname)
+	if err != nil {
+		return err
+	}
+	var lastErr error
+	for _, logdir := range args {
+		if err := processDirectory(cmd, logdir, timeCutoff, programRE, currentUser.Username); err != nil {
+			lastErr = err
+		}
+	}
+	return lastErr
+}
+
+func processDirectory(cmd *cmdline.Command, logdir string, timeCutoff time.Time, programRE *regexp.Regexp, username string) error {
+	fmt.Fprintf(cmd.Stdout(), "Processing: %q\n", logdir)
+
+	f, err := os.Open(logdir)
+	if err != nil {
+		return err
+	}
+	defer f.Close()
+
+	var lastErr error
+	deleted := 0
+	symlinks := []string{}
+	for {
+		fi, err := f.Readdir(100)
+		if err == io.EOF {
+			break
+		}
+		if err != nil {
+			lastErr = err
+			break
+		}
+		for _, file := range fi {
+			fullname := filepath.Join(logdir, file.Name())
+			if file.IsDir() {
+				if flagVerbose {
+					fmt.Fprintf(cmd.Stdout(), "Skipped directory: %q\n", fullname)
+				}
+				continue
+			}
+			lf, err := parseFileInfo(logdir, file)
+			if err != nil {
+				if flagVerbose {
+					fmt.Fprintf(cmd.Stdout(), "Not a log file: %q\n", fullname)
+				}
+				continue
+			}
+			if lf.user != username {
+				if flagVerbose {
+					fmt.Fprintf(cmd.Stdout(), "Skipped log file created by other user: %q\n", fullname)
+				}
+				continue
+			}
+			if !programRE.MatchString(lf.program) {
+				if flagVerbose {
+					fmt.Fprintf(cmd.Stdout(), "Skipped log file doesn't match %q: %q\n", flagProgname, fullname)
+				}
+				continue
+			}
+			if lf.symlink {
+				symlinks = append(symlinks, fullname)
+				continue
+			}
+			if file.ModTime().Before(timeCutoff) {
+				if flagDryrun {
+					fmt.Fprintf(cmd.Stdout(), "Would delete %q\n", fullname)
+					continue
+				}
+				if flagVerbose {
+					fmt.Fprintf(cmd.Stdout(), "Deleting %q\n", fullname)
+				}
+				if err := os.Remove(fullname); err != nil {
+					lastErr = err
+				} else {
+					deleted++
+				}
+			}
+		}
+	}
+	// Delete broken links.
+	for _, sl := range symlinks {
+		if _, err := os.Stat(sl); err != nil && os.IsNotExist(err) {
+			if flagDryrun {
+				fmt.Fprintf(cmd.Stdout(), "Would delete symlink %q\n", sl)
+				continue
+			}
+			if flagVerbose {
+				fmt.Fprintf(cmd.Stdout(), "Deleting symlink %q\n", sl)
+			}
+			if err := os.Remove(sl); err != nil {
+				lastErr = err
+			} else {
+				deleted++
+			}
+		}
+
+	}
+	fmt.Fprintf(cmd.Stdout(), "Number of files deleted: %d\n", deleted)
+	return lastErr
+}
diff --git a/cmd/gclogs/gclogs_test.go b/cmd/gclogs/gclogs_test.go
new file mode 100644
index 0000000..574fca2
--- /dev/null
+++ b/cmd/gclogs/gclogs_test.go
@@ -0,0 +1,177 @@
+package main
+
+import (
+	"bytes"
+	"fmt"
+	"io/ioutil"
+	"os"
+	"os/user"
+	"path/filepath"
+	"sort"
+	"strings"
+	"testing"
+	"time"
+)
+
+func setup(t *testing.T, workdir, username string) (tmpdir string) {
+	var err error
+	tmpdir, err = ioutil.TempDir(workdir, "gclogs-test-setup-")
+	if err != nil {
+		t.Fatalf("ioutil.TempDir failed: %v", err)
+	}
+	logfiles := []struct {
+		name string
+		link string
+		age  time.Duration
+	}{
+		{"prog1.host.%USER%.log.veyron.INFO.20141204-131502.12345", "", 4 * time.Hour},
+		{"prog1.host.%USER%.log.veyron.INFO.20141204-145040.23456", "prog1.INFO", 1 * time.Hour},
+		{"prog1.host.%USER%.log.veyron.ERROR.20141204-145040.23456", "prog1.ERROR", 1 * time.Hour},
+		{"prog2.host.%USER%.log.veyron.INFO.20141204-135040.23456", "prog2.INFO", 4 * time.Hour},
+		{"prog3.host.otheruser.log.veyron.INFO.20141204-135040.23456", "prog3.INFO", 1 * time.Hour},
+		{"foo.txt", "", 1 * time.Hour},
+		{"bar.txt", "", 1 * time.Hour},
+	}
+	for _, l := range logfiles {
+		l.name = strings.Replace(l.name, "%USER%", username, -1)
+		filename := filepath.Join(tmpdir, l.name)
+		if err := ioutil.WriteFile(filename, []byte{}, 0644); err != nil {
+			t.Fatalf("ioutil.WriteFile failed: %v", err)
+		}
+		mtime := time.Now().Add(-l.age)
+		if err := os.Chtimes(filename, mtime, mtime); err != nil {
+			t.Fatalf("os.Chtimes failed: %v", err)
+		}
+		if l.link != "" {
+			if err := os.Symlink(l.name, filepath.Join(tmpdir, l.link)); err != nil {
+				t.Fatalf("os.Symlink failed: %v", err)
+			}
+		}
+	}
+	if err := os.Mkdir(filepath.Join(tmpdir, "subdir"), 0700); err != nil {
+		t.Fatalf("os.Mkdir failed: %v", err)
+	}
+	return
+}
+
+func TestGCLogs(t *testing.T) {
+	workdir, err := ioutil.TempDir("", "parse-file-info-")
+	if err != nil {
+		t.Fatalf("ioutil.TempDir failed: %v", err)
+	}
+	defer os.RemoveAll(workdir)
+
+	u, err := user.Current()
+	if err != nil {
+		t.Fatalf("user.Current failed: %v", err)
+	}
+
+	cmd := cmdGCLogs
+	var stdout, stderr bytes.Buffer
+	cmd.Init(nil, &stdout, &stderr)
+
+	testcases := []struct {
+		cutoff   time.Duration
+		verbose  bool
+		dryrun   bool
+		expected []string
+	}{
+		{
+			cutoff:  6 * time.Hour,
+			verbose: false,
+			dryrun:  false,
+			expected: []string{
+				`Processing: "%TESTDIR%"`,
+				`Number of files deleted: 0`,
+			},
+		},
+		{
+			cutoff:  2 * time.Hour,
+			verbose: false,
+			dryrun:  true,
+			expected: []string{
+				`Processing: "%TESTDIR%"`,
+				`Would delete "%TESTDIR%/prog1.host.%USER%.log.veyron.INFO.20141204-131502.12345"`,
+				`Would delete "%TESTDIR%/prog2.host.%USER%.log.veyron.INFO.20141204-135040.23456"`,
+				`Number of files deleted: 0`,
+			},
+		},
+		{
+			cutoff:  2 * time.Hour,
+			verbose: false,
+			dryrun:  false,
+			expected: []string{
+				`Processing: "%TESTDIR%"`,
+				`Number of files deleted: 3`,
+			},
+		},
+		{
+			cutoff:  2 * time.Hour,
+			verbose: true,
+			dryrun:  false,
+			expected: []string{
+				`Processing: "%TESTDIR%"`,
+				`Deleting "%TESTDIR%/prog1.host.%USER%.log.veyron.INFO.20141204-131502.12345"`,
+				`Deleting "%TESTDIR%/prog2.host.%USER%.log.veyron.INFO.20141204-135040.23456"`,
+				`Deleting symlink "%TESTDIR%/prog2.INFO"`,
+				`Not a log file: "%TESTDIR%/bar.txt"`,
+				`Not a log file: "%TESTDIR%/foo.txt"`,
+				`Skipped directory: "%TESTDIR%/subdir"`,
+				`Skipped log file created by other user: "%TESTDIR%/prog3.INFO"`,
+				`Skipped log file created by other user: "%TESTDIR%/prog3.host.otheruser.log.veyron.INFO.20141204-135040.23456"`,
+				`Number of files deleted: 3`,
+			},
+		},
+		{
+			cutoff:  time.Minute,
+			verbose: true,
+			dryrun:  false,
+			expected: []string{
+				`Processing: "%TESTDIR%"`,
+				`Deleting "%TESTDIR%/prog1.host.%USER%.log.veyron.ERROR.20141204-145040.23456"`,
+				`Deleting "%TESTDIR%/prog1.host.%USER%.log.veyron.INFO.20141204-131502.12345"`,
+				`Deleting "%TESTDIR%/prog1.host.%USER%.log.veyron.INFO.20141204-145040.23456"`,
+				`Deleting "%TESTDIR%/prog2.host.%USER%.log.veyron.INFO.20141204-135040.23456"`,
+				`Deleting symlink "%TESTDIR%/prog1.ERROR"`,
+				`Deleting symlink "%TESTDIR%/prog1.INFO"`,
+				`Deleting symlink "%TESTDIR%/prog2.INFO"`,
+				`Not a log file: "%TESTDIR%/bar.txt"`,
+				`Not a log file: "%TESTDIR%/foo.txt"`,
+				`Skipped directory: "%TESTDIR%/subdir"`,
+				`Skipped log file created by other user: "%TESTDIR%/prog3.INFO"`,
+				`Skipped log file created by other user: "%TESTDIR%/prog3.host.otheruser.log.veyron.INFO.20141204-135040.23456"`,
+				`Number of files deleted: 7`,
+			},
+		},
+		{
+			cutoff:  time.Minute,
+			verbose: false,
+			dryrun:  false,
+			expected: []string{
+				`Processing: "%TESTDIR%"`,
+				`Number of files deleted: 7`,
+			},
+		},
+	}
+	for _, tc := range testcases {
+		testdir := setup(t, workdir, u.Username)
+		cutoff := fmt.Sprintf("--cutoff=%s", tc.cutoff)
+		verbose := fmt.Sprintf("--verbose=%v", tc.verbose)
+		dryrun := fmt.Sprintf("--n=%v", tc.dryrun)
+		if err := cmd.Execute([]string{cutoff, verbose, dryrun, testdir}); err != nil {
+			t.Fatalf("%v: %v", stderr.String(), err)
+		}
+		gotsl := strings.Split(stdout.String(), "\n")
+		if len(gotsl) >= 2 {
+			sort.Strings(gotsl[1 : len(gotsl)-2])
+		}
+		got := strings.Join(gotsl, "\n")
+		expected := strings.Join(tc.expected, "\n") + "\n"
+		expected = strings.Replace(expected, "%TESTDIR%", testdir, -1)
+		expected = strings.Replace(expected, "%USER%", u.Username, -1)
+		if got != expected {
+			t.Errorf("Unexpected result for (%v, %v): got %q, expected %q", tc.cutoff, tc.verbose, got, expected)
+		}
+		stdout.Reset()
+	}
+}
diff --git a/cmd/gclogs/main.go b/cmd/gclogs/main.go
new file mode 100644
index 0000000..e642e83
--- /dev/null
+++ b/cmd/gclogs/main.go
@@ -0,0 +1,10 @@
+// The following enables go generate to generate the doc.go file.
+//go:generate go run $VANADIUM_ROOT/release/go/src/v.io/x/lib/cmdline/testdata/gendoc.go . -help
+
+package main
+
+import "os"
+
+func main() {
+	os.Exit(cmdGCLogs.Main())
+}
diff --git a/cmd/mgmt/device/devicex b/cmd/mgmt/device/devicex
new file mode 100755
index 0000000..fe74d56
--- /dev/null
+++ b/cmd/mgmt/device/devicex
@@ -0,0 +1,395 @@
+#!/bin/bash
+#
+# Administers a device manager installation.
+#
+# This script is a thin wrapper on top of the deviced commands.  Its main
+# purpose is to set up the installation by fetching the binaries required for a
+# device manager installation from a few possible sources and setting up the
+# setuid helper.
+
+set -e
+
+usage() {
+  echo "usage:"
+  echo
+  echo "Install device manager:"
+  echo "VANADIUM_DEVICE_DIR=<installation dir> ./devicex install [<binary source>] [ args for installer... ] [ -- args for device manager...]"
+  echo "  Possible values for <binary source>:"
+  echo "     unspecified: get binaries from local repository"
+  echo "     /path/to/binaries: get binaries from local filesystem"
+  echo "     http://host/path: get binaries from HTTP server"
+  echo
+  echo "Uninstall device manager:"
+  echo "VANADIUM_DEVICE_DIR=<installation dir> ./devicex uninstall"
+  echo
+  echo "Start device manager:"
+  echo "VANADIUM_DEVICE_DIR=<installation dir> ./devicex start"
+  echo
+  echo "Stop device manager:"
+  echo "VANADIUM_DEVICE_DIR=<installation dir> ./devicex stop"
+  echo "VANADIUM_DEVICE_DIR should be 0711 when running in multi-user"
+  echo "mode and all of its parents directories need to be at least"
+  echo "0511."
+}
+
+###############################################################################
+# Wrapper around chown that works differently on Mac and Linux
+# Arguments:
+#   arguments to chown command
+# Returns:
+#   None
+###############################################################################
+portable_chown() {
+  case "$(uname)" in
+    "Darwin")
+      sudo /usr/sbin/chown "$@"
+      ;;
+    "Linux")
+      sudo chown "$@"
+      ;;
+  esac
+}
+
+###############################################################################
+# Sets up the target to be owned by root with the suid bit on.
+# Arguments:
+#   path to target
+# Returns:
+#   None
+###############################################################################
+make_suid() {
+  local -r target="$1"
+  local root_group="root"
+  if [[ "$(uname)" == "Darwin" ]]; then
+    # Group root not available on Darwin.
+    root_group="wheel"
+  fi
+  portable_chown "root:${root_group}" "${target}"
+  sudo chmod 4551 "${target}"
+}
+
+###############################################################################
+# Runs a command as the device manager user.  Assumes VANADIUM_DEVICE_DIR exists
+# and gets the device manager user from the owner of that directory.
+# Globals:
+#   VANADIUM_DEVICE_DIR
+# Arguments:
+#   command to run and its arguments
+# Returns:
+#   None
+###############################################################################
+run() {
+  local -r devmgr_user=$(getdevowner)
+  if [[ "${devmgr_user}" == $(whoami) ]]; then
+    "$@"
+  else
+    sudo -u "${devmgr_user}" \
+      NAMESPACE_ROOT="${NAMESPACE_ROOT}" \
+      VANADIUM_DEVICE_DIR="${VANADIUM_DEVICE_DIR}" \
+      "$@"
+  fi
+}
+
+readonly BIN_NAMES=(deviced suidhelper agentd inithelper)
+
+###############################################################################
+# Copies one binary from source to destination.
+# Arguments:
+#   name of the binary
+#   source dir of binary
+#   destination dir of binary
+# Returns:
+#   None
+###############################################################################
+copy_binary() {
+  local -r BIN_NAME="$1"
+  local -r BIN_SRC_DIR="$2"
+  local -r BIN_DEST_DIR="$3"
+  local -r SOURCE="${BIN_SRC_DIR}/${BIN_NAME}"
+  if [[ -x "${SOURCE}" ]]; then
+    local -r DESTINATION="${BIN_DEST_DIR}/${BIN_NAME}"
+    cp "${SOURCE}" "${DESTINATION}"
+    chmod 700 "${DESTINATION}"
+  else
+    echo "couldn't find ${SOURCE}"
+    exit 1
+  fi
+}
+
+###############################################################################
+# Fetches binaries needed by device manager installation.
+# Globals:
+#   BIN_NAMES
+#   VANADIUM_ROOT
+# Arguments:
+#   destination for binaries
+#   source of binaries
+# Returns:
+#   None
+###############################################################################
+get_binaries() {
+  local -r BIN_INSTALL="$1"
+  local -r BIN_SOURCE="$2"
+
+  local bin_names_str=""
+  for bin_name in "${BIN_NAMES[@]}"; do
+    bin_names_str+=" ${bin_name}"
+  done
+
+  # If source is not specified, try to look for it in the repository.
+  if [[ -z "${BIN_SOURCE}" ]]; then
+    if [[ -z "${VANADIUM_ROOT}" ]]; then
+      echo 'ERROR: binary source not specified and no local repository available'
+      exit 1
+    fi
+    local -r REPO_BIN_DIR="${VANADIUM_ROOT}/release/go/bin"
+    echo "Fetching binaries:${bin_names_str} from build repository: ${REPO_BIN_DIR} ..."
+    for bin_name in "${BIN_NAMES[@]}"; do
+      copy_binary "${bin_name}" "${REPO_BIN_DIR}" "${BIN_INSTALL}"
+    done
+    return
+  fi
+
+  # If the source is specified as an existing local filesystem path,
+  # look for the binaries there.
+  if [[ -d "${BIN_SOURCE}" ]]; then
+      echo "Fetching binaries:${bin_names_str} locally from: ${BIN_SOURCE} ..."
+      for bin_name in "${BIN_NAMES[@]}"; do
+        copy_binary "${bin_name}" "${BIN_SOURCE}" "${BIN_INSTALL}"
+      done
+      return
+  fi
+
+  # If the source looks like a URL, use HTTP to fetch.
+  local -r URL_REGEXP='^(https?|ftp|file)://'
+  if [[ "${BIN_SOURCE}" =~ ${URL_REGEXP} ]]; then
+    echo "Fetching binaries:${bin_names_str} remotely from: ${BIN_SOURCE} ..."
+    for bin_name in "${BIN_NAMES[@]}"; do
+      local DEST="${BIN_INSTALL}/${bin_name}"
+      curl -f -o "${DEST}" "${BIN_SOURCE}/${bin_name}"
+      chmod 700 "${DEST}"
+    done
+    return
+  fi
+
+  echo 'ERROR: couldn'"'"'t fetch binaries.'
+  exit 1
+}
+
+###############################################################################
+# Installs device manager: fetches binaries, configures suidhelper, calls the
+# install command on deviced.
+# Globals:
+#   VANADIUM_DEVICE_DIR
+# Arguments:
+#   source of binaries (optional)
+#   args for install command and for device manager (optional)
+# Returns:
+#   None
+###############################################################################
+install() {
+  if [[ -e "${VANADIUM_DEVICE_DIR}" ]]; then
+    echo "${VANADIUM_DEVICE_DIR} already exists!"
+    exit 1
+  fi
+  mkdir -m 711 "${VANADIUM_DEVICE_DIR}"
+  local -r BIN_INSTALL="${VANADIUM_DEVICE_DIR}/bin"
+  mkdir -m 700 "${BIN_INSTALL}"
+
+  # Fetch the binaries.
+  if [[ $# = 0 || "$1" == --* ]]; then
+    local -r BIN_SOURCE=""
+  else
+    local -r BIN_SOURCE="$1"
+    shift
+  fi
+  get_binaries "${BIN_INSTALL}" "${BIN_SOURCE}"
+  for bin_name in "${BIN_NAMES[@]}"; do
+    local BINARY="${BIN_INSTALL}/${bin_name}"
+    if [[ ! -s "${BINARY}" ]]; then
+      echo "${BINARY} is empty."
+      exit 1
+    fi
+  done
+  echo "Binaries are in ${BIN_INSTALL}."
+
+  # Set up the suidhelper.
+  echo "Configuring helpers ..."
+  local SINGLE_USER=false
+  local INIT_MODE=false
+  local DEVMGR_USER=$(whoami)
+  for ARG in $*; do
+    if [[ ${ARG} = "--" ]]; then
+      break
+    elif [[ ${ARG} = "--single_user" || ${ARG} = "--single_user=true" ]]; then
+      SINGLE_USER=true
+    elif [[ ${ARG} = "--init_mode" || ${ARG} = "--init_mode=true" ]]; then
+      INIT_MODE=true
+    elif [[ ${ARG} =~ --devuser=(.*) ]]; then
+      DEVMGR_USER="${BASH_REMATCH[1]}"
+    fi
+  done
+
+  if [[ ${SINGLE_USER} == false && ${DEVMGR_USER} == $(whoami) ]]; then
+    echo "Running in multi-user mode requires a --devuser=<user>"
+    echo "argument. This limits the following unfortunate chain of events:"
+    echo "install the device manager as yourself, associate an external blessee"
+    echo "with your local user name and the external blessee can invoke an app"
+    echo "which, because it has the same system name as the device manager,"
+    echo "can use suidhelper to give itself root priviledge."
+    exit 1
+  fi
+  if [[ ${SINGLE_USER}} == true && ${DEVMGR_USER} != $(whoami) ]]; then
+    echo "The --devuser flag is unnecessary in single-user mode because"
+    echo "all processes run as $(whoami)."
+    exit 1
+  fi
+  local -r SETUID_SCRIPT="${BIN_INSTALL}/suidhelper"
+  if [[ ${SINGLE_USER} == false ]]; then
+    portable_chown -R "${DEVMGR_USER}:${DEVMGR_USER}" "${VANADIUM_DEVICE_DIR}"
+    make_suid "${SETUID_SCRIPT}"
+  fi
+  local -r INIT_SCRIPT="${BIN_INSTALL}/inithelper"
+  if [[ ${INIT_MODE} == true ]]; then
+    make_suid "${INIT_SCRIPT}"
+  fi
+  echo "Helpers configured."
+
+  # Install the device manager.
+  echo "Installing device manager under ${VANADIUM_DEVICE_DIR} ..."
+  echo "VANADIUM_DEVICE_DIR=${VANADIUM_DEVICE_DIR}"
+  run "${BIN_INSTALL}/deviced" install \
+    --suid_helper="${SETUID_SCRIPT}" \
+    --agent="${BIN_INSTALL}/agentd" \
+    --init_helper="${INIT_SCRIPT}" "$@"
+  echo "Device manager installed."
+}
+
+###############################################################################
+# Determines the owner of the device manager
+# Globals:
+#   VANADIUM_DEVICE_DIR
+# Arguments:
+#   None
+# Returns:
+#  user owning the device manager
+###############################################################################
+getdevowner() {
+  case "$(uname)" in
+    "Darwin")
+      ls -dl  "${VANADIUM_DEVICE_DIR}" | awk '{print $3}'
+      ;;
+    "Linux")
+      stat -c "%U" "${VANADIUM_DEVICE_DIR}"
+      ;;
+  esac
+}
+
+###############################################################################
+# Uninstalls device manager: calls the uninstall command of deviced and removes
+# the installation.
+# Globals:
+#   VANADIUM_DEVICE_DIR
+# Arguments:
+#   None
+# Returns:
+#   None
+###############################################################################
+uninstall() {
+  if [[ ! -d "${VANADIUM_DEVICE_DIR}" ]]; then
+    echo "${VANADIUM_DEVICE_DIR} is not a directory!"
+    exit 1
+  fi
+  local -r BIN_INSTALL="${VANADIUM_DEVICE_DIR}/bin"
+  local -r SETUID_SCRIPT="${BIN_INSTALL}/suidhelper"
+  echo "Uninstalling device manager from ${VANADIUM_DEVICE_DIR} ..."
+  run "${BIN_INSTALL}/deviced" uninstall \
+    --suid_helper="${SETUID_SCRIPT}"
+
+   echo "Device manager uninstalled."
+   # Any data created underneath "${VANADIUM_DEVICE_DIR}" by the "deviced
+   # install" command would have been cleaned up already by "deviced uninstall".
+   # However, install() created "${VANADIUM_DEVICE_DIR}", so uninstall() needs
+   # to remove it (as well as data created by install(), like bin/*).
+
+   run rm -rf "${VANADIUM_DEVICE_DIR}/bin"
+   rmdir "${VANADIUM_DEVICE_DIR}"
+   echo "Removed ${VANADIUM_DEVICE_DIR}"
+}
+
+###############################################################################
+# Starts device manager: calls the start command of deviced.
+# Globals:
+#   VANADIUM_DEVICE_DIR
+# Arguments:
+#   None
+# Returns:
+#   None
+###############################################################################
+start() {
+  if [[ ! -d "${VANADIUM_DEVICE_DIR}" ]]; then
+    echo "${VANADIUM_DEVICE_DIR} is not a directory!"
+    exit 1
+  fi
+  local -r BIN_INSTALL="${VANADIUM_DEVICE_DIR}/bin"
+    run "${BIN_INSTALL}/deviced" start
+}
+
+###############################################################################
+# Stops device manager: calls the stop command of deviced.
+# Globals:
+#   VANADIUM_DEVICE_DIR
+# Arguments:
+#   None
+# Returns:
+#   None
+###############################################################################
+stop() {
+  if [[ ! -d "${VANADIUM_DEVICE_DIR}" ]]; then
+    echo "${VANADIUM_DEVICE_DIR} is not a directory!"
+    exit 1
+  fi
+  local -r BIN_INSTALL="${VANADIUM_DEVICE_DIR}/bin"
+  run "${BIN_INSTALL}/deviced" stop
+}
+
+main() {
+  if [[ -z "${VANADIUM_DEVICE_DIR}" ]]; then
+    echo 'No local device installation dir specified!'
+    usage
+    exit 1
+  fi
+  if [[ -e "${VANADIUM_DEVICE_DIR}" && ! -d "${VANADIUM_DEVICE_DIR}" ]]; then
+    echo "${VANADIUM_DEVICE_DIR} is not a directory!"
+    usage
+    exit 1
+  fi
+
+  if [[ $# = 0 ]]; then
+    echo 'No command specified!'
+    usage
+    exit 1
+  fi
+  local -r COMMAND="$1"
+  shift
+  case "${COMMAND}" in
+    install)
+      install "$@"
+      ;;
+    uninstall)
+      uninstall
+      ;;
+    start)
+      start
+      ;;
+    stop)
+      stop
+      ;;
+    *)
+      echo "Unrecognized command: ${COMMAND}!"
+      usage
+      exit 1
+  esac
+}
+
+main "$@"
diff --git a/cmd/mgmt/device/doc.go b/cmd/mgmt/device/doc.go
new file mode 100644
index 0000000..9c7f62e
--- /dev/null
+++ b/cmd/mgmt/device/doc.go
@@ -0,0 +1,334 @@
+// This file was auto-generated via go generate.
+// DO NOT UPDATE MANUALLY
+
+/*
+The device tool facilitates interaction with the veyron device manager.
+
+Usage:
+   device <command>
+
+The device commands are:
+   install       Install the given application.
+   install-local Install the given application from the local system.
+   uninstall     Uninstall the given application installation.
+   start         Start an instance of the given application.
+   associate     Tool for creating associations between Vanadium blessings and a
+                 system account
+   describe      Describe the device.
+   claim         Claim the device.
+   stop          Stop the given application instance.
+   suspend       Suspend the given application instance.
+   resume        Resume the given application instance.
+   revert        Revert the device manager or application
+   update        Update the device manager or application
+   debug         Debug the device.
+   acl           Tool for setting device manager ACLs
+   help          Display help for commands or topics
+Run "device help [command]" for command usage.
+
+The global flags are:
+ -alsologtostderr=true
+   log to standard error as well as files
+ -log_backtrace_at=:0
+   when logging hits line file:N, emit a stack trace
+ -log_dir=
+   if non-empty, write log files to this directory
+ -logtostderr=false
+   log to standard error instead of files
+ -max_stack_buf_size=4292608
+   max size in bytes of the buffer to use for logging stack traces
+ -stderrthreshold=2
+   logs at or above this threshold go to stderr
+ -v=0
+   log level for V logs
+ -vanadium.i18n_catalogue=
+   18n catalogue files to load, comma separated
+ -veyron.acl.file=map[]
+   specify an acl file as <name>:<aclfile>
+ -veyron.acl.literal=
+   explicitly specify the runtime acl as a JSON-encoded access.TaggedACLMap.
+   Overrides all --veyron.acl.file flags.
+ -veyron.credentials=
+   directory to use for storing security credentials
+ -veyron.namespace.root=[/ns.dev.v.io:8101]
+   local namespace root; can be repeated to provided multiple roots
+ -veyron.proxy=
+   object name of proxy service to use to export services across network
+   boundaries
+ -veyron.tcp.address=
+   address to listen on
+ -veyron.tcp.protocol=wsh
+   protocol to listen with
+ -veyron.vtrace.cache_size=1024
+   The number of vtrace traces to store in memory.
+ -veyron.vtrace.collect_regexp=
+   Spans and annotations that match this regular expression will trigger trace
+   collection.
+ -veyron.vtrace.dump_on_shutdown=true
+   If true, dump all stored traces on runtime shutdown.
+ -veyron.vtrace.sample_rate=0
+   Rate (from 0.0 to 1.0) to sample vtrace traces.
+ -vmodule=
+   comma-separated list of pattern=N settings for file-filtered logging
+
+Device Install
+
+Install the given application.
+
+Usage:
+   device install [flags] <device> <application>
+
+<device> is the veyron object name of the device manager's app service.
+
+<application> is the veyron object name of the application.
+
+The device install flags are:
+ -config={}
+   JSON-encoded device.Config object, of the form:
+   '{"flag1":"value1","flag2":"value2"}'
+ -packages={}
+   JSON-encoded application.Packages object, of the form:
+   '{"pkg1":{"File":"object name 1"},"pkg2":{"File":"object name 2"}}'
+
+Device Install-Local
+
+Install the given application, specified using a local path.
+
+Usage:
+   device install-local [flags] <device> <title> [ENV=VAL ...] binary [--flag=val ...] [PACKAGES path ...]
+
+<device> is the veyron object name of the device manager's app service.
+
+<title> is the app title.
+
+This is followed by an arbitrary number of environment variable settings, the
+local path for the binary to install, and arbitrary flag settings and args.
+Optionally, this can be followed by 'PACKAGES' and a list of local files and
+directories to be installed as packages for the app
+
+The device install-local flags are:
+ -config={}
+   JSON-encoded device.Config object, of the form:
+   '{"flag1":"value1","flag2":"value2"}'
+ -packages={}
+   JSON-encoded application.Packages object, of the form:
+   '{"pkg1":{"File":"local file path1"},"pkg2":{"File":"local file path 2"}}'
+
+Device Uninstall
+
+Uninstall the given application installation.
+
+Usage:
+   device uninstall <installation>
+
+<installation> is the veyron object name of the application installation to
+uninstall.
+
+Device Start
+
+Start an instance of the given application.
+
+Usage:
+   device start <application installation> <grant extension>
+
+<application installation> is the veyron object name of the application
+installation from which to start an instance.
+
+<grant extension> is used to extend the default blessing of the current
+principal when blessing the app instance.
+
+Device Associate
+
+The associate tool facilitates managing blessing to system account associations.
+
+Usage:
+   device associate <command>
+
+The device associate commands are:
+   list        Lists the account associations.
+   add         Add the listed blessings with the specified system account.
+   remove      Removes system accounts associated with the listed blessings.
+
+Device Associate List
+
+Lists all account associations.
+
+Usage:
+   device associate list <devicemanager>.
+
+<devicemanager> is the name of the device manager to connect to.
+
+Device Associate Add
+
+Add the listed blessings with the specified system account.
+
+Usage:
+   device associate add <devicemanager> <systemName> <blessing>...
+
+<devicemanager> is the name of the device manager to connect to. <systemName> is
+the name of an account holder on the local system. <blessing>.. are the
+blessings to associate systemAccount with.
+
+Device Associate Remove
+
+Removes system accounts associated with the listed blessings.
+
+Usage:
+   device associate remove <devicemanager>  <blessing>...
+
+<devicemanager> is the name of the device manager to connect to. <blessing>...
+is a list of blessings.
+
+Device Describe
+
+Describe the device.
+
+Usage:
+   device describe <device>
+
+<device> is the veyron object name of the device manager's device service.
+
+Device Claim
+
+Claim the device.
+
+Usage:
+   device claim <device> <grant extension> <pairing token> <device publickey>
+
+<device> is the veyron object name of the device manager's device service.
+
+<grant extension> is used to extend the default blessing of the current
+principal when blessing the app instance.
+
+<pairing token> is a token that the device manager expects to be replayed during
+a claim operation on the device.
+
+<device publickey> is the marshalled public key of the device manager we are
+claiming.
+
+Device Stop
+
+Stop the given application instance.
+
+Usage:
+   device stop <app instance>
+
+<app instance> is the veyron object name of the application instance to stop.
+
+Device Suspend
+
+Suspend the given application instance.
+
+Usage:
+   device suspend <app instance>
+
+<app instance> is the veyron object name of the application instance to suspend.
+
+Device Resume
+
+Resume the given application instance.
+
+Usage:
+   device resume <app instance>
+
+<app instance> is the veyron object name of the application instance to resume.
+
+Device Revert
+
+Revert the device manager or application to its previous version
+
+Usage:
+   device revert <object>
+
+<object> is the veyron object name of the device manager or application
+installation to revert.
+
+Device Update
+
+Update the device manager or application
+
+Usage:
+   device update <object>
+
+<object> is the veyron object name of the device manager or application
+installation to update.
+
+Device Debug
+
+Debug the device.
+
+Usage:
+   device debug <device>
+
+<device> is the veyron object name of an app installation or instance.
+
+Device Acl
+
+The acl tool manages ACLs on the device manger, installations and instances.
+
+Usage:
+   device acl <command>
+
+The device acl commands are:
+   get         Get ACLs for the given target.
+   set         Set ACLs for the given target.
+
+Device Acl Get
+
+Get ACLs for the given target.
+
+Usage:
+   device acl get <device manager name>
+
+<device manager name> can be a Vanadium name for a device manager, application
+installation or instance.
+
+Device Acl Set
+
+Set ACLs for the given target
+
+Usage:
+   device acl set <device manager name>  (<blessing> [!]<tag>(,[!]<tag>)*
+
+<device manager name> can be a Vanadium name for a device manager, application
+installation or instance.
+
+<blessing> is a blessing pattern. If the same pattern is repeated multiple times
+in the command, then the only the last occurrence will be honored.
+
+<tag> is a subset of defined access types ("Admin", "Read", "Write" etc.). If
+the access right is prefixed with a '!' then <blessing> is added to the NotIn
+list for that right. Using "^" as a "tag" causes all occurrences of <blessing>
+in the current ACL to be cleared.
+
+Examples: set root/self ^ will remove "root/self" from the In and NotIn lists
+for all access rights.
+
+set root/self Read,!Write will add "root/self" to the In list for Read access
+and the NotIn list for Write access (and remove "root/self" from both the In and
+NotIn lists of all other access rights)
+
+Device Help
+
+Help with no args displays the usage of the parent command.
+
+Help with args displays the usage of the specified sub-command or help topic.
+
+"help ..." recursively displays help for all commands and topics.
+
+The output is formatted to a target width in runes.  The target width is
+determined by checking the environment variable CMDLINE_WIDTH, falling back on
+the terminal width from the OS, falling back on 80 chars.  By setting
+CMDLINE_WIDTH=x, if x > 0 the width is x, if x < 0 the width is unlimited, and
+if x == 0 or is unset one of the fallbacks is used.
+
+Usage:
+   device help [flags] [command/topic ...]
+
+[command/topic ...] optionally identifies a specific sub-command or help topic.
+
+The device help flags are:
+ -style=text
+   The formatting style for help output, either "text" or "godoc".
+*/
+package main
diff --git a/cmd/mgmt/device/impl/acl_fmt.go b/cmd/mgmt/device/impl/acl_fmt.go
new file mode 100644
index 0000000..a0e3500
--- /dev/null
+++ b/cmd/mgmt/device/impl/acl_fmt.go
@@ -0,0 +1,84 @@
+package impl
+
+import (
+	"fmt"
+	"sort"
+	"strings"
+
+	"v.io/v23/security"
+)
+
+// aclEntries maps blessing patterns to the kind of access they should have.
+type aclEntries map[string]accessTags
+
+// accessTags maps access tags to whether they should be blacklisted
+// (i.e., part of the NotIn list) or not (part of the In list).
+//
+// TODO(ashankar,caprita): This structure is not friendly to a blessing
+// appearing in both the "In" and "NotIn" lists of an ACL. Arguably, such an
+// ACL is silly (In: ["foo"], NotIn: ["foo"]), but is legal. This structure can
+// end up hiding that.
+type accessTags map[string]bool
+
+// String representation of access tags.  Between String and parseAccessTags,
+// the "get" and "set" commands are able to speak the same language: the output
+// of "get" and be copied/pasted into "set".
+func (tags accessTags) String() string {
+	// Sort tags and then apply "!".
+	var list []string
+	for tag, _ := range tags {
+		list = append(list, tag)
+	}
+	sort.Strings(list)
+	for ix, tag := range list {
+		if tags[tag] {
+			list[ix] = "!" + list[ix]
+		}
+	}
+	return strings.Join(list, ",")
+}
+
+func parseAccessTags(input string) (accessTags, error) {
+	ret := make(accessTags)
+	if input == "^" {
+		return ret, nil
+	}
+	for _, tag := range strings.Split(input, ",") {
+		blacklist := strings.HasPrefix(tag, "!")
+		if blacklist {
+			tag = tag[1:]
+		}
+		if len(tag) == 0 {
+			return nil, fmt.Errorf("empty access tag in %q", input)
+		}
+		ret[tag] = blacklist
+	}
+	return ret, nil
+}
+
+func (entries aclEntries) String() string {
+	var list []string
+	for pattern, _ := range entries {
+		list = append(list, pattern)
+	}
+	sort.Strings(list)
+	for ix, pattern := range list {
+		list[ix] = fmt.Sprintf("%s %v", pattern, entries[pattern])
+	}
+	return strings.Join(list, "\n")
+}
+
+func (entries aclEntries) Tags(pattern string) accessTags {
+	tags, exists := entries[pattern]
+	if !exists {
+		tags = make(accessTags)
+		entries[pattern] = tags
+	}
+	return tags
+}
+
+type byPattern []security.BlessingPattern
+
+func (a byPattern) Len() int           { return len(a) }
+func (a byPattern) Swap(i, j int)      { a[i], a[j] = a[j], a[i] }
+func (a byPattern) Less(i, j int) bool { return a[i] < a[j] }
diff --git a/cmd/mgmt/device/impl/acl_impl.go b/cmd/mgmt/device/impl/acl_impl.go
new file mode 100644
index 0000000..ba06f07
--- /dev/null
+++ b/cmd/mgmt/device/impl/acl_impl.go
@@ -0,0 +1,132 @@
+package impl
+
+// Commands to get/set ACLs.
+
+import (
+	"fmt"
+
+	"v.io/v23/security"
+	"v.io/v23/services/mgmt/device"
+	"v.io/v23/verror"
+	"v.io/x/lib/cmdline"
+)
+
+var cmdGet = &cmdline.Command{
+	Run:      runGet,
+	Name:     "get",
+	Short:    "Get ACLs for the given target.",
+	Long:     "Get ACLs for the given target.",
+	ArgsName: "<device manager name>",
+	ArgsLong: `
+<device manager name> can be a Vanadium name for a device manager,
+application installation or instance.`,
+}
+
+func runGet(cmd *cmdline.Command, args []string) error {
+	if expected, got := 1, len(args); expected != got {
+		return cmd.UsageErrorf("get: incorrect number of arguments, expected %d, got %d", expected, got)
+	}
+
+	vanaName := args[0]
+	objACL, _, err := device.ApplicationClient(vanaName).GetACL(gctx)
+	if err != nil {
+		return fmt.Errorf("GetACL on %s failed: %v", vanaName, err)
+	}
+	// Convert objACL (TaggedACLMap) into aclEntries for pretty printing.
+	entries := make(aclEntries)
+	for tag, acl := range objACL {
+		for _, p := range acl.In {
+			entries.Tags(string(p))[tag] = false
+		}
+		for _, b := range acl.NotIn {
+			entries.Tags(b)[tag] = true
+		}
+	}
+	fmt.Fprintln(cmd.Stdout(), entries)
+	return nil
+}
+
+var cmdSet = &cmdline.Command{
+	Run:      runSet,
+	Name:     "set",
+	Short:    "Set ACLs for the given target.",
+	Long:     "Set ACLs for the given target",
+	ArgsName: "<device manager name>  (<blessing> [!]<tag>(,[!]<tag>)*",
+	ArgsLong: `
+<device manager name> can be a Vanadium name for a device manager,
+application installation or instance.
+
+<blessing> is a blessing pattern.
+If the same pattern is repeated multiple times in the command, then
+the only the last occurrence will be honored.
+
+<tag> is a subset of defined access types ("Admin", "Read", "Write" etc.).
+If the access right is prefixed with a '!' then <blessing> is added to the
+NotIn list for that right. Using "^" as a "tag" causes all occurrences of
+<blessing> in the current ACL to be cleared.
+
+Examples:
+set root/self ^
+will remove "root/self" from the In and NotIn lists for all access rights.
+
+set root/self Read,!Write
+will add "root/self" to the In list for Read access and the NotIn list
+for Write access (and remove "root/self" from both the In and NotIn
+lists of all other access rights)`,
+}
+
+func runSet(cmd *cmdline.Command, args []string) error {
+	if got := len(args); !((got%2) == 1 && got >= 3) {
+		return cmd.UsageErrorf("set: incorrect number of arguments %d, must be 1 + 2n", got)
+	}
+
+	vanaName := args[0]
+	pairs := args[1:]
+
+	entries := make(aclEntries)
+	for i := 0; i < len(pairs); i += 2 {
+		blessing := pairs[i]
+		tags, err := parseAccessTags(pairs[i+1])
+		if err != nil {
+			return cmd.UsageErrorf("failed to parse access tags for %q: %v", blessing, err)
+		}
+		entries[blessing] = tags
+	}
+
+	// Set the ACLs on the specified names.
+	for {
+		objACL, etag, err := device.ApplicationClient(vanaName).GetACL(gctx)
+		if err != nil {
+			return cmd.UsageErrorf("GetACL(%s) failed: %v", vanaName, err)
+		}
+		for blessingOrPattern, tags := range entries {
+			objACL.Clear(blessingOrPattern) // Clear out any existing references
+			for tag, blacklist := range tags {
+				if blacklist {
+					objACL.Blacklist(blessingOrPattern, tag)
+				} else {
+					objACL.Add(security.BlessingPattern(blessingOrPattern), tag)
+				}
+			}
+		}
+		switch err := device.ApplicationClient(vanaName).SetACL(gctx, objACL, etag); {
+		case err != nil && !verror.Is(err, verror.ErrBadEtag.ID):
+			return cmd.UsageErrorf("SetACL(%s) failed: %v", vanaName, err)
+		case err == nil:
+			return nil
+		}
+		fmt.Fprintln(cmd.Stderr(), "WARNING: trying again because of asynchronous change")
+	}
+	return nil
+}
+
+func aclRoot() *cmdline.Command {
+	return &cmdline.Command{
+		Name:  "acl",
+		Short: "Tool for setting device manager ACLs",
+		Long: `
+The acl tool manages ACLs on the device manger, installations and instances.
+`,
+		Children: []*cmdline.Command{cmdGet, cmdSet},
+	}
+}
diff --git a/cmd/mgmt/device/impl/acl_test.go b/cmd/mgmt/device/impl/acl_test.go
new file mode 100644
index 0000000..d49e08c
--- /dev/null
+++ b/cmd/mgmt/device/impl/acl_test.go
@@ -0,0 +1,299 @@
+package impl_test
+
+import (
+	"bytes"
+	"reflect"
+	"regexp"
+	"strings"
+	"testing"
+
+	"v.io/v23/security"
+	"v.io/v23/services/security/access"
+	"v.io/v23/verror"
+
+	"v.io/x/ref/cmd/mgmt/device/impl"
+)
+
+const pkgPath = "v.io/x/ref/cmd/mgmt/device/main"
+
+var (
+	errOops = verror.Register(pkgPath+".errOops", verror.NoRetry, "oops!")
+)
+
+func TestACLGetCommand(t *testing.T) {
+	shutdown := initTest()
+	defer shutdown()
+
+	tape := NewTape()
+	server, endpoint, err := startServer(t, gctx, tape)
+	if err != nil {
+		return
+	}
+	defer stopServer(t, server)
+
+	// Setup the command-line.
+	cmd := impl.Root()
+	var stdout, stderr bytes.Buffer
+	cmd.Init(nil, &stdout, &stderr)
+	deviceName := endpoint.Name()
+
+	// Test the 'get' command.
+	tape.SetResponses([]interface{}{GetACLResponse{
+		acl: access.TaggedACLMap{
+			"Admin": access.ACL{
+				In:    []security.BlessingPattern{"self"},
+				NotIn: []string{"self/bad"},
+			},
+			"Read": access.ACL{
+				In: []security.BlessingPattern{"other", "self"},
+			},
+		},
+		etag: "anEtagForToday",
+		err:  nil,
+	}})
+
+	if err := cmd.Execute([]string{"acl", "get", deviceName}); err != nil {
+		t.Fatalf("%v, output: %v, error: %v", err)
+	}
+	if expected, got := strings.TrimSpace(`
+other Read
+self Admin,Read
+self/bad !Admin
+`), strings.TrimSpace(stdout.String()); got != expected {
+		t.Errorf("Unexpected output from get. Got %q, expected %q", got, expected)
+	}
+	if got, expected := tape.Play(), []interface{}{"GetACL"}; !reflect.DeepEqual(expected, got) {
+		t.Errorf("invalid call sequence. Got %#v, want %#v", got, expected)
+	}
+	tape.Rewind()
+	stdout.Reset()
+}
+
+func TestACLSetCommand(t *testing.T) {
+	shutdown := initTest()
+	defer shutdown()
+
+	tape := NewTape()
+	server, endpoint, err := startServer(t, gctx, tape)
+	if err != nil {
+		return
+	}
+	defer stopServer(t, server)
+
+	// Setup the command-line.
+	cmd := impl.Root()
+	var stdout, stderr bytes.Buffer
+	cmd.Init(nil, &stdout, &stderr)
+	deviceName := endpoint.Name()
+
+	// Some tests to validate parse.
+	if err := cmd.Execute([]string{"acl", "set", deviceName}); err == nil {
+		t.Fatalf("failed to correctly detect insufficient parameters")
+	}
+	if expected, got := "ERROR: set: incorrect number of arguments 1, must be 1 + 2n", strings.TrimSpace(stderr.String()); !strings.HasPrefix(got, expected) {
+		t.Fatalf("Unexpected output from list. Got %q, expected prefix %q", got, expected)
+	}
+
+	stderr.Reset()
+	stdout.Reset()
+	if err := cmd.Execute([]string{"acl", "set", deviceName, "foo"}); err == nil {
+		t.Fatalf("failed to correctly detect insufficient parameters")
+	}
+	if expected, got := "ERROR: set: incorrect number of arguments 2, must be 1 + 2n", strings.TrimSpace(stderr.String()); !strings.HasPrefix(got, expected) {
+		t.Fatalf("Unexpected output from list. Got %q, expected prefix %q", got, expected)
+	}
+
+	stderr.Reset()
+	stdout.Reset()
+	if err := cmd.Execute([]string{"acl", "set", deviceName, "foo", "bar", "ohno"}); err == nil {
+		t.Fatalf("failed to correctly detect insufficient parameters")
+	}
+	if expected, got := "ERROR: set: incorrect number of arguments 4, must be 1 + 2n", strings.TrimSpace(stderr.String()); !strings.HasPrefix(got, expected) {
+		t.Fatalf("Unexpected output from list. Got %q, expected prefix %q", got, expected)
+	}
+
+	stderr.Reset()
+	stdout.Reset()
+	if err := cmd.Execute([]string{"acl", "set", deviceName, "foo", "!"}); err == nil {
+		t.Fatalf("failed to detect invalid parameter")
+	}
+	if expected, got := "ERROR: failed to parse access tags for \"foo\": empty access tag", strings.TrimSpace(stderr.String()); !strings.HasPrefix(got, expected) {
+		t.Errorf("Unexpected output from list. Got %q, expected prefix %q", got, expected)
+	}
+
+	// Correct operation in the absence of errors.
+	stderr.Reset()
+	stdout.Reset()
+	tape.SetResponses([]interface{}{GetACLResponse{
+		acl: access.TaggedACLMap{
+			"Admin": access.ACL{
+				In: []security.BlessingPattern{"self"},
+			},
+			"Read": access.ACL{
+				In:    []security.BlessingPattern{"other", "self"},
+				NotIn: []string{"other/bob"},
+			},
+		},
+		etag: "anEtagForToday",
+		err:  nil,
+	},
+		verror.NewErrBadEtag(nil),
+		GetACLResponse{
+			acl: access.TaggedACLMap{
+				"Admin": access.ACL{
+					In: []security.BlessingPattern{"self"},
+				},
+				"Read": access.ACL{
+					In:    []security.BlessingPattern{"other", "self"},
+					NotIn: []string{"other/bob/baddevice"},
+				},
+			},
+			etag: "anEtagForTomorrow",
+			err:  nil,
+		},
+		nil,
+	})
+
+	// set command that:
+	// - Adds entry for "friends" to "Write" & "Admin"
+	// - Adds a blacklist entry for "friend/alice"  for "Admin"
+	// - Edits existing entry for "self" (adding "Write" access)
+	// - Removes entry for "other/bob/baddevice"
+	if err := cmd.Execute([]string{
+		"acl",
+		"set",
+		deviceName,
+		"friends", "Admin,Write",
+		"friends/alice", "!Admin,Write",
+		"self", "Admin,Write,Read",
+		"other/bob/baddevice", "^",
+	}); err != nil {
+		t.Fatalf("SetACL failed: %v", err)
+	}
+
+	if expected, got := "", strings.TrimSpace(stdout.String()); got != expected {
+		t.Fatalf("Unexpected output from list. Got %q, expected %q", got, expected)
+	}
+	if expected, got := "WARNING: trying again because of asynchronous change", strings.TrimSpace(stderr.String()); got != expected {
+		t.Fatalf("Unexpected output from list. Got %q, expected %q", got, expected)
+	}
+	expected := []interface{}{
+		"GetACL",
+		SetACLStimulus{
+			fun: "SetACL",
+			acl: access.TaggedACLMap{
+				"Admin": access.ACL{
+					In:    []security.BlessingPattern{"friends", "self"},
+					NotIn: []string{"friends/alice"},
+				},
+				"Read": access.ACL{
+					In:    []security.BlessingPattern{"other", "self"},
+					NotIn: []string{"other/bob"},
+				},
+				"Write": access.ACL{
+					In:    []security.BlessingPattern{"friends", "friends/alice", "self"},
+					NotIn: []string(nil),
+				},
+			},
+			etag: "anEtagForToday",
+		},
+		"GetACL",
+		SetACLStimulus{
+			fun: "SetACL",
+			acl: access.TaggedACLMap{
+				"Admin": access.ACL{
+					In:    []security.BlessingPattern{"friends", "self"},
+					NotIn: []string{"friends/alice"},
+				},
+				"Read": access.ACL{
+					In:    []security.BlessingPattern{"other", "self"},
+					NotIn: []string(nil),
+				},
+				"Write": access.ACL{
+					In:    []security.BlessingPattern{"friends", "friends/alice", "self"},
+					NotIn: []string(nil),
+				},
+			},
+			etag: "anEtagForTomorrow",
+		},
+	}
+
+	if got := tape.Play(); !reflect.DeepEqual(expected, got) {
+		t.Errorf("invalid call sequence. Got %#v, want %#v", got, expected)
+	}
+	tape.Rewind()
+	stdout.Reset()
+	stderr.Reset()
+
+	// GetACL fails.
+	tape.SetResponses([]interface{}{GetACLResponse{
+		acl:  access.TaggedACLMap{},
+		etag: "anEtagForToday",
+		err:  verror.New(errOops, nil),
+	},
+	})
+
+	if err := cmd.Execute([]string{"acl", "set", deviceName, "vana/bad", "Read"}); err == nil {
+		t.Fatalf("GetACL RPC inside acl set command failed but error wrongly not detected")
+	}
+	if expected, got := `^ERROR: GetACL\(`+deviceName+`\) failed:.*oops!`, strings.TrimSpace(stderr.String()); !regexp.MustCompile(expected).MatchString(got) {
+		t.Fatalf("Unexpected output from list. Got %q, regexp %q", got, expected)
+	}
+	if expected, got := "", strings.TrimSpace(stdout.String()); got != expected {
+		t.Fatalf("Unexpected output from list. Got %q, expected %q", got, expected)
+	}
+	expected = []interface{}{
+		"GetACL",
+	}
+
+	if got := tape.Play(); !reflect.DeepEqual(expected, got) {
+		t.Errorf("invalid call sequence. Got %#v, want %#v", got, expected)
+	}
+	tape.Rewind()
+	stdout.Reset()
+	stderr.Reset()
+
+	// SetACL fails with something other than a bad etag failure.
+	tape.SetResponses([]interface{}{GetACLResponse{
+		acl: access.TaggedACLMap{
+			"Read": access.ACL{
+				In: []security.BlessingPattern{"other", "self"},
+			},
+		},
+		etag: "anEtagForToday",
+		err:  nil,
+	},
+		verror.New(errOops, nil),
+	})
+
+	if err := cmd.Execute([]string{"acl", "set", deviceName, "friend", "Read"}); err == nil {
+		t.Fatalf("SetACL should have failed: %v", err)
+	}
+	if expected, got := "", strings.TrimSpace(stdout.String()); got != expected {
+		t.Fatalf("Unexpected output from list. Got %q, expected %q", got, expected)
+	}
+	if expected, got := `^ERROR: SetACL\(`+deviceName+`\) failed:.*oops!`, strings.TrimSpace(stderr.String()); !regexp.MustCompile(expected).MatchString(got) {
+		t.Fatalf("Unexpected output from list. Got %q, regexp %q", got, expected)
+	}
+
+	expected = []interface{}{
+		"GetACL",
+		SetACLStimulus{
+			fun: "SetACL",
+			acl: access.TaggedACLMap{
+				"Read": access.ACL{
+					In:    []security.BlessingPattern{"friend", "other", "self"},
+					NotIn: []string(nil),
+				},
+			},
+			etag: "anEtagForToday",
+		},
+	}
+
+	if got := tape.Play(); !reflect.DeepEqual(expected, got) {
+		t.Errorf("invalid call sequence. Got %#v, want %#v", got, expected)
+	}
+	tape.Rewind()
+	stdout.Reset()
+	stderr.Reset()
+}
diff --git a/cmd/mgmt/device/impl/associate_impl.go b/cmd/mgmt/device/impl/associate_impl.go
new file mode 100644
index 0000000..1c82cad
--- /dev/null
+++ b/cmd/mgmt/device/impl/associate_impl.go
@@ -0,0 +1,90 @@
+package impl
+
+import (
+	"fmt"
+	"time"
+
+	"v.io/v23/context"
+	"v.io/v23/services/mgmt/device"
+	"v.io/x/lib/cmdline"
+)
+
+var cmdList = &cmdline.Command{
+	Run:      runList,
+	Name:     "list",
+	Short:    "Lists the account associations.",
+	Long:     "Lists all account associations.",
+	ArgsName: "<devicemanager>.",
+	ArgsLong: `
+<devicemanager> is the name of the device manager to connect to.`,
+}
+
+func runList(cmd *cmdline.Command, args []string) error {
+	if expected, got := 1, len(args); expected != got {
+		return cmd.UsageErrorf("list: incorrect number of arguments, expected %d, got %d", expected, got)
+	}
+
+	ctx, cancel := context.WithTimeout(gctx, time.Minute)
+	defer cancel()
+	assocs, err := device.DeviceClient(args[0]).ListAssociations(ctx)
+	if err != nil {
+		return fmt.Errorf("ListAssociations failed: %v", err)
+	}
+
+	for _, a := range assocs {
+		fmt.Fprintf(cmd.Stdout(), "%s %s\n", a.IdentityName, a.AccountName)
+	}
+	return nil
+}
+
+var cmdAdd = &cmdline.Command{
+	Run:      runAdd,
+	Name:     "add",
+	Short:    "Add the listed blessings with the specified system account.",
+	Long:     "Add the listed blessings with the specified system account.",
+	ArgsName: "<devicemanager> <systemName> <blessing>...",
+	ArgsLong: `
+<devicemanager> is the name of the device manager to connect to.
+<systemName> is the name of an account holder on the local system.
+<blessing>.. are the blessings to associate systemAccount with.`,
+}
+
+func runAdd(cmd *cmdline.Command, args []string) error {
+	if expected, got := 3, len(args); got < expected {
+		return cmd.UsageErrorf("add: incorrect number of arguments, expected at least %d, got %d", expected, got)
+	}
+	ctx, cancel := context.WithTimeout(gctx, time.Minute)
+	defer cancel()
+	return device.DeviceClient(args[0]).AssociateAccount(ctx, args[2:], args[1])
+}
+
+var cmdRemove = &cmdline.Command{
+	Run:      runRemove,
+	Name:     "remove",
+	Short:    "Removes system accounts associated with the listed blessings.",
+	Long:     "Removes system accounts associated with the listed blessings.",
+	ArgsName: "<devicemanager>  <blessing>...",
+	ArgsLong: `
+<devicemanager> is the name of the device manager to connect to.
+<blessing>... is a list of blessings.`,
+}
+
+func runRemove(cmd *cmdline.Command, args []string) error {
+	if expected, got := 2, len(args); got < expected {
+		return cmd.UsageErrorf("remove: incorrect number of arguments, expected at least %d, got %d", expected, got)
+	}
+	ctx, cancel := context.WithTimeout(gctx, time.Minute)
+	defer cancel()
+	return device.DeviceClient(args[0]).AssociateAccount(ctx, args[1:], "")
+}
+
+func associateRoot() *cmdline.Command {
+	return &cmdline.Command{
+		Name:  "associate",
+		Short: "Tool for creating associations between Vanadium blessings and a system account",
+		Long: `
+The associate tool facilitates managing blessing to system account associations.
+`,
+		Children: []*cmdline.Command{cmdList, cmdAdd, cmdRemove},
+	}
+}
diff --git a/cmd/mgmt/device/impl/devicemanager_mock_test.go b/cmd/mgmt/device/impl/devicemanager_mock_test.go
new file mode 100644
index 0000000..cebf014
--- /dev/null
+++ b/cmd/mgmt/device/impl/devicemanager_mock_test.go
@@ -0,0 +1,306 @@
+package impl_test
+
+import (
+	"fmt"
+	"io/ioutil"
+	"log"
+	"os"
+	"path/filepath"
+	"testing"
+
+	"v.io/v23"
+	"v.io/v23/context"
+	"v.io/v23/ipc"
+	"v.io/v23/naming"
+	"v.io/v23/security"
+	"v.io/v23/services/mgmt/application"
+	"v.io/v23/services/mgmt/binary"
+	"v.io/v23/services/mgmt/device"
+	"v.io/v23/services/mgmt/repository"
+	"v.io/v23/services/security/access"
+	"v.io/x/lib/vlog"
+
+	binlib "v.io/x/ref/services/mgmt/lib/binary"
+	pkglib "v.io/x/ref/services/mgmt/lib/packages"
+)
+
+type mockDeviceInvoker struct {
+	tape *Tape
+	t    *testing.T
+}
+
+// Mock ListAssociations
+type ListAssociationResponse struct {
+	na  []device.Association
+	err error
+}
+
+func (mni *mockDeviceInvoker) ListAssociations(ipc.ServerCall) (associations []device.Association, err error) {
+	vlog.VI(2).Infof("ListAssociations() was called")
+
+	ir := mni.tape.Record("ListAssociations")
+	r := ir.(ListAssociationResponse)
+	return r.na, r.err
+}
+
+// Mock AssociateAccount
+type AddAssociationStimulus struct {
+	fun           string
+	identityNames []string
+	accountName   string
+}
+
+// simpleCore implements the core of all mock methods that take
+// arguments and return error.
+func (mni *mockDeviceInvoker) simpleCore(callRecord interface{}, name string) error {
+	ri := mni.tape.Record(callRecord)
+	switch r := ri.(type) {
+	case nil:
+		return nil
+	case error:
+		return r
+	}
+	log.Fatalf("%s (mock) response %v is of bad type", name, ri)
+	return nil
+}
+
+func (mni *mockDeviceInvoker) AssociateAccount(call ipc.ServerCall, identityNames []string, accountName string) error {
+	return mni.simpleCore(AddAssociationStimulus{"AssociateAccount", identityNames, accountName}, "AssociateAccount")
+}
+
+func (mni *mockDeviceInvoker) Claim(call ipc.ServerCall, pairingToken string) error {
+	return mni.simpleCore("Claim", "Claim")
+}
+
+func (*mockDeviceInvoker) Describe(ipc.ServerCall) (device.Description, error) {
+	return device.Description{}, nil
+}
+
+func (*mockDeviceInvoker) IsRunnable(_ ipc.ServerCall, description binary.Description) (bool, error) {
+	return false, nil
+}
+
+func (*mockDeviceInvoker) Reset(call ipc.ServerCall, deadline uint64) error { return nil }
+
+// Mock Install
+type InstallStimulus struct {
+	fun      string
+	appName  string
+	config   device.Config
+	packages application.Packages
+	envelope application.Envelope
+	// files holds a map from file  or package name to file or package size.
+	// The app binary  has the key "binary". Each of  the packages will have
+	// the key "package/<package name>". The override packages will have the
+	// key "overridepackage/<package name>".
+	files map[string]int64
+}
+
+type InstallResponse struct {
+	appId string
+	err   error
+}
+
+const (
+	// If provided with this app name, the mock device manager skips trying
+	// to fetch the envelope from the name.
+	appNameNoFetch = "skip-envelope-fetching"
+	// If provided with a fetcheable app name, the mock device manager sets
+	// the app name in the stimulus to this constant.
+	appNameAfterFetch = "envelope-fetched"
+	// The mock device manager sets the binary name in the envelope in the
+	// stimulus to this constant.
+	binaryNameAfterFetch = "binary-fetched"
+)
+
+func packageSize(pkgPath string) int64 {
+	info, err := os.Stat(pkgPath)
+	if err != nil {
+		return -1
+	}
+	if info.IsDir() {
+		infos, err := ioutil.ReadDir(pkgPath)
+		if err != nil {
+			return -1
+		}
+		var size int64
+		for _, i := range infos {
+			size += i.Size()
+		}
+		return size
+	} else {
+		return info.Size()
+	}
+}
+
+func fetchPackageSize(ctx *context.T, pkgVON string) (int64, error) {
+	dir, err := ioutil.TempDir("", "package")
+	if err != nil {
+		return 0, fmt.Errorf("failed to create temp package dir: %v", err)
+	}
+	defer os.RemoveAll(dir)
+	tmpFile := filepath.Join(dir, "downloaded")
+	if err := binlib.DownloadToFile(ctx, pkgVON, tmpFile); err != nil {
+		return 0, fmt.Errorf("DownloadToFile failed: %v", err)
+	}
+	dst := filepath.Join(dir, "install")
+	if err := pkglib.Install(tmpFile, dst); err != nil {
+		return 0, fmt.Errorf("packages.Install failed: %v", err)
+	}
+	return packageSize(dst), nil
+}
+
+func (mni *mockDeviceInvoker) Install(call ipc.ServerCall, appName string, config device.Config, packages application.Packages) (string, error) {
+	is := InstallStimulus{"Install", appName, config, packages, application.Envelope{}, nil}
+	if appName != appNameNoFetch {
+		// Fetch the envelope and record it in the stimulus.
+		envelope, err := repository.ApplicationClient(appName).Match(call.Context(), []string{"test"})
+		if err != nil {
+			return "", err
+		}
+		binaryName := envelope.Binary.File
+		envelope.Binary.File = binaryNameAfterFetch
+		is.appName = appNameAfterFetch
+		is.files = make(map[string]int64)
+		// Fetch the binary and record its size in the stimulus.
+		data, mediaInfo, err := binlib.Download(call.Context(), binaryName)
+		if err != nil {
+			return "", err
+		}
+		is.files["binary"] = int64(len(data))
+		if mediaInfo.Type != "application/octet-stream" {
+			return "", fmt.Errorf("unexpected media type: %v", mediaInfo)
+		}
+		// Iterate over the packages, download them, compute the size of
+		// the file(s) that make up each package, and record that in the
+		// stimulus.
+		for pkgLocalName, pkgVON := range envelope.Packages {
+			size, err := fetchPackageSize(call.Context(), pkgVON.File)
+			if err != nil {
+				return "", err
+			}
+			is.files[naming.Join("packages", pkgLocalName)] = size
+		}
+		envelope.Packages = nil
+		for pkgLocalName, pkg := range packages {
+			size, err := fetchPackageSize(call.Context(), pkg.File)
+			if err != nil {
+				return "", err
+			}
+			is.files[naming.Join("overridepackages", pkgLocalName)] = size
+		}
+		is.packages = nil
+		is.envelope = envelope
+	}
+	r := mni.tape.Record(is).(InstallResponse)
+	return r.appId, r.err
+}
+
+func (*mockDeviceInvoker) Refresh(ipc.ServerCall) error { return nil }
+
+func (*mockDeviceInvoker) Restart(ipc.ServerCall) error { return nil }
+
+func (mni *mockDeviceInvoker) Resume(_ ipc.ServerCall) error {
+	return mni.simpleCore("Resume", "Resume")
+}
+
+func (i *mockDeviceInvoker) Revert(call ipc.ServerCall) error { return nil }
+
+type StartResponse struct {
+	appIds []string
+	err    error
+}
+
+func (mni *mockDeviceInvoker) Start(ipc.ServerCall) ([]string, error) {
+	ir := mni.tape.Record("Start")
+	r := ir.(StartResponse)
+	return r.appIds, r.err
+}
+
+type StopStimulus struct {
+	fun       string
+	timeDelta uint32
+}
+
+func (mni *mockDeviceInvoker) Stop(_ ipc.ServerCall, timeDelta uint32) error {
+	return mni.simpleCore(StopStimulus{"Stop", timeDelta}, "Stop")
+}
+
+func (mni *mockDeviceInvoker) Suspend(_ ipc.ServerCall) error {
+	return mni.simpleCore("Suspend", "Suspend")
+}
+
+func (*mockDeviceInvoker) Uninstall(ipc.ServerCall) error { return nil }
+
+func (i *mockDeviceInvoker) Update(ipc.ServerCall) error { return nil }
+
+func (*mockDeviceInvoker) UpdateTo(ipc.ServerCall, string) error { return nil }
+
+// Mock ACL getting and setting
+type GetACLResponse struct {
+	acl  access.TaggedACLMap
+	etag string
+	err  error
+}
+
+type SetACLStimulus struct {
+	fun  string
+	acl  access.TaggedACLMap
+	etag string
+}
+
+func (mni *mockDeviceInvoker) SetACL(_ ipc.ServerCall, acl access.TaggedACLMap, etag string) error {
+	return mni.simpleCore(SetACLStimulus{"SetACL", acl, etag}, "SetACL")
+}
+
+func (mni *mockDeviceInvoker) GetACL(ipc.ServerCall) (access.TaggedACLMap, string, error) {
+	ir := mni.tape.Record("GetACL")
+	r := ir.(GetACLResponse)
+	return r.acl, r.etag, r.err
+}
+
+func (mni *mockDeviceInvoker) Debug(ipc.ServerCall) (string, error) {
+	ir := mni.tape.Record("Debug")
+	r := ir.(string)
+	return r, nil
+}
+
+type dispatcher struct {
+	tape *Tape
+	t    *testing.T
+}
+
+func NewDispatcher(t *testing.T, tape *Tape) ipc.Dispatcher {
+	return &dispatcher{tape: tape, t: t}
+}
+
+func (d *dispatcher) Lookup(suffix string) (interface{}, security.Authorizer, error) {
+	return &mockDeviceInvoker{tape: d.tape, t: d.t}, nil, nil
+}
+
+func startServer(t *testing.T, ctx *context.T, tape *Tape) (ipc.Server, naming.Endpoint, error) {
+	dispatcher := NewDispatcher(t, tape)
+	server, err := v23.NewServer(ctx)
+	if err != nil {
+		t.Errorf("NewServer failed: %v", err)
+		return nil, nil, err
+	}
+	endpoints, err := server.Listen(v23.GetListenSpec(ctx))
+	if err != nil {
+		t.Errorf("Listen failed: %v", err)
+		stopServer(t, server)
+		return nil, nil, err
+	}
+	if err := server.ServeDispatcher("", dispatcher); err != nil {
+		t.Errorf("ServeDispatcher failed: %v", err)
+		stopServer(t, server)
+		return nil, nil, err
+	}
+	return server, endpoints[0], nil
+}
+
+func stopServer(t *testing.T, server ipc.Server) {
+	if err := server.Stop(); err != nil {
+		t.Errorf("server.Stop failed: %v", err)
+	}
+}
diff --git a/cmd/mgmt/device/impl/impl.go b/cmd/mgmt/device/impl/impl.go
new file mode 100644
index 0000000..fa8349d
--- /dev/null
+++ b/cmd/mgmt/device/impl/impl.go
@@ -0,0 +1,287 @@
+package impl
+
+import (
+	"encoding/base64"
+	"encoding/json"
+	"fmt"
+
+	"v.io/v23"
+	"v.io/v23/ipc"
+	"v.io/v23/naming"
+	"v.io/v23/options"
+	"v.io/v23/security"
+	"v.io/v23/services/mgmt/application"
+	"v.io/v23/services/mgmt/device"
+	"v.io/x/lib/cmdline"
+)
+
+type configFlag device.Config
+
+func (c *configFlag) String() string {
+	jsonConfig, _ := json.Marshal(c)
+	return string(jsonConfig)
+}
+func (c *configFlag) Set(s string) error {
+	if err := json.Unmarshal([]byte(s), c); err != nil {
+		return fmt.Errorf("Unmarshal(%v) failed: %v", s, err)
+	}
+	return nil
+}
+
+var configOverride configFlag = configFlag{}
+
+type packagesFlag application.Packages
+
+func (c *packagesFlag) String() string {
+	jsonPackages, _ := json.Marshal(c)
+	return string(jsonPackages)
+}
+func (c *packagesFlag) Set(s string) error {
+	if err := json.Unmarshal([]byte(s), c); err != nil {
+		return fmt.Errorf("Unmarshal(%v) failed: %v", s, err)
+	}
+	return nil
+}
+
+var packagesOverride packagesFlag = packagesFlag{}
+
+func init() {
+	cmdInstall.Flags.Var(&configOverride, "config", "JSON-encoded device.Config object, of the form: '{\"flag1\":\"value1\",\"flag2\":\"value2\"}'")
+	cmdInstall.Flags.Var(&packagesOverride, "packages", "JSON-encoded application.Packages object, of the form: '{\"pkg1\":{\"File\":\"object name 1\"},\"pkg2\":{\"File\":\"object name 2\"}}'")
+}
+
+var cmdInstall = &cmdline.Command{
+	Run:      runInstall,
+	Name:     "install",
+	Short:    "Install the given application.",
+	Long:     "Install the given application.",
+	ArgsName: "<device> <application>",
+	ArgsLong: `
+<device> is the veyron object name of the device manager's app service.
+
+<application> is the veyron object name of the application.
+`,
+}
+
+func runInstall(cmd *cmdline.Command, args []string) error {
+	if expected, got := 2, len(args); expected != got {
+		return cmd.UsageErrorf("install: incorrect number of arguments, expected %d, got %d", expected, got)
+	}
+	deviceName, appName := args[0], args[1]
+	appID, err := device.ApplicationClient(deviceName).Install(gctx, appName, device.Config(configOverride), application.Packages(packagesOverride))
+	// Reset the value for any future invocations of "install" or
+	// "install-local" (we run more than one command per process in unit
+	// tests).
+	configOverride = configFlag{}
+	packagesOverride = packagesFlag{}
+	if err != nil {
+		return fmt.Errorf("Install failed: %v", err)
+	}
+	fmt.Fprintf(cmd.Stdout(), "Successfully installed: %q\n", naming.Join(deviceName, appID))
+	return nil
+}
+
+var cmdUninstall = &cmdline.Command{
+	Run:      runUninstall,
+	Name:     "uninstall",
+	Short:    "Uninstall the given application installation.",
+	Long:     "Uninstall the given application installation.",
+	ArgsName: "<installation>",
+	ArgsLong: `
+<installation> is the veyron object name of the application installation to
+uninstall.
+`,
+}
+
+func runUninstall(cmd *cmdline.Command, args []string) error {
+	if expected, got := 1, len(args); expected != got {
+		return cmd.UsageErrorf("uninstall: incorrect number of arguments, expected %d, got %d", expected, got)
+	}
+	installName := args[0]
+	if err := device.ApplicationClient(installName).Uninstall(gctx); err != nil {
+		return fmt.Errorf("Uninstall failed: %v", err)
+	}
+	fmt.Fprintf(cmd.Stdout(), "Successfully uninstalled: %q\n", installName)
+	return nil
+}
+
+var cmdStart = &cmdline.Command{
+	Run:      runStart,
+	Name:     "start",
+	Short:    "Start an instance of the given application.",
+	Long:     "Start an instance of the given application.",
+	ArgsName: "<application installation> <grant extension>",
+	ArgsLong: `
+<application installation> is the veyron object name of the
+application installation from which to start an instance.
+
+<grant extension> is used to extend the default blessing of the
+current principal when blessing the app instance.`,
+}
+
+type granter struct {
+	ipc.CallOpt
+	p         security.Principal
+	extension string
+}
+
+func (g *granter) Grant(other security.Blessings) (security.Blessings, error) {
+	return g.p.Bless(other.PublicKey(), g.p.BlessingStore().Default(), g.extension, security.UnconstrainedUse())
+}
+
+func runStart(cmd *cmdline.Command, args []string) error {
+	if expected, got := 2, len(args); expected != got {
+		return cmd.UsageErrorf("start: incorrect number of arguments, expected %d, got %d", expected, got)
+	}
+	appInstallation, grant := args[0], args[1]
+	principal := v23.GetPrincipal(gctx)
+	appInstanceIDs, err := device.ApplicationClient(appInstallation).Start(gctx, &granter{p: principal, extension: grant})
+	if err != nil {
+		return fmt.Errorf("Start failed: %v", err)
+	}
+	for _, id := range appInstanceIDs {
+		fmt.Fprintf(cmd.Stdout(), "Successfully started: %q\n", naming.Join(appInstallation, id))
+	}
+	return nil
+}
+
+var cmdClaim = &cmdline.Command{
+	Run:      runClaim,
+	Name:     "claim",
+	Short:    "Claim the device.",
+	Long:     "Claim the device.",
+	ArgsName: "<device> <grant extension> <pairing token> <device publickey>",
+	ArgsLong: `
+<device> is the veyron object name of the device manager's device service.
+
+<grant extension> is used to extend the default blessing of the
+current principal when blessing the app instance.
+
+<pairing token> is a token that the device manager expects to be replayed
+during a claim operation on the device.
+
+<device publickey> is the marshalled public key of the device manager we
+are claiming.`,
+}
+
+func runClaim(cmd *cmdline.Command, args []string) error {
+	if expected, max, got := 2, 4, len(args); expected > got || got > max {
+		return cmd.UsageErrorf("claim: incorrect number of arguments, expected atleast %d (max: %d), got %d", expected, max, got)
+	}
+	deviceName, grant := args[0], args[1]
+	var pairingToken string
+	if len(args) > 2 {
+		pairingToken = args[2]
+	}
+	var serverKeyOpts ipc.CallOpt
+	if len(args) > 3 {
+		marshalledPublicKey, err := base64.URLEncoding.DecodeString(args[3])
+		if err != nil {
+			return fmt.Errorf("Failed to base64 decode publickey: %v", err)
+		}
+		if deviceKey, err := security.UnmarshalPublicKey(marshalledPublicKey); err != nil {
+			return fmt.Errorf("Failed to unmarshal device public key:%v", err)
+		} else {
+			serverKeyOpts = options.ServerPublicKey{deviceKey}
+		}
+	}
+	// Skip server resolve authorization since an unclaimed device might have
+	// roots that will not be recognized by the claimer.
+	if err := device.ClaimableClient(deviceName).Claim(gctx, pairingToken, &granter{p: v23.GetPrincipal(gctx), extension: grant}, serverKeyOpts, options.SkipResolveAuthorization{}); err != nil {
+		return err
+	}
+	fmt.Fprintln(cmd.Stdout(), "Successfully claimed.")
+	return nil
+}
+
+var cmdDescribe = &cmdline.Command{
+	Run:      runDescribe,
+	Name:     "describe",
+	Short:    "Describe the device.",
+	Long:     "Describe the device.",
+	ArgsName: "<device>",
+	ArgsLong: `
+<device> is the veyron object name of the device manager's device service.`,
+}
+
+func runDescribe(cmd *cmdline.Command, args []string) error {
+	if expected, got := 1, len(args); expected != got {
+		return cmd.UsageErrorf("describe: incorrect number of arguments, expected %d, got %d", expected, got)
+	}
+	deviceName := args[0]
+	if description, err := device.DeviceClient(deviceName).Describe(gctx); err != nil {
+		return fmt.Errorf("Describe failed: %v", err)
+	} else {
+		fmt.Fprintf(cmd.Stdout(), "%+v\n", description)
+	}
+	return nil
+}
+
+var cmdUpdate = &cmdline.Command{
+	Run:      runUpdate,
+	Name:     "update",
+	Short:    "Update the device manager or application",
+	Long:     "Update the device manager or application",
+	ArgsName: "<object>",
+	ArgsLong: `
+<object> is the veyron object name of the device manager or application
+installation to update.`,
+}
+
+func runUpdate(cmd *cmdline.Command, args []string) error {
+	if expected, got := 1, len(args); expected != got {
+		return cmd.UsageErrorf("update: incorrect number of arguments, expected %d, got %d", expected, got)
+	}
+	deviceName := args[0]
+	if err := device.ApplicationClient(deviceName).Update(gctx); err != nil {
+		return err
+	}
+	fmt.Fprintln(cmd.Stdout(), "Update successful.")
+	return nil
+}
+
+var cmdRevert = &cmdline.Command{
+	Run:      runRevert,
+	Name:     "revert",
+	Short:    "Revert the device manager or application",
+	Long:     "Revert the device manager or application to its previous version",
+	ArgsName: "<object>",
+	ArgsLong: `
+<object> is the veyron object name of the device manager or application
+installation to revert.`,
+}
+
+func runRevert(cmd *cmdline.Command, args []string) error {
+	if expected, got := 1, len(args); expected != got {
+		return cmd.UsageErrorf("revert: incorrect number of arguments, expected %d, got %d", expected, got)
+	}
+	deviceName := args[0]
+	if err := device.ApplicationClient(deviceName).Revert(gctx); err != nil {
+		return err
+	}
+	fmt.Fprintln(cmd.Stdout(), "Revert successful.")
+	return nil
+}
+
+var cmdDebug = &cmdline.Command{
+	Run:      runDebug,
+	Name:     "debug",
+	Short:    "Debug the device.",
+	Long:     "Debug the device.",
+	ArgsName: "<device>",
+	ArgsLong: `
+<device> is the veyron object name of an app installation or instance.`,
+}
+
+func runDebug(cmd *cmdline.Command, args []string) error {
+	if expected, got := 1, len(args); expected != got {
+		return cmd.UsageErrorf("debug: incorrect number of arguments, expected %d, got %d", expected, got)
+	}
+	deviceName := args[0]
+	if description, err := device.DeviceClient(deviceName).Debug(gctx); err != nil {
+		return fmt.Errorf("Debug failed: %v", err)
+	} else {
+		fmt.Fprintf(cmd.Stdout(), "%v\n", description)
+	}
+	return nil
+}
diff --git a/cmd/mgmt/device/impl/impl_test.go b/cmd/mgmt/device/impl/impl_test.go
new file mode 100644
index 0000000..247c40e
--- /dev/null
+++ b/cmd/mgmt/device/impl/impl_test.go
@@ -0,0 +1,487 @@
+package impl_test
+
+import (
+	"bytes"
+	"encoding/base64"
+	"encoding/json"
+	"fmt"
+	"reflect"
+	"strings"
+	"testing"
+
+	"v.io/v23"
+	"v.io/v23/naming"
+	"v.io/v23/services/mgmt/application"
+	"v.io/v23/services/mgmt/device"
+	"v.io/v23/verror"
+
+	"v.io/x/ref/cmd/mgmt/device/impl"
+	"v.io/x/ref/security"
+)
+
+func TestListCommand(t *testing.T) {
+	shutdown := initTest()
+	defer shutdown()
+
+	tape := NewTape()
+	server, endpoint, err := startServer(t, gctx, tape)
+	if err != nil {
+		return
+	}
+	defer stopServer(t, server)
+
+	// Setup the command-line.
+	cmd := impl.Root()
+	var stdout, stderr bytes.Buffer
+	cmd.Init(nil, &stdout, &stderr)
+	deviceName := naming.JoinAddressName(endpoint.String(), "")
+
+	// Test the 'list' command.
+	tape.SetResponses([]interface{}{ListAssociationResponse{
+		na: []device.Association{
+			{
+				"root/self",
+				"alice_self_account",
+			},
+			{
+				"root/other",
+				"alice_other_account",
+			},
+		},
+		err: nil,
+	}})
+
+	if err := cmd.Execute([]string{"associate", "list", deviceName}); err != nil {
+		t.Fatalf("%v", err)
+	}
+	if expected, got := "root/self alice_self_account\nroot/other alice_other_account", strings.TrimSpace(stdout.String()); got != expected {
+		t.Fatalf("Unexpected output from list. Got %q, expected %q", got, expected)
+	}
+	if got, expected := tape.Play(), []interface{}{"ListAssociations"}; !reflect.DeepEqual(expected, got) {
+		t.Errorf("invalid call sequence. Got %v, want %v", got, expected)
+	}
+	tape.Rewind()
+	stdout.Reset()
+
+	// Test list with bad parameters.
+	if err := cmd.Execute([]string{"associate", "list", deviceName, "hello"}); err == nil {
+		t.Fatalf("wrongly failed to receive a non-nil error.")
+	}
+	if got, expected := len(tape.Play()), 0; got != expected {
+		t.Errorf("invalid call sequence. Got %v, want %v", got, expected)
+	}
+	tape.Rewind()
+	stdout.Reset()
+}
+
+func TestAddCommand(t *testing.T) {
+	shutdown := initTest()
+	defer shutdown()
+
+	tape := NewTape()
+	server, endpoint, err := startServer(t, gctx, tape)
+	if err != nil {
+		return
+	}
+	defer stopServer(t, server)
+
+	// Setup the command-line.
+	cmd := impl.Root()
+	var stdout, stderr bytes.Buffer
+	cmd.Init(nil, &stdout, &stderr)
+	deviceName := naming.JoinAddressName(endpoint.String(), "/myapp/1")
+
+	if err := cmd.Execute([]string{"add", "one"}); err == nil {
+		t.Fatalf("wrongly failed to receive a non-nil error.")
+	}
+	if got, expected := len(tape.Play()), 0; got != expected {
+		t.Errorf("invalid call sequence. Got %v, want %v", got, expected)
+	}
+	tape.Rewind()
+	stdout.Reset()
+
+	tape.SetResponses([]interface{}{nil})
+	if err := cmd.Execute([]string{"associate", "add", deviceName, "alice", "root/self"}); err != nil {
+		t.Fatalf("%v", err)
+	}
+	expected := []interface{}{
+		AddAssociationStimulus{"AssociateAccount", []string{"root/self"}, "alice"},
+	}
+	if got := tape.Play(); !reflect.DeepEqual(expected, got) {
+		t.Errorf("unexpected result. Got %v want %v", got, expected)
+	}
+	tape.Rewind()
+	stdout.Reset()
+
+	tape.SetResponses([]interface{}{nil})
+	if err := cmd.Execute([]string{"associate", "add", deviceName, "alice", "root/other", "root/self"}); err != nil {
+		t.Fatalf("%v", err)
+	}
+	expected = []interface{}{
+		AddAssociationStimulus{"AssociateAccount", []string{"root/other", "root/self"}, "alice"},
+	}
+	if got := tape.Play(); !reflect.DeepEqual(expected, got) {
+		t.Errorf("unexpected result. Got %v want %v", got, expected)
+	}
+	tape.Rewind()
+	stdout.Reset()
+}
+
+func TestRemoveCommand(t *testing.T) {
+	shutdown := initTest()
+	defer shutdown()
+
+	tape := NewTape()
+	server, endpoint, err := startServer(t, gctx, tape)
+	if err != nil {
+		return
+	}
+	defer stopServer(t, server)
+
+	// Setup the command-line.
+	cmd := impl.Root()
+	var stdout, stderr bytes.Buffer
+	cmd.Init(nil, &stdout, &stderr)
+	deviceName := naming.JoinAddressName(endpoint.String(), "")
+
+	if err := cmd.Execute([]string{"remove", "one"}); err == nil {
+		t.Fatalf("wrongly failed to receive a non-nil error.")
+	}
+	if got, expected := len(tape.Play()), 0; got != expected {
+		t.Errorf("invalid call sequence. Got %v, want %v", got, expected)
+	}
+	tape.Rewind()
+	stdout.Reset()
+
+	tape.SetResponses([]interface{}{nil})
+	if err := cmd.Execute([]string{"associate", "remove", deviceName, "root/self"}); err != nil {
+		t.Fatalf("%v", err)
+	}
+	expected := []interface{}{
+		AddAssociationStimulus{"AssociateAccount", []string{"root/self"}, ""},
+	}
+	if got := tape.Play(); !reflect.DeepEqual(expected, got) {
+		t.Errorf("unexpected result. Got %v want %v", got, expected)
+	}
+	tape.Rewind()
+	stdout.Reset()
+}
+
+func TestInstallCommand(t *testing.T) {
+	shutdown := initTest()
+	defer shutdown()
+
+	tape := NewTape()
+	server, endpoint, err := startServer(t, gctx, tape)
+	if err != nil {
+		return
+	}
+	defer stopServer(t, server)
+
+	// Setup the command-line.
+	cmd := impl.Root()
+	var stdout, stderr bytes.Buffer
+	cmd.Init(nil, &stdout, &stderr)
+	deviceName := naming.JoinAddressName(endpoint.String(), "")
+	appId := "myBestAppID"
+	cfg := device.Config{"someflag": "somevalue"}
+	pkg := application.Packages{"pkg": application.SignedFile{File: "somename"}}
+	for i, c := range []struct {
+		args         []string
+		config       device.Config
+		packages     application.Packages
+		shouldErr    bool
+		tapeResponse interface{}
+		expectedTape interface{}
+	}{
+		{
+			[]string{"blech"},
+			nil,
+			nil,
+			true,
+			nil,
+			nil,
+		},
+		{
+			[]string{"blech1", "blech2", "blech3", "blech4"},
+			nil,
+			nil,
+			true,
+			nil,
+			nil,
+		},
+		{
+			[]string{deviceName, appNameNoFetch, "not-valid-json"},
+			nil,
+			nil,
+			true,
+			nil,
+			nil,
+		},
+		{
+			[]string{deviceName, appNameNoFetch},
+			nil,
+			nil,
+			false,
+			InstallResponse{appId, nil},
+			InstallStimulus{"Install", appNameNoFetch, nil, nil, application.Envelope{}, nil},
+		},
+		{
+			[]string{deviceName, appNameNoFetch},
+			cfg,
+			pkg,
+			false,
+			InstallResponse{appId, nil},
+			InstallStimulus{"Install", appNameNoFetch, cfg, pkg, application.Envelope{}, nil},
+		},
+	} {
+		tape.SetResponses([]interface{}{c.tapeResponse})
+		if c.config != nil {
+			jsonConfig, err := json.Marshal(c.config)
+			if err != nil {
+				t.Fatalf("test case %d: Marshal(%v) failed: %v", i, c.config, err)
+			}
+			c.args = append([]string{fmt.Sprintf("--config=%s", string(jsonConfig))}, c.args...)
+		}
+		if c.packages != nil {
+			jsonPackages, err := json.Marshal(c.packages)
+			if err != nil {
+				t.Fatalf("test case %d: Marshal(%v) failed: %v", i, c.packages, err)
+			}
+			c.args = append([]string{fmt.Sprintf("--packages=%s", string(jsonPackages))}, c.args...)
+		}
+		c.args = append([]string{"install"}, c.args...)
+		err := cmd.Execute(c.args)
+		if c.shouldErr {
+			if err == nil {
+				t.Fatalf("test case %d: wrongly failed to receive a non-nil error.", i)
+			}
+			if got, expected := len(tape.Play()), 0; got != expected {
+				t.Errorf("test case %d: invalid call sequence. Got %v, want %v", got, expected)
+			}
+		} else {
+			if err != nil {
+				t.Fatalf("test case %d: %v", i, err)
+			}
+			if expected, got := fmt.Sprintf("Successfully installed: %q", naming.Join(deviceName, appId)), strings.TrimSpace(stdout.String()); got != expected {
+				t.Fatalf("test case %d: Unexpected output from Install. Got %q, expected %q", i, got, expected)
+			}
+			if got, expected := tape.Play(), []interface{}{c.expectedTape}; !reflect.DeepEqual(expected, got) {
+				t.Errorf("test case %d: invalid call sequence. Got %#v, want %#v", i, got, expected)
+			}
+		}
+		tape.Rewind()
+		stdout.Reset()
+	}
+}
+
+func TestClaimCommand(t *testing.T) {
+	shutdown := initTest()
+	defer shutdown()
+
+	tape := NewTape()
+	server, endpoint, err := startServer(t, gctx, tape)
+	if err != nil {
+		return
+	}
+	defer stopServer(t, server)
+
+	// Setup the command-line.
+	cmd := impl.Root()
+	var stdout, stderr bytes.Buffer
+	cmd.Init(nil, &stdout, &stderr)
+	deviceName := naming.JoinAddressName(endpoint.String(), "")
+	deviceKey, err := v23.GetPrincipal(gctx).PublicKey().MarshalBinary()
+	if err != nil {
+		t.Fatalf("Failed to marshal principal public key: %v", err)
+	}
+
+	// Confirm that we correctly enforce the number of arguments.
+	if err := cmd.Execute([]string{"claim", "nope"}); err == nil {
+		t.Fatalf("wrongly failed to receive a non-nil error.")
+	}
+	if expected, got := "ERROR: claim: incorrect number of arguments, expected atleast 2 (max: 4), got 1", strings.TrimSpace(stderr.String()); !strings.HasPrefix(got, expected) {
+		t.Fatalf("Unexpected output from claim. Got %q, expected prefix %q", got, expected)
+	}
+	stdout.Reset()
+	stderr.Reset()
+	tape.Rewind()
+
+	if err := cmd.Execute([]string{"claim", "nope", "nope", "nope", "nope", "nope"}); err == nil {
+		t.Fatalf("wrongly failed to receive a non-nil error.")
+	}
+	if expected, got := "ERROR: claim: incorrect number of arguments, expected atleast 2 (max: 4), got 5", strings.TrimSpace(stderr.String()); !strings.HasPrefix(got, expected) {
+		t.Fatalf("Unexpected output from claim. Got %q, expected prefix %q", got, expected)
+	}
+	stdout.Reset()
+	stderr.Reset()
+	tape.Rewind()
+
+	// Incorrect operation
+	var pairingToken string
+	var deviceKeyWrong []byte
+	if publicKey, _, err := security.NewPrincipalKey(); err != nil {
+		t.Fatalf("NewPrincipalKey failed: %v", err)
+	} else {
+		if deviceKeyWrong, err = publicKey.MarshalBinary(); err != nil {
+			t.Fatalf("Failed to marshal principal public key: %v", err)
+		}
+	}
+	if err := cmd.Execute([]string{"claim", deviceName, "grant", pairingToken, base64.URLEncoding.EncodeToString(deviceKeyWrong)}); !verror.Is(err, verror.ErrNotTrusted.ID) {
+		t.Fatalf("wrongly failed to receive correct error on claim with incorrect device key:%v id:%v", err, verror.ErrorID(err))
+	}
+	stdout.Reset()
+	stderr.Reset()
+	tape.Rewind()
+
+	// Correct operation.
+	tape.SetResponses([]interface{}{
+		nil,
+	})
+	if err := cmd.Execute([]string{"claim", deviceName, "grant", pairingToken, base64.URLEncoding.EncodeToString(deviceKey)}); err != nil {
+		t.Fatalf("Claim(%s, %s, %s) failed: %v", deviceName, "grant", pairingToken, err)
+	}
+	if got, expected := len(tape.Play()), 1; got != expected {
+		t.Errorf("invalid call sequence. Got %v, want %v", got, expected)
+	}
+	if expected, got := "Successfully claimed.", strings.TrimSpace(stdout.String()); !strings.HasPrefix(got, expected) {
+		t.Fatalf("Unexpected output from claim. Got %q, expected prefix %q", got, expected)
+	}
+	expected := []interface{}{
+		"Claim",
+	}
+	if got := tape.Play(); !reflect.DeepEqual(expected, got) {
+		t.Errorf("unexpected result. Got %v want %v", got, expected)
+	}
+	tape.Rewind()
+	stdout.Reset()
+	stderr.Reset()
+
+	// Error operation.
+	tape.SetResponses([]interface{}{
+		verror.New(errOops, nil),
+	})
+	if err := cmd.Execute([]string{"claim", deviceName, "grant", pairingToken}); err == nil {
+		t.Fatalf("claim() failed to detect error", err)
+	}
+	expected = []interface{}{
+		"Claim",
+	}
+	if got := tape.Play(); !reflect.DeepEqual(expected, got) {
+		t.Errorf("unexpected result. Got %v want %v", got, expected)
+	}
+	tape.Rewind()
+	stdout.Reset()
+	stderr.Reset()
+}
+
+func TestStartCommand(t *testing.T) {
+	shutdown := initTest()
+	defer shutdown()
+
+	tape := NewTape()
+	server, endpoint, err := startServer(t, gctx, tape)
+	if err != nil {
+		return
+	}
+	defer stopServer(t, server)
+
+	// Setup the command-line.
+	cmd := impl.Root()
+	var stdout, stderr bytes.Buffer
+	cmd.Init(nil, &stdout, &stderr)
+	appName := naming.JoinAddressName(endpoint.String(), "")
+
+	// Confirm that we correctly enforce the number of arguments.
+	if err := cmd.Execute([]string{"start", "nope"}); err == nil {
+		t.Fatalf("wrongly failed to receive a non-nil error.")
+	}
+	if expected, got := "ERROR: start: incorrect number of arguments, expected 2, got 1", strings.TrimSpace(stderr.String()); !strings.HasPrefix(got, expected) {
+		t.Fatalf("Unexpected output from start. Got %q, expected prefix %q", got, expected)
+	}
+	stdout.Reset()
+	stderr.Reset()
+	tape.Rewind()
+
+	if err := cmd.Execute([]string{"start", "nope", "nope", "nope"}); err == nil {
+		t.Fatalf("wrongly failed to receive a non-nil error.")
+	}
+	if expected, got := "ERROR: start: incorrect number of arguments, expected 2, got 3", strings.TrimSpace(stderr.String()); !strings.HasPrefix(got, expected) {
+		t.Fatalf("Unexpected output from start. Got %q, expected prefix %q", got, expected)
+	}
+	stdout.Reset()
+	stderr.Reset()
+	tape.Rewind()
+
+	// Correct operation.
+	tape.SetResponses([]interface{}{StartResponse{
+		appIds: []string{"app1", "app2"},
+		err:    nil,
+	},
+	})
+	if err := cmd.Execute([]string{"start", appName, "grant"}); err != nil {
+		t.Fatalf("Start(%s, %s) failed: %v", appName, "grant", err)
+	}
+
+	b := new(bytes.Buffer)
+	fmt.Fprintf(b, "Successfully started: %q\nSuccessfully started: %q", appName+"/app1", appName+"/app2")
+	if expected, got := b.String(), strings.TrimSpace(stdout.String()); !strings.HasPrefix(got, expected) {
+		t.Fatalf("Unexpected output from start. Got %q, expected prefix %q", got, expected)
+	}
+	expected := []interface{}{
+		"Start",
+	}
+	if got := tape.Play(); !reflect.DeepEqual(expected, got) {
+		t.Errorf("unexpected result. Got %v want %v", got, expected)
+	}
+	tape.Rewind()
+	stdout.Reset()
+	stderr.Reset()
+
+	// Error operation.
+	tape.SetResponses([]interface{}{StartResponse{
+		[]string{},
+		verror.New(errOops, nil),
+	},
+	})
+	if err := cmd.Execute([]string{"start", appName, "grant"}); err == nil {
+		t.Fatalf("start failed to detect error")
+	}
+	expected = []interface{}{
+		"Start",
+	}
+	if got := tape.Play(); !reflect.DeepEqual(expected, got) {
+		t.Errorf("unexpected result. Got %v want %v", got, expected)
+	}
+	tape.Rewind()
+	stdout.Reset()
+	stderr.Reset()
+}
+
+func TestDebugCommand(t *testing.T) {
+	shutdown := initTest()
+	defer shutdown()
+	tape := NewTape()
+	server, endpoint, err := startServer(t, gctx, tape)
+	if err != nil {
+		return
+	}
+	defer stopServer(t, server)
+	// Setup the command-line.
+	cmd := impl.Root()
+	var stdout, stderr bytes.Buffer
+	cmd.Init(nil, &stdout, &stderr)
+	appName := naming.JoinAddressName(endpoint.String(), "")
+
+	debugMessage := "the secrets of the universe, revealed"
+	tape.SetResponses([]interface{}{debugMessage})
+	if err := cmd.Execute([]string{"debug", appName}); err != nil {
+		t.Fatalf("%v", err)
+	}
+	if expected, got := debugMessage, strings.TrimSpace(stdout.String()); got != expected {
+		t.Fatalf("Unexpected output from debug. Got %q, expected %q", got, expected)
+	}
+	if got, expected := tape.Play(), []interface{}{"Debug"}; !reflect.DeepEqual(expected, got) {
+		t.Errorf("invalid call sequence. Got %v, want %v", got, expected)
+	}
+}
diff --git a/cmd/mgmt/device/impl/instance_impl.go b/cmd/mgmt/device/impl/instance_impl.go
new file mode 100644
index 0000000..d0a5783
--- /dev/null
+++ b/cmd/mgmt/device/impl/instance_impl.go
@@ -0,0 +1,79 @@
+package impl
+
+// Commands to modify instance.
+
+import (
+	"fmt"
+
+	"v.io/v23/services/mgmt/device"
+	"v.io/x/lib/cmdline"
+)
+
+var cmdStop = &cmdline.Command{
+	Run:      runStop,
+	Name:     "stop",
+	Short:    "Stop the given application instance.",
+	Long:     "Stop the given application instance.",
+	ArgsName: "<app instance>",
+	ArgsLong: `
+<app instance> is the veyron object name of the application instance to stop.`,
+}
+
+func runStop(cmd *cmdline.Command, args []string) error {
+	if expected, got := 1, len(args); expected != got {
+		return cmd.UsageErrorf("stop: incorrect number of arguments, expected %d, got %d", expected, got)
+	}
+	appName := args[0]
+
+	if err := device.ApplicationClient(appName).Stop(gctx, 5); err != nil {
+		return fmt.Errorf("Stop failed: %v", err)
+	}
+	fmt.Fprintf(cmd.Stdout(), "Stop succeeded\n")
+	return nil
+}
+
+var cmdSuspend = &cmdline.Command{
+	Run:      runSuspend,
+	Name:     "suspend",
+	Short:    "Suspend the given application instance.",
+	Long:     "Suspend the given application instance.",
+	ArgsName: "<app instance>",
+	ArgsLong: `
+<app instance> is the veyron object name of the application instance to suspend.`,
+}
+
+func runSuspend(cmd *cmdline.Command, args []string) error {
+	if expected, got := 1, len(args); expected != got {
+		return cmd.UsageErrorf("suspend: incorrect number of arguments, expected %d, got %d", expected, got)
+	}
+	appName := args[0]
+
+	if err := device.ApplicationClient(appName).Suspend(gctx); err != nil {
+		return fmt.Errorf("Suspend failed: %v", err)
+	}
+	fmt.Fprintf(cmd.Stdout(), "Suspend succeeded\n")
+	return nil
+}
+
+var cmdResume = &cmdline.Command{
+	Run:      runResume,
+	Name:     "resume",
+	Short:    "Resume the given application instance.",
+	Long:     "Resume the given application instance.",
+	ArgsName: "<app instance>",
+	ArgsLong: `
+<app instance> is the veyron object name of the application instance to resume.`,
+}
+
+func runResume(cmd *cmdline.Command, args []string) error {
+	if expected, got := 1, len(args); expected != got {
+		return cmd.UsageErrorf("resume: incorrect number of arguments, expected %d, got %d", expected, got)
+	}
+	appName := args[0]
+
+	if err := device.ApplicationClient(appName).Resume(gctx); err != nil {
+		return fmt.Errorf("Resume failed: %v", err)
+	}
+	fmt.Fprintf(cmd.Stdout(), "Resume succeeded\n")
+	return nil
+}
diff --git a/cmd/mgmt/device/impl/instance_impl_test.go b/cmd/mgmt/device/impl/instance_impl_test.go
new file mode 100644
index 0000000..8cdcbf9
--- /dev/null
+++ b/cmd/mgmt/device/impl/instance_impl_test.go
@@ -0,0 +1,167 @@
+package impl_test
+
+import (
+	"bytes"
+	"reflect"
+	"strings"
+	"testing"
+
+	"v.io/v23/naming"
+	"v.io/v23/verror"
+
+	"v.io/x/ref/cmd/mgmt/device/impl"
+)
+
+func TestStopCommand(t *testing.T) {
+	shutdown := initTest()
+	defer shutdown()
+
+	tape := NewTape()
+	server, endpoint, err := startServer(t, gctx, tape)
+	if err != nil {
+		return
+	}
+	defer stopServer(t, server)
+
+	// Setup the command-line.
+	cmd := impl.Root()
+	var stdout, stderr bytes.Buffer
+	cmd.Init(nil, &stdout, &stderr)
+	appName := naming.JoinAddressName(endpoint.String(), "")
+
+	// Confirm that we correctly enforce the number of arguments.
+	if err := cmd.Execute([]string{"stop"}); err == nil {
+		t.Fatalf("wrongly failed to receive a non-nil error.")
+	}
+	if expected, got := "ERROR: stop: incorrect number of arguments, expected 1, got 0", strings.TrimSpace(stderr.String()); !strings.HasPrefix(got, expected) {
+		t.Fatalf("Unexpected output from stop. Got %q, expected prefix %q", got, expected)
+	}
+	stdout.Reset()
+	stderr.Reset()
+	tape.Rewind()
+
+	if err := cmd.Execute([]string{"stop", "nope", "nope"}); err == nil {
+		t.Fatalf("wrongly failed to receive a non-nil error.")
+	}
+	if expected, got := "ERROR: stop: incorrect number of arguments, expected 1, got 2", strings.TrimSpace(stderr.String()); !strings.HasPrefix(got, expected) {
+		t.Fatalf("Unexpected output from stop. Got %q, expected prefix %q", got, expected)
+	}
+	stdout.Reset()
+	stderr.Reset()
+	tape.Rewind()
+
+	// Test the 'stop' command.
+	tape.SetResponses([]interface{}{
+		nil,
+	})
+
+	if err := cmd.Execute([]string{"stop", appName + "/appname"}); err != nil {
+		t.Fatalf("stop failed when it shouldn't: %v", err)
+	}
+	if expected, got := "Stop succeeded", strings.TrimSpace(stdout.String()); got != expected {
+		t.Fatalf("Unexpected output from list. Got %q, expected %q", got, expected)
+	}
+	expected := []interface{}{
+		StopStimulus{"Stop", 5},
+	}
+	if got := tape.Play(); !reflect.DeepEqual(expected, got) {
+		t.Errorf("invalid call sequence. Got %v, want %v", got, expected)
+	}
+	tape.Rewind()
+	stderr.Reset()
+	stdout.Reset()
+
+	// Test stop with bad parameters.
+	tape.SetResponses([]interface{}{
+		verror.New(errOops, nil),
+	})
+	if err := cmd.Execute([]string{"stop", appName + "/appname"}); err == nil {
+		t.Fatalf("wrongly didn't receive a non-nil error.")
+	}
+	// expected the same.
+	if got := tape.Play(); !reflect.DeepEqual(expected, got) {
+		t.Errorf("invalid call sequence. Got %v, want %v", got, expected)
+	}
+	tape.Rewind()
+	stderr.Reset()
+	stdout.Reset()
+}
+
+func testHelper(t *testing.T, lower, upper string) {
+	shutdown := initTest()
+	defer shutdown()
+
+	tape := NewTape()
+	server, endpoint, err := startServer(t, gctx, tape)
+	if err != nil {
+		return
+	}
+	defer stopServer(t, server)
+
+	// Setup the command-line.
+	cmd := impl.Root()
+	var stdout, stderr bytes.Buffer
+	cmd.Init(nil, &stdout, &stderr)
+	appName := naming.JoinAddressName(endpoint.String(), "")
+
+	// Confirm that we correctly enforce the number of arguments.
+	if err := cmd.Execute([]string{lower}); err == nil {
+		t.Fatalf("wrongly failed to receive a non-nil error.")
+	}
+	if expected, got := "ERROR: "+lower+": incorrect number of arguments, expected 1, got 0", strings.TrimSpace(stderr.String()); !strings.HasPrefix(got, expected) {
+		t.Fatalf("Unexpected output from %s. Got %q, expected prefix %q", lower, got, expected)
+	}
+	stdout.Reset()
+	stderr.Reset()
+	tape.Rewind()
+
+	if err := cmd.Execute([]string{lower, "nope", "nope"}); err == nil {
+		t.Fatalf("wrongly failed to receive a non-nil error.")
+	}
+	if expected, got := "ERROR: "+lower+": incorrect number of arguments, expected 1, got 2", strings.TrimSpace(stderr.String()); !strings.HasPrefix(got, expected) {
+		t.Fatalf("Unexpected output from %s. Got %q, expected prefix %q", lower, got, expected)
+	}
+	stdout.Reset()
+	stderr.Reset()
+	tape.Rewind()
+
+	// Correct operation.
+	tape.SetResponses([]interface{}{
+		nil,
+	})
+	if err := cmd.Execute([]string{lower, appName + "/appname"}); err != nil {
+		t.Fatalf("%s failed when it shouldn't: %v", lower, err)
+	}
+	if expected, got := upper+" succeeded", strings.TrimSpace(stdout.String()); got != expected {
+		t.Fatalf("Unexpected output from %s. Got %q, expected %q", lower, got, expected)
+	}
+	if expected, got := []interface{}{upper}, tape.Play(); !reflect.DeepEqual(expected, got) {
+		t.Errorf("invalid call sequence. Got %v, want %v", got, expected)
+	}
+	tape.Rewind()
+	stderr.Reset()
+	stdout.Reset()
+
+	// Test list with bad parameters.
+	tape.SetResponses([]interface{}{
+		verror.New(errOops, nil),
+	})
+	if err := cmd.Execute([]string{lower, appName + "/appname"}); err == nil {
+		t.Fatalf("wrongly didn't receive a non-nil error.")
+	}
+	// expected the same.
+	if expected, got := []interface{}{upper}, tape.Play(); !reflect.DeepEqual(expected, got) {
+		t.Errorf("invalid call sequence. Got %v, want %v", got, expected)
+	}
+	tape.Rewind()
+	stderr.Reset()
+	stdout.Reset()
+}
+
+func TestSuspendCommand(t *testing.T) {
+	testHelper(t, "suspend", "Suspend")
+}
+
+func TestResumeCommand(t *testing.T) {
+	testHelper(t, "resume", "Resume")
+}
diff --git a/cmd/mgmt/device/impl/local_install.go b/cmd/mgmt/device/impl/local_install.go
new file mode 100644
index 0000000..4054201
--- /dev/null
+++ b/cmd/mgmt/device/impl/local_install.go
@@ -0,0 +1,326 @@
+package impl
+
+import (
+	"crypto/md5"
+	"encoding/hex"
+	"fmt"
+	"io"
+	"io/ioutil"
+	"os"
+	"path/filepath"
+	"strings"
+
+	"v.io/v23"
+	"v.io/v23/context"
+	"v.io/v23/ipc"
+	"v.io/v23/naming"
+	"v.io/v23/security"
+	"v.io/v23/services/mgmt/application"
+	"v.io/v23/services/mgmt/binary"
+	"v.io/v23/services/mgmt/device"
+	"v.io/v23/services/mgmt/repository"
+	"v.io/v23/services/security/access"
+	"v.io/v23/uniqueid"
+	"v.io/x/lib/vlog"
+
+	"v.io/x/lib/cmdline"
+	pkglib "v.io/x/ref/services/mgmt/lib/packages"
+)
+
+var cmdInstallLocal = &cmdline.Command{
+	Run:      runInstallLocal,
+	Name:     "install-local",
+	Short:    "Install the given application from the local system.",
+	Long:     "Install the given application, specified using a local path.",
+	ArgsName: "<device> <title> [ENV=VAL ...] binary [--flag=val ...] [PACKAGES path ...]",
+	ArgsLong: `
+<device> is the veyron object name of the device manager's app service.
+
+<title> is the app title.
+
+This is followed by an arbitrary number of environment variable settings, the
+local path for the binary to install, and arbitrary flag settings and args.
+Optionally, this can be followed by 'PACKAGES' and a list of local files and
+directories to be installed as packages for the app`}
+
+func init() {
+	cmdInstallLocal.Flags.Var(&configOverride, "config", "JSON-encoded device.Config object, of the form: '{\"flag1\":\"value1\",\"flag2\":\"value2\"}'")
+	cmdInstallLocal.Flags.Var(&packagesOverride, "packages", "JSON-encoded application.Packages object, of the form: '{\"pkg1\":{\"File\":\"local file path1\"},\"pkg2\":{\"File\":\"local file path 2\"}}'")
+}
+
+type openAuthorizer struct{}
+
+func (openAuthorizer) Authorize(security.Call) error { return nil }
+
+type mapDispatcher map[string]interface{}
+
+func (d mapDispatcher) Lookup(suffix string) (interface{}, security.Authorizer, error) {
+	o, ok := d[suffix]
+	if !ok {
+		return nil, nil, fmt.Errorf("suffix %s not found", suffix)
+	}
+	// TODO(caprita): Do not open authorizer even for a short-lived server.
+	return o, &openAuthorizer{}, nil
+}
+
+type mapServer struct {
+	name       string
+	dispatcher mapDispatcher
+}
+
+func (ms *mapServer) serve(name string, object interface{}) (string, error) {
+	if _, ok := ms.dispatcher[name]; ok {
+		return "", fmt.Errorf("can't have more than one object with name %v", name)
+	}
+	ms.dispatcher[name] = object
+	return naming.Join(ms.name, name), nil
+}
+
+func createServer(ctx *context.T, stderr io.Writer) (*mapServer, func(), error) {
+	server, err := v23.NewServer(ctx)
+	if err != nil {
+		return nil, nil, err
+	}
+	spec := v23.GetListenSpec(ctx)
+	endpoints, err := server.Listen(spec)
+	if err != nil {
+		return nil, nil, err
+	}
+	var name string
+	if spec.Proxy != "" {
+		id, err := uniqueid.Random()
+		if err != nil {
+			return nil, nil, err
+		}
+		name = id.String()
+	}
+	dispatcher := make(mapDispatcher)
+	if err := server.ServeDispatcher(name, dispatcher); err != nil {
+		return nil, nil, err
+	}
+	vlog.VI(1).Infof("Server listening on %v (%v)", endpoints, name)
+	cleanup := func() {
+		if err := server.Stop(); err != nil {
+			fmt.Fprintf(stderr, "server.Stop failed: %v", err)
+		}
+	}
+	if name != "" {
+		// Send a name rooted in our namespace root rather than the
+		// relative name (in case the device manager uses a different
+		// namespace root).
+		//
+		// TODO(caprita): Avoid relying on a mounttable altogether, and
+		// instead pull out the proxied address and just send that.
+		nsRoots := v23.GetNamespace(ctx).Roots()
+		if len(nsRoots) > 0 {
+			name = naming.Join(nsRoots[0], name)
+		}
+	} else if len(endpoints) > 0 {
+		name = endpoints[0].Name()
+	} else {
+		return nil, nil, fmt.Errorf("no endpoints")
+	}
+	return &mapServer{name: name, dispatcher: dispatcher}, cleanup, nil
+}
+
+var errNotImplemented = fmt.Errorf("method not implemented")
+
+type binaryInvoker string
+
+func (binaryInvoker) Create(ipc.ServerCall, int32, repository.MediaInfo) error {
+	return errNotImplemented
+}
+
+func (binaryInvoker) Delete(ipc.ServerCall) error {
+	return errNotImplemented
+}
+
+func (i binaryInvoker) Download(ctx repository.BinaryDownloadContext, _ int32) error {
+	fileName := string(i)
+	file, err := os.Open(fileName)
+	if err != nil {
+		return err
+	}
+	defer file.Close()
+	bufferLength := 4096
+	buffer := make([]byte, bufferLength)
+	sender := ctx.SendStream()
+	for {
+		n, err := file.Read(buffer)
+		switch err {
+		case io.EOF:
+			return nil
+		case nil:
+			if err := sender.Send(buffer[:n]); err != nil {
+				return err
+			}
+		default:
+			return err
+		}
+	}
+}
+
+func (binaryInvoker) DownloadURL(ipc.ServerCall) (string, int64, error) {
+	return "", 0, errNotImplemented
+}
+
+func (i binaryInvoker) Stat(ctx ipc.ServerCall) ([]binary.PartInfo, repository.MediaInfo, error) {
+	fileName := string(i)
+	h := md5.New()
+	bytes, err := ioutil.ReadFile(fileName)
+	if err != nil {
+		return []binary.PartInfo{}, repository.MediaInfo{}, err
+	}
+	h.Write(bytes)
+	part := binary.PartInfo{Checksum: hex.EncodeToString(h.Sum(nil)), Size: int64(len(bytes))}
+	return []binary.PartInfo{part}, pkglib.MediaInfoForFileName(fileName), nil
+}
+
+func (binaryInvoker) Upload(repository.BinaryUploadContext, int32) error {
+	return errNotImplemented
+}
+
+func (binaryInvoker) GetACL(ctx ipc.ServerCall) (acl access.TaggedACLMap, etag string, err error) {
+	return nil, "", errNotImplemented
+}
+
+func (binaryInvoker) SetACL(ctx ipc.ServerCall, acl access.TaggedACLMap, etag string) error {
+	return errNotImplemented
+}
+
+type envelopeInvoker application.Envelope
+
+func (i envelopeInvoker) Match(ipc.ServerCall, []string) (application.Envelope, error) {
+	return application.Envelope(i), nil
+}
+func (envelopeInvoker) GetACL(ipc.ServerCall) (acl access.TaggedACLMap, etag string, err error) {
+	return nil, "", errNotImplemented
+}
+
+func (envelopeInvoker) SetACL(ipc.ServerCall, access.TaggedACLMap, string) error {
+	return errNotImplemented
+}
+
+func servePackage(p string, ms *mapServer, tmpZipDir string) (string, string, error) {
+	info, err := os.Stat(p)
+	if os.IsNotExist(err) {
+		return "", "", fmt.Errorf("%v not found: %v", p, err)
+	} else if err != nil {
+		return "", "", fmt.Errorf("Stat(%v) failed: %v", p, err)
+	}
+	pkgName := naming.Join("packages", info.Name())
+	fileName := p
+	// Directory packages first get zip'ped.
+	if info.IsDir() {
+		fileName = filepath.Join(tmpZipDir, info.Name()+".zip")
+		if err := pkglib.CreateZip(fileName, p); err != nil {
+			return "", "", err
+		}
+	}
+	name, err := ms.serve(pkgName, repository.BinaryServer(binaryInvoker(fileName)))
+	return info.Name(), name, err
+}
+
+// runInstallLocal creates a new envelope on the fly from the provided
+// arguments, and then points the device manager back to itself for downloading
+// the app envelope and binary.
+//
+// It sets up an app and binary server that only lives for the duration of the
+// command, and listens on the profile's listen spec.  The caller should set the
+// --veyron.proxy if the machine running the command is not accessible from the
+// device manager.
+//
+// TODO(caprita/ashankar): We should use bi-directional streams to get this
+// working over the same connection that the command makes to the device
+// manager.
+func runInstallLocal(cmd *cmdline.Command, args []string) error {
+	if expectedMin, got := 2, len(args); got < expectedMin {
+		return cmd.UsageErrorf("install-local: incorrect number of arguments, expected at least %d, got %d", expectedMin, got)
+	}
+	deviceName, title := args[0], args[1]
+	args = args[2:]
+	envelope := application.Envelope{Title: title}
+	// Extract the environment settings, binary, and arguments.
+	firstNonEnv := len(args)
+	for i, arg := range args {
+		if strings.Index(arg, "=") <= 0 {
+			firstNonEnv = i
+			break
+		}
+	}
+	envelope.Env = args[:firstNonEnv]
+	args = args[firstNonEnv:]
+	if len(args) == 0 {
+		return cmd.UsageErrorf("install-local: missing binary")
+	}
+	binary := args[0]
+	args = args[1:]
+	firstNonArg, firstPackage := len(args), len(args)
+	for i, arg := range args {
+		if arg == "PACKAGES" {
+			firstNonArg = i
+			firstPackage = i + 1
+			break
+		}
+	}
+	envelope.Args = args[:firstNonArg]
+	pkgs := args[firstPackage:]
+	if _, err := os.Stat(binary); err != nil {
+		return fmt.Errorf("binary %v not found: %v", binary, err)
+	}
+	server, cancel, err := createServer(gctx, cmd.Stderr())
+	if err != nil {
+		return fmt.Errorf("failed to create server: %v", err)
+	}
+	defer cancel()
+	envelope.Binary.File, err = server.serve("binary", repository.BinaryServer(binaryInvoker(binary)))
+	if err != nil {
+		return err
+	}
+
+	// For each package dir/file specified in the arguments list, set up an
+	// object in the binary service to serve that package, and add the
+	// object name to the envelope's Packages map.
+	tmpZipDir, err := ioutil.TempDir("", "packages")
+	if err != nil {
+		return fmt.Errorf("failed to create a temp dir for zip packages: %v", err)
+	}
+	defer os.RemoveAll(tmpZipDir)
+	for _, p := range pkgs {
+		if envelope.Packages == nil {
+			envelope.Packages = make(application.Packages)
+		}
+		pname, oname, err := servePackage(p, server, tmpZipDir)
+		if err != nil {
+			return err
+		}
+		vlog.VI(1).Infof("package %v serving as %v", pname, oname)
+		envelope.Packages[pname] = application.SignedFile{File: oname}
+	}
+	packagesRewritten := application.Packages{}
+	for pname, pspec := range packagesOverride {
+		_, oname, err := servePackage(pspec.File, server, tmpZipDir)
+		if err != nil {
+			return err
+		}
+		vlog.VI(1).Infof("package %v serving as %v", pname, oname)
+		pspec.File = oname
+		packagesRewritten[pname] = pspec
+	}
+	appName, err := server.serve("application", repository.ApplicationServer(envelopeInvoker(envelope)))
+	if err != nil {
+		return err
+	}
+	vlog.VI(1).Infof("application serving envelope as %v", appName)
+	appID, err := device.ApplicationClient(deviceName).Install(gctx, appName, device.Config(configOverride), packagesRewritten)
+	// Reset the value for any future invocations of "install" or
+	// "install-local" (we run more than one command per process in unit
+	// tests).
+	configOverride = configFlag{}
+	packagesOverride = packagesFlag{}
+	if err != nil {
+		return fmt.Errorf("Install failed: %v", err)
+	}
+	fmt.Fprintf(cmd.Stdout(), "Successfully installed: %q\n", naming.Join(deviceName, appID))
+	return nil
+}
diff --git a/cmd/mgmt/device/impl/local_install_test.go b/cmd/mgmt/device/impl/local_install_test.go
new file mode 100644
index 0000000..c696198
--- /dev/null
+++ b/cmd/mgmt/device/impl/local_install_test.go
@@ -0,0 +1,248 @@
+package impl_test
+
+import (
+	"bytes"
+	"encoding/json"
+	"fmt"
+	"io/ioutil"
+	"os"
+	"path/filepath"
+	"reflect"
+	"strings"
+	"testing"
+
+	"v.io/v23/naming"
+	"v.io/v23/security"
+	"v.io/v23/services/mgmt/application"
+	"v.io/v23/services/mgmt/device"
+
+	"v.io/x/ref/cmd/mgmt/device/impl"
+)
+
+func createFile(t *testing.T, path string, contents string) {
+	if err := ioutil.WriteFile(path, []byte(contents), 0700); err != nil {
+		t.Fatalf("Failed to create %v: %v", path, err)
+	}
+}
+
+func TestInstallLocalCommand(t *testing.T) {
+	shutdown := initTest()
+	defer shutdown()
+
+	tape := NewTape()
+	server, endpoint, err := startServer(t, gctx, tape)
+	if err != nil {
+		return
+	}
+	defer stopServer(t, server)
+	// Setup the command-line.
+	cmd := impl.Root()
+	var stdout, stderr bytes.Buffer
+	cmd.Init(nil, &stdout, &stderr)
+	deviceName := naming.JoinAddressName(endpoint.String(), "")
+	const appTitle = "Appo di tutti Appi"
+	binary := os.Args[0]
+	fi, err := os.Stat(binary)
+	if err != nil {
+		t.Fatalf("Failed to stat %v: %v", binary, err)
+	}
+	binarySize := fi.Size()
+	for i, c := range []struct {
+		args         []string
+		stderrSubstr string
+	}{
+		{
+			[]string{deviceName}, "incorrect number of arguments",
+		},
+		{
+			[]string{deviceName, appTitle}, "missing binary",
+		},
+		{
+			[]string{deviceName, appTitle, "a=b"}, "missing binary",
+		},
+		{
+			[]string{deviceName, appTitle, "foo"}, "binary foo not found",
+		},
+		{
+			[]string{deviceName, appTitle, binary, "PACKAGES", "foo"}, "foo not found",
+		},
+	} {
+		c.args = append([]string{"install-local"}, c.args...)
+		if err := cmd.Execute(c.args); err == nil {
+			t.Fatalf("test case %d: wrongly failed to receive a non-nil error.", i)
+		} else {
+			fmt.Fprintln(&stderr, "ERROR:", err)
+			if want, got := c.stderrSubstr, stderr.String(); !strings.Contains(got, want) {
+				t.Errorf("test case %d: %q not found in stderr: %q", i, want, got)
+			}
+		}
+		if got, expected := len(tape.Play()), 0; got != expected {
+			t.Errorf("test case %d: invalid call sequence. Got %v, want %v", got, expected)
+		}
+		tape.Rewind()
+		stdout.Reset()
+		stderr.Reset()
+	}
+	emptySig := security.Signature{}
+	emptyBlessings := security.Blessings{}
+	cfg := device.Config{"someflag": "somevalue"}
+
+	testPackagesDir, err := ioutil.TempDir("", "testdir")
+	if err != nil {
+		t.Fatalf("Failed to create temp dir: %v", err)
+	}
+	defer os.RemoveAll(testPackagesDir)
+	pkgFile1 := filepath.Join(testPackagesDir, "file1.txt")
+	createFile(t, pkgFile1, "1234567")
+	pkgFile2 := filepath.Join(testPackagesDir, "file2")
+	createFile(t, pkgFile2, string([]byte{0x01, 0x02, 0x03, 0x04}))
+	pkgDir1 := filepath.Join(testPackagesDir, "dir1")
+	if err := os.Mkdir(pkgDir1, 0700); err != nil {
+		t.Fatalf("Failed to create dir1: %v", err)
+	}
+	createFile(t, filepath.Join(pkgDir1, "f1"), "123")
+	createFile(t, filepath.Join(pkgDir1, "f2"), "456")
+	createFile(t, filepath.Join(pkgDir1, "f3"), "7890")
+
+	pkgFile3 := filepath.Join(testPackagesDir, "file3")
+	createFile(t, pkgFile3, "12345")
+	pkgFile4 := filepath.Join(testPackagesDir, "file4")
+	createFile(t, pkgFile4, "123")
+	pkgDir2 := filepath.Join(testPackagesDir, "dir2")
+	if err := os.Mkdir(pkgDir2, 0700); err != nil {
+		t.Fatalf("Failed to create dir2: %v", err)
+	}
+	createFile(t, filepath.Join(pkgDir2, "f1"), "123456")
+	createFile(t, filepath.Join(pkgDir2, "f2"), "78")
+	pkg := application.Packages{
+		"overridepkg1": application.SignedFile{File: pkgFile3},
+		"overridepkg2": application.SignedFile{File: pkgFile4},
+		"overridepkg3": application.SignedFile{File: pkgDir2},
+	}
+
+	for i, c := range []struct {
+		args         []string
+		config       device.Config
+		packages     application.Packages
+		expectedTape interface{}
+	}{
+		{
+			[]string{deviceName, appTitle, binary},
+			nil,
+			nil,
+			InstallStimulus{
+				"Install",
+				appNameAfterFetch,
+				nil,
+				nil,
+				application.Envelope{
+					Title: appTitle,
+					Binary: application.SignedFile{
+						File:      binaryNameAfterFetch,
+						Signature: emptySig,
+					},
+					Publisher: emptyBlessings,
+				},
+				map[string]int64{"binary": binarySize}},
+		},
+		{
+			[]string{deviceName, appTitle, binary},
+			cfg,
+			nil,
+			InstallStimulus{
+				"Install",
+				appNameAfterFetch,
+				cfg,
+				nil,
+				application.Envelope{
+					Title: appTitle,
+					Binary: application.SignedFile{
+						File:      binaryNameAfterFetch,
+						Signature: emptySig,
+					},
+					Publisher: emptyBlessings,
+				},
+				map[string]int64{"binary": binarySize}},
+		},
+		{
+			[]string{deviceName, appTitle, "ENV1=V1", "ENV2=V2", binary, "FLAG1=V1", "FLAG2=V2"},
+			nil,
+			nil,
+			InstallStimulus{
+				"Install",
+				appNameAfterFetch,
+				nil,
+				nil,
+				application.Envelope{
+					Title: appTitle,
+					Binary: application.SignedFile{
+						File:      binaryNameAfterFetch,
+						Signature: emptySig,
+					},
+					Publisher: emptyBlessings,
+					Env:       []string{"ENV1=V1", "ENV2=V2"},
+					Args:      []string{"FLAG1=V1", "FLAG2=V2"},
+				},
+				map[string]int64{"binary": binarySize}},
+		},
+		{
+			[]string{deviceName, appTitle, "ENV=V", binary, "FLAG=V", "PACKAGES", pkgFile1, pkgFile2, pkgDir1},
+			nil,
+			pkg,
+			InstallStimulus{"Install",
+				appNameAfterFetch,
+				nil,
+				nil,
+				application.Envelope{
+					Title: appTitle,
+					Binary: application.SignedFile{
+						File:      binaryNameAfterFetch,
+						Signature: emptySig,
+					},
+					Publisher: emptyBlessings,
+					Env:       []string{"ENV=V"},
+					Args:      []string{"FLAG=V"},
+				},
+				map[string]int64{
+					"binary":                        binarySize,
+					"packages/file1.txt":            7,
+					"packages/file2":                4,
+					"packages/dir1":                 10,
+					"overridepackages/overridepkg1": 5,
+					"overridepackages/overridepkg2": 3,
+					"overridepackages/overridepkg3": 8,
+				},
+			},
+		},
+	} {
+		const appId = "myBestAppID"
+		tape.SetResponses([]interface{}{InstallResponse{appId, nil}})
+		if c.config != nil {
+			jsonConfig, err := json.Marshal(c.config)
+			if err != nil {
+				t.Fatalf("test case %d: Marshal(%v) failed: %v", i, c.config, err)
+			}
+			c.args = append([]string{fmt.Sprintf("--config=%s", string(jsonConfig))}, c.args...)
+		}
+		if c.packages != nil {
+			jsonPackages, err := json.Marshal(c.packages)
+			if err != nil {
+				t.Fatalf("test case %d: Marshal(%v) failed: %v", i, c.packages, err)
+			}
+			c.args = append([]string{fmt.Sprintf("--packages=%s", string(jsonPackages))}, c.args...)
+		}
+		c.args = append([]string{"install-local"}, c.args...)
+		if err := cmd.Execute(c.args); err != nil {
+			t.Fatalf("test case %d: %v", i, err)
+		}
+		if expected, got := fmt.Sprintf("Successfully installed: %q", naming.Join(deviceName, appId)), strings.TrimSpace(stdout.String()); got != expected {
+			t.Fatalf("test case %d: Unexpected output from Install. Got %q, expected %q", i, got, expected)
+		}
+		if got, expected := tape.Play(), []interface{}{c.expectedTape}; !reflect.DeepEqual(expected, got) {
+			t.Errorf("test case %d: Invalid call sequence. Got %#v, want %#v", i, got, expected)
+		}
+		tape.Rewind()
+		stdout.Reset()
+		stderr.Reset()
+	}
+}
diff --git a/cmd/mgmt/device/impl/mock_test.go b/cmd/mgmt/device/impl/mock_test.go
new file mode 100644
index 0000000..f08b44c
--- /dev/null
+++ b/cmd/mgmt/device/impl/mock_test.go
@@ -0,0 +1,40 @@
+package impl_test
+
+import (
+	"fmt"
+)
+
+type Tape struct {
+	stimuli   []interface{}
+	responses []interface{}
+}
+
+func (r *Tape) Record(call interface{}) interface{} {
+	r.stimuli = append(r.stimuli, call)
+
+	if len(r.responses) < 1 {
+		return fmt.Errorf("Record(%#v) had no response", call)
+	}
+	resp := r.responses[0]
+	r.responses = r.responses[1:]
+	return resp
+}
+
+func (r *Tape) SetResponses(responses []interface{}) {
+	r.responses = responses
+}
+
+func (r *Tape) Rewind() {
+	r.stimuli = make([]interface{}, 0)
+	r.responses = make([]interface{}, 0)
+}
+
+func (r *Tape) Play() []interface{} {
+	return r.stimuli
+}
+
+func NewTape() *Tape {
+	tape := new(Tape)
+	tape.Rewind()
+	return tape
+}
diff --git a/cmd/mgmt/device/impl/root.go b/cmd/mgmt/device/impl/root.go
new file mode 100644
index 0000000..a05baaa
--- /dev/null
+++ b/cmd/mgmt/device/impl/root.go
@@ -0,0 +1,24 @@
+package impl
+
+import (
+	"v.io/v23/context"
+
+	"v.io/x/lib/cmdline"
+)
+
+var gctx *context.T
+
+func SetGlobalContext(ctx *context.T) {
+	gctx = ctx
+}
+
+func Root() *cmdline.Command {
+	return &cmdline.Command{
+		Name:  "device",
+		Short: "Tool for interacting with the veyron device manager",
+		Long: `
+The device tool facilitates interaction with the veyron device manager.
+`,
+		Children: []*cmdline.Command{cmdInstall, cmdInstallLocal, cmdUninstall, cmdStart, associateRoot(), cmdDescribe, cmdClaim, cmdStop, cmdSuspend, cmdResume, cmdRevert, cmdUpdate, cmdDebug, aclRoot()},
+	}
+}
diff --git a/cmd/mgmt/device/impl/util_test.go b/cmd/mgmt/device/impl/util_test.go
new file mode 100644
index 0000000..1914614
--- /dev/null
+++ b/cmd/mgmt/device/impl/util_test.go
@@ -0,0 +1,23 @@
+package impl_test
+
+import (
+	"v.io/v23"
+	"v.io/v23/context"
+
+	"v.io/x/ref/cmd/mgmt/device/impl"
+	"v.io/x/ref/lib/testutil"
+	_ "v.io/x/ref/profiles"
+)
+
+var gctx *context.T
+
+func initTest() v23.Shutdown {
+	var shutdown v23.Shutdown
+	gctx, shutdown = testutil.InitForTest()
+	impl.SetGlobalContext(gctx)
+	return func() {
+		shutdown()
+		impl.SetGlobalContext(nil)
+		gctx = nil
+	}
+}
diff --git a/cmd/mgmt/device/main.go b/cmd/mgmt/device/main.go
new file mode 100644
index 0000000..c5714e2
--- /dev/null
+++ b/cmd/mgmt/device/main.go
@@ -0,0 +1,21 @@
+// The following enables go generate to generate the doc.go file.
+//go:generate go run $VANADIUM_ROOT/release/go/src/v.io/x/lib/cmdline/testdata/gendoc.go .
+
+package main
+
+import (
+	"os"
+
+	"v.io/v23"
+
+	"v.io/x/ref/cmd/mgmt/device/impl"
+	_ "v.io/x/ref/profiles/static"
+)
+
+func main() {
+	gctx, shutdown := v23.Init()
+	impl.SetGlobalContext(gctx)
+	exitCode := impl.Root().Main()
+	shutdown()
+	os.Exit(exitCode)
+}
diff --git a/cmd/mgmt/dummy.go b/cmd/mgmt/dummy.go
new file mode 100644
index 0000000..2d4fedb
--- /dev/null
+++ b/cmd/mgmt/dummy.go
@@ -0,0 +1 @@
+package mgmt
diff --git a/cmd/mgmt/mgmt_v23_test.go b/cmd/mgmt/mgmt_v23_test.go
new file mode 100644
index 0000000..172ecdb
--- /dev/null
+++ b/cmd/mgmt/mgmt_v23_test.go
@@ -0,0 +1,394 @@
+// Test the device manager and related services and tools.
+//
+// By default, this script tests the device manager in a fashion amenable
+// to automatic testing: the --single_user is passed to the device
+// manager so that all device manager components run as the same user and
+// no user input (such as an agent pass phrase) is needed.
+//
+// When this script is invoked with the --with_suid <user> flag, it
+// installs the device manager in its more secure multi-account
+// configuration where the device manager runs under the account of the
+// invoker and test apps will be executed as <user>. This mode will
+// require root permisisons to install and may require configuring an
+// agent passphrase.
+//
+// For exanple:
+//
+//   v23 go test -v . --v23.tests --with_suid vanaguest
+//
+// to test a device manager with multi-account support enabled for app
+// account vanaguest.
+//
+package mgmt_test
+
+//go:generate v23 test generate .
+
+import (
+	"errors"
+	"flag"
+	"fmt"
+	"io/ioutil"
+	"math/rand"
+	"os"
+	"path/filepath"
+	"strings"
+	"time"
+
+	"v.io/x/ref/lib/testutil/v23tests"
+	_ "v.io/x/ref/profiles"
+)
+
+var (
+	suidUserFlag string
+	hostname     string
+	errTimeout   = errors.New("timeout")
+)
+
+func init() {
+	flag.StringVar(&suidUserFlag, "with_suid", "", "run the device manager as the specified user")
+	name, err := os.Hostname()
+	if err != nil {
+		panic(fmt.Sprintf("Hostname() failed: %v", err))
+	}
+	hostname = name
+}
+
+func V23TestNodeManager(i *v23tests.T) {
+	defer fmt.Fprintf(os.Stderr, "--------------- SHUTDOWN ---------------\n")
+	userFlag := "--single_user"
+	withSuid := false
+	if len(suidUserFlag) > 0 {
+		userFlag = "--with_suid=" + suidUserFlag
+		withSuid = true
+	}
+	i.Logf("user flag: %q", userFlag)
+
+	v23tests.RunRootMT(i, "--veyron.tcp.address=127.0.0.1:0")
+	workDir := i.NewTempDir()
+
+	mkSubdir := func(sub string) string {
+		n := filepath.Join(workDir, sub)
+		if err := os.Mkdir(n, 0755); err != nil {
+			i.Fatalf("failed to create %q: %v", n, err)
+		}
+		return n
+	}
+
+	binStagingDir := mkSubdir("bin")
+	agentServerBin := i.BuildGoPkg("v.io/x/ref/security/agent/agentd")
+	suidHelperBin := i.BuildGoPkg("v.io/x/ref/services/mgmt/suidhelper")
+	initHelperBin := i.BuildGoPkg("v.io/x/ref/services/mgmt/inithelper")
+
+	// Device manager and principal use their own set of credentials.
+	// The credentials directory will be populated with Start an application
+	// server under the blessing "alice/myworkstation/applicationd" so that
+	// the device ("alice/myworkstation") can talk to it. ALl of the binaries
+	// that communicate with each other must share this credentials directory.
+	credentials := "VEYRON_CREDENTIALS=" + i.NewTempDir()
+	namespaceBin := i.BuildGoPkg("v.io/x/ref/cmd/namespace").WithEnv(credentials)
+	debugBin := i.BuildGoPkg("v.io/x/ref/cmd/debug").WithEnv(credentials)
+	deviceBin := i.BuildGoPkg("v.io/x/ref/cmd/mgmt/device").WithEnv(credentials)
+	devicedBin := i.BuildGoPkg("v.io/x/ref/services/mgmt/device/deviced").WithEnv(credentials)
+	deviceScript := i.BinaryFromPath("device/devicex").WithEnv(credentials)
+	principalBin := i.BuildGoPkg("v.io/x/ref/cmd/principal").WithEnv(credentials)
+	binarydBin := i.BuildGoPkg("v.io/x/ref/services/mgmt/binary/binaryd").WithEnv(credentials)
+	binaryBin := i.BuildGoPkg("v.io/x/ref/cmd/binary").WithEnv(credentials)
+	applicationdBin := i.BuildGoPkg("v.io/x/ref/services/mgmt/application/applicationd").WithEnv(credentials)
+	applicationBin := i.BuildGoPkg("v.io/x/ref/cmd/application").WithEnv(credentials)
+
+	appDName := "applicationd"
+	devicedAppName := filepath.Join(appDName, "deviced", "test")
+
+	i.BinaryFromPath("/bin/cp").Start(agentServerBin.Path(), suidHelperBin.Path(), initHelperBin.Path(), devicedBin.Path(), binStagingDir).WaitOrDie(os.Stdout, os.Stderr)
+
+	dmInstallDir := filepath.Join(workDir, "dm")
+	i.SetVar("VANADIUM_DEVICE_DIR", dmInstallDir)
+
+	neighborhoodName := fmt.Sprintf("%s-%d-%d", hostname, os.Getpid(), rand.Int())
+
+	deviceScript.Start(
+		"install",
+		binStagingDir,
+		userFlag,
+		"--origin="+devicedAppName,
+		"--",
+		"--veyron.tcp.address=127.0.0.1:0",
+		"--neighborhood_name="+neighborhoodName).
+		WaitOrDie(os.Stdout, os.Stderr)
+
+	deviceScript.Start("start").WaitOrDie(os.Stdout, os.Stderr)
+
+	mtName := "devices/" + hostname
+
+	resolve := func(name string) string {
+		resolver := func() (interface{}, error) {
+			// Use Start, rather than Run, sinde it's ok for 'namespace resolve'
+			// to fail with 'name doesn't exist'
+			inv := namespaceBin.Start("resolve", name)
+			// Cleanup after ourselves to avoid leaving a ton of invocations
+			// lying around which obscure logging output.
+			defer inv.Wait(nil, os.Stderr)
+			if r := strings.TrimRight(inv.Output(), "\n"); len(r) > 0 {
+				return r, nil
+			}
+			return nil, nil
+		}
+		return i.WaitFor(resolver, 100*time.Millisecond, time.Minute).(string)
+	}
+	mtEP := resolve(mtName)
+
+	// Verify that device manager's mounttable is published under the expected
+	// name (hostname).
+	if got := namespaceBin.Run("glob", mtName); len(got) == 0 {
+		i.Fatalf("glob failed for %q", mtName)
+	}
+
+	// Create a self-signed blessing with name "alice" and set it as default
+	// and shareable with all peers on the principal that the device manager
+	// and principal are sharing (via the .WithEnv method) above. This
+	// blessing will be used by all commands run by the device manager that
+	// specify the same credentials.
+	// TODO - update these commands
+	// that except those
+	// run with a different which gets a principal forked from the
+	// process principal.
+	blessingFilename := filepath.Join(workDir, "alice.bless")
+	blessing := principalBin.Run("blessself", "alice")
+	if err := ioutil.WriteFile(blessingFilename, []byte(blessing), 0755); err != nil {
+		i.Fatal(err)
+	}
+	principalBin.Run("store", "setdefault", blessingFilename)
+	principalBin.Run("store", "set", blessingFilename, "...")
+	defer os.Remove(blessingFilename)
+
+	// Claim the device as "alice/myworkstation".
+	deviceBin.Start("claim", mtName+"/devmgr/device", "myworkstation")
+
+	resolveChange := func(name, old string) string {
+		resolver := func() (interface{}, error) {
+			inv := namespaceBin.Start("resolve", name)
+			defer inv.Wait(nil, os.Stderr)
+			if r := strings.TrimRight(inv.Output(), "\n"); len(r) > 0 && r != old {
+				return r, nil
+			}
+			return nil, nil
+		}
+		return i.WaitFor(resolver, 100*time.Millisecond, time.Minute).(string)
+	}
+
+	// Wait for the device manager to update its mount table entry.
+	mtEP = resolveChange(mtName, mtEP)
+
+	if withSuid {
+		/*
+		   		   "${DEVICE_BIN}" associate add "${MT_NAME}/devmgr/device" "${SUID_USER}"  "alice"
+		        shell_test::assert_eq   "$("${DEVICE_BIN}" associate list "${MT_NAME}/devmgr/device")" \
+		          "alice ${SUID_USER}" "${LINENO}"
+		*/
+	}
+
+	// Verify the device's default blessing is as expected.
+	inv := debugBin.Start("stats", "read", mtName+"/devmgr/__debug/stats/security/principal/*/blessingstore")
+	inv.ExpectRE(".*Default blessings: alice/myworkstation$", -1)
+
+	// Get the device's profile, which should be set to non-empty string
+	inv = deviceBin.Start("describe", mtName+"/devmgr/device")
+
+	parts := inv.ExpectRE(`{Profiles:map\[(.*):{}\]}`, 1)
+	expectOneMatch := func(parts [][]string) string {
+		if len(parts) != 1 || len(parts[0]) != 2 {
+			loc := v23tests.Caller(1)
+			i.Fatalf("%s: failed to match profile: %#v", loc, parts)
+		}
+		return parts[0][1]
+	}
+	deviceProfile := expectOneMatch(parts)
+	if len(deviceProfile) == 0 {
+		i.Fatalf("failed to get profile")
+	}
+
+	binarydName := "binaryd"
+	// Start an application server under the blessing
+	// "alice/myworkstation/applicationd" so that
+	// the device ("alice/myworkstation") can talk to it.
+	binarydBin.Start(
+		"--name="+binarydName,
+		"--root_dir="+filepath.Join(workDir, "binstore"),
+		"--veyron.tcp.address=127.0.0.1:0",
+		"--http=127.0.0.1:0")
+
+	sampleAppBinName := binarydName + "/testapp"
+	binaryBin.Run("upload", sampleAppBinName, binarydBin.Path())
+
+	// Verify that the binary we uploaded is shown by glob, we need to run
+	// with the same blessed credentials as binaryd in order to be able to
+	// glob its names pace.
+	if got := namespaceBin.WithEnv(credentials).Run("glob", sampleAppBinName); len(got) == 0 {
+		i.Fatalf("glob failed for %q", sampleAppBinName)
+	}
+
+	appstoreDir := mkSubdir("apptstore")
+
+	applicationdBin.Start(
+		"--name="+appDName,
+		"--store="+appstoreDir,
+		"--veyron.tcp.address=127.0.0.1:0",
+	)
+
+	sampleAppName := appDName + "/testapp/v0"
+	appPubName := "testbinaryd"
+	appEnvelopeFilename := filepath.Join(workDir, "app.envelope")
+	appEnvelope := fmt.Sprintf("{\"Title\":\"BINARYD\", \"Args\":[\"--name=%s\", \"--root_dir=./binstore\", \"--veyron.tcp.address=127.0.0.1:0\", \"--http=127.0.0.1:0\"], \"Binary\":{\"File\":%q}, \"Env\":[]}", appPubName, sampleAppBinName)
+	ioutil.WriteFile(appEnvelopeFilename, []byte(appEnvelope), 0666)
+	defer os.Remove(appEnvelopeFilename)
+
+	output := applicationBin.Run("put", sampleAppName, deviceProfile, appEnvelopeFilename)
+	if got, want := output, "Application envelope added successfully."; got != want {
+		i.Fatalf("got %q, want %q", got, want)
+	}
+
+	// Verify that the envelope we uploaded shows up with glob.
+	inv = applicationBin.Start("match", sampleAppName, deviceProfile)
+	parts = inv.ExpectSetEventuallyRE(`"Title": "(.*)",`, `"File": "(.*)",`)
+	if got, want := len(parts), 2; got != want {
+		i.Fatalf("got %d, want %d", got, want)
+	}
+	for line, want := range []string{"BINARYD", sampleAppBinName} {
+		if got := parts[line][1]; got != want {
+			i.Fatalf("got %q, want %q", got, want)
+		}
+	}
+
+	// Install the app on the device.
+	inv = deviceBin.Start("install", mtName+"/devmgr/apps", sampleAppName)
+	parts = inv.ExpectRE(`Successfully installed: "(.*)"`, 1)
+	installationName := expectOneMatch(parts)
+
+	// Verify that the installation shows up when globbing the device manager.
+	output = namespaceBin.Run("glob", mtName+"/devmgr/apps/BINARYD/*")
+	if got, want := output, installationName; got != want {
+		i.Fatalf("got %q, want %q", got, want)
+	}
+
+	// Start an instance of the app, granting it blessing extension myapp.
+	inv = deviceBin.Start("start", installationName, "myapp")
+	parts = inv.ExpectRE(`Successfully started: "(.*)"`, 1)
+	instanceName := expectOneMatch(parts)
+
+	resolve(mtName + "/" + appPubName)
+
+	// Verify that the instance shows up when globbing the device manager.
+	output = namespaceBin.Run("glob", mtName+"/devmgr/apps/BINARYD/*/*")
+	if got, want := output, instanceName; got != want {
+		i.Fatalf("got %q, want %q", got, want)
+	}
+
+	// TODO(rjkroege): Verify that the app is actually running as ${SUID_USER}
+
+	// Verify the app's default blessing.
+	inv = debugBin.Start("stats", "read", instanceName+"/stats/security/principal/*/blessingstore")
+	// Why is this alice/myworkstation/myapp/BINARYD and not
+	// alice/myapp/BINARYD as seen by the test.sh?
+	inv.ExpectRE(".*Default blessings: alice/myworkstation/myapp/BINARYD$", -1)
+
+	// Stop the instance
+	deviceBin.Run("stop", instanceName)
+
+	// Verify that logs, but not stats, show up when globbing the
+	// stopped instance.
+	if output = namespaceBin.Run("glob", instanceName+"/stats/..."); len(output) > 0 {
+		i.Fatalf("no output expected for glob %s/stats/..., got %q", output, instanceName)
+	}
+	if output = namespaceBin.Run("glob", instanceName+"/logs/..."); len(output) == 0 {
+		i.Fatalf("output expected for glob %s/logs/..., but got none", instanceName)
+	}
+
+	// Upload a deviced binary
+	devicedAppBinName := binarydName + "/deviced"
+	binaryBin.Run("upload", devicedAppBinName, devicedBin.Path())
+
+	// Upload a device manager envelope, make sure that we set
+	// VEYRON_CREDENTIALS in the enevelope, otherwise the updated device
+	// manager will use new credentials.
+	devicedEnvelopeFilename := filepath.Join(workDir, "deviced.envelope")
+	devicedEnvelope := fmt.Sprintf("{\"Title\":\"device manager\", \"Binary\":{\"File\":%q}, \"Env\":[%q]}", devicedAppBinName, credentials)
+	ioutil.WriteFile(devicedEnvelopeFilename, []byte(devicedEnvelope), 0666)
+	defer os.Remove(devicedEnvelopeFilename)
+	applicationBin.Run("put", devicedAppName, deviceProfile, devicedEnvelopeFilename)
+
+	// Update the device manager.
+	deviceBin.Run("update", mtName+"/devmgr/device")
+	mtEP = resolveChange(mtName, mtEP)
+
+	// Verify that device manager's mounttable is still published under the
+	// expected name (hostname).
+	if namespaceBin.Run("glob", mtName) == "" {
+		i.Fatalf("failed to glob %s", mtName)
+	}
+
+	// Revert the device manager
+	deviceBin.Run("revert", mtName+"/devmgr/device")
+	mtEP = resolveChange(mtName, mtEP)
+
+	// Verify that device manager's mounttable is still published under the
+	// expected name (hostname).
+	if namespaceBin.Run("glob", mtName) == "" {
+		i.Fatalf("failed to glob %s", mtName)
+	}
+
+	// Verify that the local mounttable exists, and that the device manager,
+	// the global namespace, and the neighborhood are mounted on it.
+	n := mtEP + "/devmgr"
+	if namespaceBin.Run("resolve", n) == "" {
+		i.Fatalf("failed to resolve %s", n)
+	}
+	n = mtEP + "/nh"
+	if namespaceBin.Run("resolve", n) == "" {
+		i.Fatalf("failed to resolve %s", n)
+	}
+	namespaceRoot, _ := i.GetVar("NAMESPACE_ROOT")
+	n = mtEP + "/global"
+	// TODO(ashankar): The expected blessings of the namespace root should
+	// also be from some VAR or something.  For now, hardcoded, but this
+	// should be fixed along with
+	// https://github.com/veyron/release-issues/issues/98
+	if got, want := namespaceBin.Run("resolve", n), fmt.Sprintf("[alice/myworkstation]%v", namespaceRoot); got != want {
+		i.Fatalf("got %q, want %q", got, want)
+	}
+
+	// Suspend the device manager, wait for the endpoint to change
+	deviceBin.Run("suspend", mtName+"/devmgr/device")
+	mtEP = resolveChange(mtName, mtEP)
+
+	// Stop the device manager.
+	deviceScript.Run("stop")
+
+	// Wait for the mounttable entry to go away.
+	resolveGone := func(name string) string {
+		resolver := func() (interface{}, error) {
+			inv := namespaceBin.Start("resolve", name)
+			defer inv.Wait(nil, os.Stderr)
+			if r := strings.TrimRight(inv.Output(), "\n"); len(r) == 0 {
+				return r, nil
+			}
+			return nil, nil
+		}
+		return i.WaitFor(resolver, 100*time.Millisecond, time.Minute).(string)
+	}
+	resolveGone(mtName)
+
+	fi, err := ioutil.ReadDir(dmInstallDir)
+	if err != nil {
+		i.Fatalf("failed to readdir for %q: %v", dmInstallDir, err)
+	}
+
+	deviceScript.Run("uninstall")
+
+	fi, err = ioutil.ReadDir(dmInstallDir)
+	if err == nil || len(fi) > 0 {
+		i.Fatalf("managed to read %d entries from %q", len(fi), dmInstallDir)
+	}
+	if err != nil && !strings.Contains(err.Error(), "no such file or directory") {
+		i.Fatalf("wrong error: %v", err)
+	}
+}
diff --git a/cmd/mgmt/shell/lib/pkg.go b/cmd/mgmt/shell/lib/pkg.go
new file mode 100644
index 0000000..55c21f8
--- /dev/null
+++ b/cmd/mgmt/shell/lib/pkg.go
@@ -0,0 +1 @@
+package lib
diff --git a/cmd/mgmt/shell/lib/shell.sh b/cmd/mgmt/shell/lib/shell.sh
new file mode 100755
index 0000000..aa2319c
--- /dev/null
+++ b/cmd/mgmt/shell/lib/shell.sh
@@ -0,0 +1,214 @@
+#!/bin/bash
+
+# The shell library is used to execute veyron shell scripts.
+
+# IMPORTANT: If your script registers its own "trap" handler, that handler must
+# call shell::at_exit to clean up all temporary files and directories created by
+# this library.
+
+set -e
+set -u
+
+TMPDIR="${TMPDIR-/tmp}"
+VEYRON_SHELL_TMP_DIRS=${VEYRON_SHELL_TMP_DIRS-$(mktemp "${TMPDIR}/XXXXXXXX")}
+VEYRON_SHELL_TMP_FILES=${VEYRON_SHELL_TMP_FILES-$(mktemp "${TMPDIR}/XXXXXXXX")}
+VEYRON_SHELL_TMP_PIDS=${VEYRON_SHELL_TMP_PIDS-$(mktemp "${TMPDIR}/XXXXXXXX")}
+
+trap shell::at_exit INT TERM EXIT
+
+# When a user of this library needs cleanup to be performed by root
+# because different processes are running with different system
+# identities, it should set this variable to root to escalate
+# privilege appropriately.
+SUDO_USER="$(whoami)"
+
+# shell::at_exit is executed when the shell script exits. It is used,
+# for example, to garbage collect any temporary files and directories
+# created by invocations of shell::tmp_file and shell:tmp_dir.
+shell::at_exit() {
+  # If the variable VEYRON_SHELL_CMD_LOOP_AT_EXIT is non-empty, accept commands
+  # from the user in a command loop.  This can preserve the state in the logs
+  # directories while the user examines them.
+  case "${VEYRON_SHELL_CMD_LOOP_AT_EXIT-}" in
+  ?*)
+    local cmdline
+    set +u
+    echo 'Entering debug shell.  Type control-D or "exit" to exit.' >&2
+    while echo -n "test> " >&2; read cmdline; do
+      eval "$cmdline"
+    done
+    set -u
+    ;;
+  esac
+  # Unset the trap so that it doesn't run again on exit.
+  trap - INT TERM EXIT
+  for pid in $(cat "${VEYRON_SHELL_TMP_PIDS}"); do
+    sudo -u "${SUDO_USER}" kill "${pid}" &> /dev/null || true
+  done
+  for tmp_dir in $(cat "${VEYRON_SHELL_TMP_DIRS}"); do
+     sudo -u "${SUDO_USER}" rm -rf "${tmp_dir}" &>/dev/null
+  done
+  for tmp_file in $(cat "${VEYRON_SHELL_TMP_FILES}"); do
+     sudo -u "${SUDO_USER}" rm -f "${tmp_file}" &>/dev/null
+  done
+   sudo -u "${SUDO_USER}" rm -f "${VEYRON_SHELL_TMP_DIRS}" "${VEYRON_SHELL_TMP_FILES}" "${VEYRON_SHELL_TMP_PIDS}" &>/dev/null
+}
+
+# shell::kill_child_processes kills all child processes.
+# Note, child processes must set their own "at_exit: kill_child_processes"
+# signal handlers in order for this to kill their descendants.
+shell::kill_child_processes() {
+  # Attempt to shutdown child processes by sending the TERM signal.
+  if [[ -n "$(jobs -p -r)" ]]; then
+    shell::silence pkill -P $$
+    sleep 1
+    # Force shutdown of any remaining child processes using the KILL signal.
+    # Note, we only "sleep 1" and "kill -9" if there were child processes.
+    if [[ -n "$(jobs -p -r)" ]]; then
+      shell::silence sudo -u "${SUDO_USER}"  pkill -9 -P $$
+    fi
+  fi
+}
+
+# shell::check_deps can be used to check which dependencies are
+# missing on the host.
+#
+# Example:
+#   local -r DEPS=$(shell::check_deps git golang-go)
+#   if [[ -n "${DEPS} ]]; then
+#     sudo apt-get install $DEPS
+#   fi
+shell::check_deps() {
+  local RESULT=""
+  case $(uname -s) in
+    "Linux")
+      set +e
+      for pkg in "$@"; do
+        dpkg -s "${pkg}" &> /dev/null
+        if [[ "$?" -ne 0 ]]; then
+          RESULT+=" ${pkg}"
+        fi
+      done
+      set -e
+      ;;
+    "Darwin")
+      set +e
+      for pkg in "$@"; do
+        if [[ -z "$(brew ls --versions ${pkg})" ]]; then
+          RESULT+=" ${pkg}"
+        fi
+      done
+      set -e
+      ;;
+    *)
+      echo "Operating system $(uname -s) is not supported."
+      exit 1
+  esac
+  echo "${RESULT}"
+}
+
+# shell::check_result can be used to obtain the exit status of a bash
+# command. By the semantics of "set -e", a script exits immediately
+# upon encountering a command that fails. This function should be used
+# if instead, a script wants to take an action depending on the exit
+# status.
+#
+# Example:
+#   local -r RESULT=$(shell::check_result <command>)
+#   if [[ "${RESULT}" -ne 0 ]]; then
+#     <handle failure>
+#   else
+#     <handle success>
+#   fi
+shell::check_result() {
+  set +e
+  "$@" &> /dev/null
+  echo "$?"
+  set -e
+}
+
+# shell::tmp_dir can be used to create a temporary directory.
+#
+# Example:
+#   local -R TMP_DIR=$(shell::tmp_dir)
+shell::tmp_dir() {
+  local -r RESULT=$(mktemp -d "${TMPDIR}/XXXXXXXX")
+  echo "${RESULT}" >> "${VEYRON_SHELL_TMP_DIRS}"
+  echo "${RESULT}"
+}
+
+# shell::tmp_file can be used to create a temporary file.
+#
+# Example:
+#   local -R TMP_FILE=$(shell::tmp_file)
+shell::tmp_file() {
+  local -r RESULT=$(mktemp "${TMPDIR}/XXXXXXXX")
+  echo "${RESULT}" >> "${VEYRON_SHELL_TMP_FILES}"
+  echo "${RESULT}"
+}
+
+# shell::run_server is used to start a long running server process and
+# to verify that it is still running after a set timeout period. This
+# is useful for catching cases where the server either fails to start or
+# fails soon after it has started.
+# The script will return 0 if the command is running at that point in time
+# and a non-zero value otherwise. It echos the server's pid.
+#
+# Example:
+#   shell::run_server 5 stdout stderr command arg1 arg2 || exit 1
+shell::run_server() {
+  local -r TIMEOUT="$1"
+  local -r STDOUT="$2"
+  local -r STDERR="$3"
+  shift; shift; shift
+  if [[ "${STDOUT}" = "${STDERR}" ]]; then
+    "$@" > "${STDOUT}" 2>&1 &
+  else
+    "$@" > "${STDOUT}" 2> "${STDERR}" &
+  fi
+  local -r SERVER_PID=$!
+  echo "${SERVER_PID}" >> "${VEYRON_SHELL_TMP_PIDS}"
+  echo "${SERVER_PID}"
+
+  for i in $(seq 1 "${TIMEOUT}"); do
+    sleep 1
+    local RESULT=$(shell::check_result kill -0 "${SERVER_PID}")
+    if [[ "${RESULT}" = "0" ]]; then
+      # server's up, can return early
+      return 0
+    fi
+  done
+  return "${RESULT}"
+}
+
+# shell::timed_wait_for is used to wait until the given pattern appears
+# in the given file within a set timeout. It returns 0 if the pattern
+# was successfully matched and 1 if the timeout expires before the pattern
+# is found.
+#
+# Example:
+#   local -r LOG_FILE=$(shell::tmp_file)
+#   shell::run_server 1 "${LOG_FILE}" "${LOG_FILE}" <server> <args>...
+#   shell::timed_wait_for 1 "${LOG_FILE}" "server started"
+shell::timed_wait_for() {
+  local -r TIMEOUT="$1"
+  local -r FILE="$2"
+  local -r PATTERN="$3"
+  for i in $(seq 1 "${TIMEOUT}"); do
+    sleep 1
+    grep -m 1 "${PATTERN}" "${FILE}" > /dev/null && return 0 || true
+  done
+  # grep has timed out.
+  echo "Timed out. Here's the file:"
+  echo "====BEGIN FILE==="
+  cat "${FILE}"
+  echo "====END FILE==="
+  return 1
+}
+
+# shell::silence runs the given command with the supplied arguments,
+# redirecting all of its output to /dev/null and ignoring its exit
+# code.
+shell::silence() {
+  "$@" &> /dev/null || true
+}
diff --git a/cmd/mgmt/shell/lib/shell_test.sh b/cmd/mgmt/shell/lib/shell_test.sh
new file mode 100755
index 0000000..51e22b4
--- /dev/null
+++ b/cmd/mgmt/shell/lib/shell_test.sh
@@ -0,0 +1,273 @@
+#!/bin/bash
+
+# The shell_test library is used to execute shell tests.
+
+# IMPORTANT: If your script registers its own "trap" handler, that
+# handler must call shell_test::at_exit to clean up all temporary
+# files and directories created by this library.
+
+source "$(go list -f {{.Dir}} v.io/x/ref/cmd/mgmt)/shell/lib/shell.sh"
+
+trap shell_test::at_exit INT TERM EXIT
+
+# ensure that we print a 'FAIL' message unless shell_test::pass is
+# explicitly called.
+shell_test_EXIT_MESSAGE="FAIL_UNEXPECTED_EXIT"
+
+# default timeout values.
+shell_test_DEFAULT_SERVER_TIMEOUT="10"
+shell_test_DEFAULT_MESSAGE_TIMEOUT="30"
+
+# default binary dir and working dir.
+shell_test_BIN_DIR="${shell_test_BIN_DIR:-$(shell::tmp_dir)}"
+shell_test_WORK_DIR="$(shell::tmp_dir)"
+
+# shell_test::assert_eq GOT WANT LINENO checks that GOT == WANT and if
+# not, fails the test, outputting the offending line number.
+shell_test::assert_eq() {
+  local -r GOT="$1"
+  local -r WANT="$2"
+  local -r LINENO="$3"
+  if [[ "${GOT}" != "${WANT}" ]]; then
+    shell_test::fail "line ${LINENO}: expected '${GOT}' == '${WANT}'"
+  fi
+}
+
+# shell_test::assert_ge GOT WANT LINENO checks that GOT >= WANT and if
+# not, fails the test, outputting the given line number and error
+# message.
+shell_test::assert_ge() {
+  local -r GOT="$1"
+  local -r WANT="$2"
+  local -r LINENO="$3"
+  if [[ "${GOT}" -lt "${WANT}" ]]; then
+    shell_test::fail "line ${LINENO}: expected ${GOT} >= ${WANT}"
+  fi
+}
+
+# shell_test::assert_gt GOT WANT LINENO checks that GOT > WANT and if
+# not, fails the test, outputting the given line number and error
+# message.
+shell_test::assert_gt() {
+  local -r GOT="$1"
+  local -r WANT="$2"
+  local -r LINENO="$3"
+  if [[ "${GOT}" -le "${WANT}" ]]; then
+    shell_test::fail "line ${LINENO}: expected ${GOT} > ${WANT}"
+  fi
+}
+
+# shell_test::assert_le GOT WANT LINENO checks that GOT <= WANT and if
+# not, fails the test, outputting the given line number and error
+# message.
+shell_test::assert_le() {
+  local -r GOT="$1"
+  local -r WANT="$2"
+  local -r LINENO="$3"
+  if [[ "${GOT}" -gt "${WANT}" ]]; then
+    shell_test::fail "line ${LINENO}: expected ${GOT} <= ${WANT}"
+  fi
+}
+
+# shell_test::assert_lt GOT WANT LINENO checks that GOT < WANT and if
+# not, fails the test, outputting the given line number and error
+# message.
+shell_test::assert_lt() {
+  local -r GOT="$1"
+  local -r WANT="$2"
+  local -r LINENO="$3"
+  if [[ "${GOT}" -ge "${WANT}" ]]; then
+    shell_test::fail "line ${LINENO}: expected ${GOT} < ${WANT}"
+  fi
+}
+
+# shell_test::assert_ne GOT WANT LINENO checks that GOT != WANT and if
+# not, fails the test, outputting the offending line number.
+shell_test::assert_ne() {
+  local -r GOT="$1"
+  local -r WANT="$2"
+  local -r LINENO="$3"
+  if [[ "${GOT}" == "${WANT}" ]]; then
+    shell_test::fail "line ${LINENO}: expected '${GOT}' != '${WANT}'"
+  fi
+}
+
+# shell_test::assert_contains GOT WANT LINENO checks that GOT contains WANT and
+# if not, fails the test, outputting the offending line number.
+shell_test::assert_contains() {
+  local -r GOT="$1"
+  local -r WANT="$2"
+  local -r LINENO="$3"
+  if [[ ! "${GOT}" =~ "${WANT}" ]]; then
+    shell_test::fail "line ${LINENO}: expected '${GOT}' contains '${WANT}'"
+  fi
+}
+
+# shell_test::at_exit is executed when the shell script exits.
+shell_test::at_exit() {
+  echo "${shell_test_EXIT_MESSAGE}"
+  # Note: shell::at_exit unsets our trap, so it won't run again on exit.
+  shell::at_exit
+  shell::kill_child_processes
+}
+
+# shell_test::fail is used to identify a failing test.
+#
+# Example:
+#   <command> || shell_test::fail "line ${LINENO}: <error message>"
+shell_test::fail() {
+  [[ "$#" -gt 0 ]] && echo "$0 $*"
+  shell_test_EXIT_MESSAGE="FAIL"
+  exit 1
+}
+
+# shell_test::pass is used to identify a passing test.
+shell_test::pass() {
+  shell_test_EXIT_MESSAGE="PASS"
+  exit 0
+}
+
+# shell_test::build_go_binary builds the binary for the given go package ($1) with
+# the given output ($2) in $shell_test_BIN_DIR. Before building a binary, it will
+# check whether it already exists. If so, it will skip it.
+shell_test::build_go_binary() {
+  pushd "${shell_test_BIN_DIR}" > /dev/null
+  local -r PACKAGE="$1"
+  local OUTPUT="${2:-$(basename ${PACKAGE})}"
+  if [[ -f "${OUTPUT}" ]]; then
+    echo "${shell_test_BIN_DIR}/${OUTPUT}"
+    return
+  fi
+
+  go build -o "${OUTPUT}" "${PACKAGE}" 2>/dev/null || shell_test::fail "line ${LINENO}: failed to build ${OUTPUT}"
+  echo "${shell_test_BIN_DIR}/${OUTPUT}"
+  popd > /dev/null
+}
+
+# shell_test::setup_server_test is common boilerplate used for testing veyron
+# servers. In particular, this function sets up an instance of the mount table
+# daemon, and sets the NAMESPACE_ROOT environment variable accordingly.  It also
+# sets up credentials as needed.
+shell_test::setup_server_test() {
+  if [[ -n ${shell_test_RUNNING_UNDER_AGENT+1} ]]; then
+    shell_test::setup_server_test_agent
+  else
+    shell_test::setup_server_test_no_agent
+  fi
+}
+
+# shell_test::setup_mounttable brings up a mounttable and sets the
+# NAMESPACE_ROOT to point to it.
+shell_test::setup_mounttable() {
+  # Build the mount table daemon and related tools.
+  MOUNTTABLED_BIN="$(shell_test::build_go_binary 'v.io/x/ref/services/mounttable/mounttabled')"
+
+  # Start the mounttable daemon.
+  local -r MT_LOG=$(shell::tmp_file)
+  if [[ -n ${shell_test_RUNNING_UNDER_AGENT+1} ]]; then
+    shell::run_server "${shell_test_DEFAULT_SERVER_TIMEOUT}" "${MT_LOG}" "${MT_LOG}" "${VRUN}" "${MOUNTTABLED_BIN}" --veyron.tcp.address="127.0.0.1:0" &> /dev/null || (cat "${MT_LOG}" && shell_test::fail "line ${LINENO}: failed to start mounttabled")
+  else
+    shell::run_server "${shell_test_DEFAULT_SERVER_TIMEOUT}" "${MT_LOG}" "${MT_LOG}" "${MOUNTTABLED_BIN}" --veyron.tcp.address="127.0.0.1:0" &> /dev/null || (cat "${MT_LOG}" && shell_test::fail "line ${LINENO}: failed to start mounttabled")
+  fi
+  shell::timed_wait_for "${shell_test_DEFAULT_MESSAGE_TIMEOUT}" "${MT_LOG}" "Mount table service" || shell_test::fail "line ${LINENO}: failed to find expected output"
+
+  export NAMESPACE_ROOT=$(grep "Mount table service at:" "${MT_LOG}" | sed -e 's/^.*endpoint: //')
+  [[ -n "${NAMESPACE_ROOT}" ]] || shell_test::fail "line ${LINENO}: failed to read endpoint from logfile"
+}
+
+# shell_test::setup_server_test_no_agent does the work of setup_server_test when
+# not running under enable_agent (using credentials directories).
+shell_test::setup_server_test_no_agent() {
+  [[ ! -n ${shell_test_RUNNING_UNDER_AGENT+1} ]] \
+    || shell_test::fail "setup_server_test_no_agent called when running under enable_agent"
+  shell_test::setup_mounttable
+
+  # Create and use a new credentials directory /after/ the mount table is
+  # started so that servers or clients started after this point start with different
+  # credentials.
+  local -r TMP=${VEYRON_CREDENTIALS:-}
+  if [[ -z "${TMP}" ]]; then
+    export VEYRON_CREDENTIALS=$(shell::tmp_dir)
+  fi
+}
+
+# shell_test::setup_server_test_agent does the work of setup_server_test when
+# running under enable_agent.
+shell_test::setup_server_test_agent() {
+  [[ -n ${shell_test_RUNNING_UNDER_AGENT+1} ]] \
+    || shell_test::fail "setup_server_test_agent called when not running under enable_agent"
+  shell_test::setup_mounttable
+}
+
+# shell_test::start_server is used to start a server. The server is
+# considered started when it successfully mounts itself into a mount
+# table; an event that is detected using the shell::wait_for
+# function.
+#
+# Example:
+#   shell_test::start_server <server> <args>
+shell_test::start_server() {
+  START_SERVER_LOG_FILE=$(shell::tmp_file)
+  START_SERVER_PID=$(shell::run_server "${shell_test_DEFAULT_SERVER_TIMEOUT}" "${START_SERVER_LOG_FILE}" "${START_SERVER_LOG_FILE}" "$@" -logtostderr -vmodule=publisher=2)
+  shell::timed_wait_for "${shell_test_DEFAULT_MESSAGE_TIMEOUT}" "${START_SERVER_LOG_FILE}" "ipc pub: mount"
+}
+
+# shell_test::credentials is used to create a new VeyronCredentials directory.
+# The directory is initialized with a new principal that has a self-signed
+# blessing of the specified name set as default and shareable with all peers.
+#
+# Example:
+#   shell_test::credentials <self blessing name>
+shell_test::credentials() {
+  [[ ! -n ${shell_test_RUNNING_UNDER_AGENT+1} ]] \
+    || shell_test::fail "credentials called when running under enable_agent"
+  local -r PRINCIPAL_BIN="$(shell_test::build_go_binary 'v.io/x/ref/cmd/principal')"
+  local -r CRED=$(shell::tmp_dir)
+  "${PRINCIPAL_BIN}" create --overwrite=true "${CRED}" "$1" >/dev/null || shell_test::fail "line ${LINENO}: create failed"
+  echo "${CRED}"
+}
+
+# shell_test::forkcredentials is used to create a new VeyronCredentials directory
+# that is initialized with a principal blessed by the provided principal under the
+# provided extension.
+#
+# Example:
+#   shell_test::forkcredentials <blesser principal's VeyronCredentials> <extension>
+shell_test::forkcredentials() {
+  [[ ! -n ${shell_test_RUNNING_UNDER_AGENT+1} ]] \
+    || shell_test::fail "forkcredentials called when running under enable_agent"
+
+  local -r PRINCIPAL_BIN="$(shell_test::build_go_binary 'v.io/x/ref/cmd/principal')"
+  local -r FORKCRED=$(shell::tmp_dir)
+  "${PRINCIPAL_BIN}" create --overwrite=true "${FORKCRED}" self >/dev/null || shell_test::fail "line ${LINENO}: create failed"
+  "${PRINCIPAL_BIN}" --veyron.credentials="$1" bless --require_caveats=false "${FORKCRED}" "$2" >blessing || shell_test::fail "line ${LINENO}: bless failed"
+  "${PRINCIPAL_BIN}" --veyron.credentials="${FORKCRED}" store setdefault blessing || shell_test::fail "line ${LINENO}: store setdefault failed"
+  "${PRINCIPAL_BIN}" --veyron.credentials="${FORKCRED}" store set blessing ... || shell_test::fail "line ${LINENO}: store set failed"
+  echo "${FORKCRED}"
+}
+
+# shell_test::enable_agent causes the test to be executed under the security
+# agent.  It sets up the agent and then runs itself under that agent.
+#
+# Example:
+#   shell_test::enable_agent "$@"
+#   build() {
+#     ...
+#   }
+#   main() {
+#     build
+#     ...
+#     shell_test::pass
+#   }
+#   main "$@"
+shell_test::enable_agent() {
+  if [[ ! -n ${shell_test_RUNNING_UNDER_AGENT+1} ]]; then
+    local -r AGENTD="$(shell_test::build_go_binary 'v.io/x/ref/security/agent/agentd')"
+    local -r WORKDIR="${shell_test_WORK_DIR}"
+    export shell_test_RUNNING_UNDER_AGENT=1
+    VEYRON_CREDENTIALS="${WORKDIR}/credentials" exec ${AGENTD} --no_passphrase --additional_principals="${WORKDIR}/childcredentials" bash -"$-" "$0" "$@"
+    shell_test::fail "failed to run test under agent"
+  else
+    VRUN="$(shell_test::build_go_binary 'v.io/x/ref/cmd/vrun')"
+  fi
+}
diff --git a/cmd/mgmt/shell/lib/test.sh b/cmd/mgmt/shell/lib/test.sh
new file mode 100755
index 0000000..ccd7351
--- /dev/null
+++ b/cmd/mgmt/shell/lib/test.sh
@@ -0,0 +1,111 @@
+#!/bin/bash
+
+#
+# Unit tests for the shell functions in this directory
+#
+
+source "$(go list -f {{.Dir}} v.io/x/ref/cmd/mgmt)/shell/lib/shell_test.sh"
+
+test_assert() {
+  shell_test::assert_eq "foo" "foo" "${LINENO}"
+  shell_test::assert_eq "42" "42" "${LINENO}"
+  shell_test::assert_ge "1" "1" "${LINENO}"
+  shell_test::assert_ge "42" "1" "${LINENO}"
+  shell_test::assert_gt "42" "1" "${LINENO}"
+  shell_test::assert_le "1" "1" "${LINENO}"
+  shell_test::assert_le "1" "42" "${LINENO}"
+  shell_test::assert_lt "1" "42" "${LINENO}"
+  shell_test::assert_ne "1" "42" "${LINENO}"
+  shell_test::assert_ne "foo" "bar" "${LINENO}"
+}
+
+test_run_server() {
+  shell::run_server 1 /dev/null /dev/null foobar > /dev/null 2>&1
+  shell_test::assert_eq "$?" "1" "${LINENO}"
+
+  shell::run_server 1 /dev/null /dev/null sleep 10 > /dev/null 2>&1
+  shell_test::assert_eq "$?" "0" "${LINENO}"
+}
+
+test_timed_wait_for() {
+  local GOT WANT
+  local -r TMPFILE=$(shell::tmp_file)
+  touch "${TMPFILE}"
+
+  shell::timed_wait_for 2 "${TMPFILE}" "doesn't matter, it's all zeros anyway"
+  shell_test::assert_eq "$?" "1" "${LINENO}"
+
+  echo "foo" > "${TMPFILE}"
+  shell::timed_wait_for 2 "${TMPFILE}" "bar"
+  shell_test::assert_eq "$?" "1" "${LINENO}"
+
+  echo "bar" >> "${TMPFILE}"
+  echo "baz" >> "${TMPFILE}"
+  shell::timed_wait_for 2 "${TMPFILE}" "bar"
+  shell_test::assert_eq "$?" "0" "${LINENO}"
+}
+
+# rmpublickey replaces public keys (16 hex bytes, :-separated) with XX:....
+# This substitution enables comparison with golden output even when keys are freshly
+# minted by the "principal create" command.
+rmpublickey() {
+    sed -e "s/\([0-9a-f]\{2\}:\)\{15\}[0-9a-f]\{2\}/XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX/g"
+}
+
+test_credentials() {
+  local -r CRED=$(shell_test::credentials alice)
+
+  local -r PRINCIPAL_BIN="$(shell_test::build_go_binary 'v.io/x/ref/cmd/principal')"
+
+  "${PRINCIPAL_BIN}" --veyron.credentials="${CRED}" dump >alice.dump ||  shell_test::fail "line ${LINENO}: ${PRINCIPAL_BIN} dump ${CRED} failed"
+  cat alice.dump | rmpublickey >got || shell_test::fail "line ${LINENO}: cat alice.dump | rmpublickey failed"
+  cat >want <<EOF
+Public key : XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX
+---------------- BlessingStore ----------------
+Default blessings: alice
+Peer pattern                   : Blessings
+...                            : alice
+---------------- BlessingRoots ----------------
+Public key                                      : Pattern
+XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX : [alice]
+EOF
+  if ! diff got want; then
+    shell_test::fail "line ${LINENO}"
+  fi
+}
+
+test_forkcredentials() {
+  local -r CRED=$(shell_test::credentials alice)
+  local -r FORKCRED=$(shell_test::forkcredentials "${CRED}" fork)
+
+  local -r PRINCIPAL_BIN="$(shell_test::build_go_binary 'v.io/x/ref/cmd/principal')"
+
+  "${PRINCIPAL_BIN}" --veyron.credentials="${FORKCRED}" dump >alice.dump ||  shell_test::fail "line ${LINENO}: ${PRINCIPAL_BIN} dump ${CRED} failed"
+  cat alice.dump | rmpublickey >got || shell_test::fail "line ${LINENO}: cat alice.dump | rmpublickey failed"
+  cat >want <<EOF
+Public key : XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX
+---------------- BlessingStore ----------------
+Default blessings: alice/fork
+Peer pattern                   : Blessings
+...                            : alice/fork
+---------------- BlessingRoots ----------------
+Public key                                      : Pattern
+XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX : [alice]
+XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX : [self]
+EOF
+  if ! diff got want; then
+    shell_test::fail "line ${LINENO}"
+  fi
+}
+
+main() {
+  test_assert || shell_test::fail "assert"
+  test_run_server || shell_test::fail "test_run_server"
+  test_timed_wait_for  || shell_test::fail "test_run_server"
+  test_credentials  || shell_test::fail "test_credentials"
+  test_forkcredentials  || shell_test::fail "test_forkcredentials"
+
+  shell_test::pass
+}
+
+main "$@"
diff --git a/cmd/mgmt/suid_test.sh b/cmd/mgmt/suid_test.sh
new file mode 100755
index 0000000..febd73a
--- /dev/null
+++ b/cmd/mgmt/suid_test.sh
@@ -0,0 +1,345 @@
+#!/bin/bash
+
+# Test the device manager and related services and tools.
+#
+#
+# By default, this script tests the device manager in a fashion amenable
+# to automatic testing: the --single_user is passed to the device
+# manager so that all device manager components run as the same user and
+# no user input (such as an agent pass phrase) is needed.
+#
+# When this script is invoked with the --with_suid <user1> <user2> flag, it
+# installs the device manager in its more secure multi-account
+# configuration where the device manager runs under the account of <user1>
+# while test apps will be executed as <user2>. This mode will
+# require root permissions to install and may require configuring an
+# agent passphrase.
+#
+# For exanple:
+#
+#   ./suid_test.sh --with_suid devicemanager vana
+#
+# to test a device manager with multi-account support enabled for app
+# account vana.
+#
+
+# When running --with_suid, TMPDIR must grant the invoking user rwx
+# permissions and x permissions for all directories back to / for world.
+# Otherwise, the with_suid user will not be able to use absolute paths.
+# On Darwin, TMPDIR defaults to a directory hieararchy in /var that is
+# 0700. This is unworkable so force TMPDIR to /tmp in this case.
+WITH_SUID="${1:-no}"
+# TODO(caprita,rjkroege): Add logic to the integration test that verifies
+# installing and accessing packages from apps.  This would add coverage to the
+# package-related code in suid mode.
+if [[ "${WITH_SUID}" == "--with_suid" ]]; then
+  DEVMGR_USER="${2:?--with_suid requires a devicemgr user}"
+  SUID_USER="${3:?--with_suid requires a app user}"
+  SUDO_USER="root"
+  TMPDIR=/tmp
+  umask 066
+fi
+
+ source "$(go list -f {{.Dir}} v.io/x/ref/cmd/mgmt)/shell/lib/shell_test.sh"
+
+# Run the test under the security agent.
+shell_test::enable_agent "$@"
+
+readonly WORKDIR="${shell_test_WORK_DIR}"
+
+build() {
+  echo ">> Building binaries"
+  BINARYD_BIN="$(shell_test::build_go_binary 'v.io/x/ref/services/mgmt/binary/binaryd')"
+  BINARY_BIN="$(shell_test::build_go_binary 'v.io/x/ref/cmd/binary')"
+  APPLICATIOND_BIN="$(shell_test::build_go_binary 'v.io/x/ref/services/mgmt/application/applicationd')"
+  APPLICATION_BIN="$(shell_test::build_go_binary 'v.io/x/ref/cmd/application')"
+  AGENTD_BIN="$(shell_test::build_go_binary 'v.io/x/ref/security/agent/agentd')"
+  SUIDHELPER_BIN="$(shell_test::build_go_binary 'v.io/x/ref/services/mgmt/suidhelper')"
+  INITHELPER_BIN="$(shell_test::build_go_binary 'v.io/x/ref/services/mgmt/inithelper')"
+  DEVICEMANAGER_BIN="$(shell_test::build_go_binary 'v.io/x/ref/services/mgmt/device/deviced')"
+  DEVICE_BIN="$(shell_test::build_go_binary 'v.io/x/ref/cmd/mgmt/device')"
+  NAMESPACE_BIN="$(shell_test::build_go_binary 'v.io/x/ref/cmd/namespace')"
+  PRINCIPAL_BIN="$(shell_test::build_go_binary 'v.io/x/ref/cmd/principal')"
+  DEBUG_BIN="$(shell_test::build_go_binary 'v.io/x/ref/cmd/debug')"
+  DEVICE_SCRIPT="$(go list -f {{.Dir}} v.io/x/ref/cmd/mgmt/device)/devicex"
+}
+
+# TODO(caprita): Move to shell_tesh.sh
+
+###############################################################################
+# Waits until the given name appears in the mounttable, within a set timeout.
+# Arguments:
+#   path to namespace command-line tool
+#   timeout in seconds
+#   name to look up
+#   old mount entry value (if specified, waits until a different value appears)
+# Returns:
+#   0 if the name was successfully found, and 1 if the timeout expires before
+#   the name appears.
+#   Prints the new value of the mount entry.
+###############################################################################
+wait_for_mountentry() {
+  local -r NAMESPACE_BIN="$1"
+  local -r TIMEOUT="$2"
+  local -r NAME="$3"
+  local -r OLD_ENTRY="${4:+}"
+  for i in $(seq 1 "${TIMEOUT}"); do
+    local ENTRY=$("${NAMESPACE_BIN}" resolve "${NAME}" 2>/dev/null)
+    if [[ -n "${ENTRY}" && "${ENTRY}" != "${OLD_ENTRY}" ]]; then
+      echo ${ENTRY}
+      return 0
+    fi
+    sleep 1
+  done
+  echo "Timed out waiting for ${NAME} to have a mounttable entry different from ${OLD_ENTRY}."
+  return 1
+}
+
+###############################################################################
+# Waits until the given name disappears from the mounttable, within a set
+# timeout.
+# Arguments:
+#   path to namespace command-line tool
+#   timeout in seconds
+#   name to look up
+# Returns:
+#   0 if the name was gone from the mounttable, and 1 if the timeout expires
+#   while the name is still in the mounttable.
+###############################################################################
+wait_for_no_mountentry() {
+  local -r NAMESPACE_BIN="$1"
+  local -r TIMEOUT="$2"
+  local -r NAME="$3"
+  for i in $(seq 1 "${TIMEOUT}"); do
+    local ENTRY=$("${NAMESPACE_BIN}" resolve "${NAME}" 2>/dev/null)
+    if [[ -z "${ENTRY}" ]]; then
+      return 0
+    fi
+    sleep 1
+  done
+  echo "Timed out waiting for ${NAME} to disappear from the mounttable."
+  return 1
+}
+
+main() {
+  cd "${WORKDIR}"
+  build
+
+  local -r APPLICATIOND_NAME="applicationd"
+  local -r DEVICED_APP_NAME="${APPLICATIOND_NAME}/deviced/test"
+
+  BIN_STAGING_DIR="${WORKDIR}/bin"
+  mkdir -p "${BIN_STAGING_DIR}"
+  cp "${AGENTD_BIN}" "${SUIDHELPER_BIN}" "${INITHELPER_BIN}" "${DEVICEMANAGER_BIN}" "${BIN_STAGING_DIR}"
+  shell_test::setup_server_test
+
+  if [[ "${WITH_SUID}" == "--with_suid" ]]; then
+    chmod go+x "${WORKDIR}"
+  fi
+
+  echo ">> Installing and starting the device manager"
+  DM_INSTALL_DIR="${WORKDIR}/dm"
+
+  export VANADIUM_DEVICE_DIR="${DM_INSTALL_DIR}"
+
+  if [[ "${WITH_SUID}" != "--with_suid" ]]; then
+    local -r extra_arg="--single_user"
+  else
+    local -r extra_arg="--devuser=${DEVMGR_USER}"
+  fi
+
+  local -r NEIGHBORHOODNAME="$(hostname)-$$-${RANDOM}"
+  "${DEVICE_SCRIPT}" install "${BIN_STAGING_DIR}" \
+    ${extra_arg} \
+    --origin="${DEVICED_APP_NAME}" \
+    -- \
+    --veyron.tcp.address=127.0.0.1:0 \
+    --neighborhood_name="${NEIGHBORHOODNAME}"
+
+  "${VRUN}" "${DEVICE_SCRIPT}" start
+  local -r MT_NAME=devices/$(hostname)
+  MT_EP=$(wait_for_mountentry "${NAMESPACE_BIN}" 5 "${MT_NAME}")
+
+  # Verify that device manager's mounttable is published under the expected name
+  # (hostname).
+  shell_test::assert_ne "$("${NAMESPACE_BIN}" glob "${MT_NAME}")" "" "${LINENO}"
+
+  # Create a self-signed blessing with name "alice" and set it as default and
+  # shareable with all peers on the principal that this process is running
+  # as. This blessing will be used by all commands except those running under
+  # "vrun" which gets a principal forked from the process principal.
+  "${PRINCIPAL_BIN}" blessself alice > alice.bless || \
+    shell_test::fail "line ${LINENO}: blessself alice failed"
+  "${PRINCIPAL_BIN}" store setdefault alice.bless || \
+    shell_test::fail "line ${LINENO}: store setdefault failed"
+  "${PRINCIPAL_BIN}" store set alice.bless ... || \
+    shell_test::fail "line ${LINENO}: store set failed"
+
+  # Claim the device as "alice/myworkstation".
+  echo ">> Claiming the device manager"
+  "${DEVICE_BIN}" claim "${MT_NAME}/devmgr/device" myworkstation
+  # Wait for the device manager to re-mount after being claimed
+  MT_EP=$(wait_for_mountentry "${NAMESPACE_BIN}" 5 "${MT_NAME}" "${MT_EP}")
+
+  if [[ "${WITH_SUID}" == "--with_suid" ]]; then
+    echo ">> Verify that devicemanager has valid association for alice"
+    "${DEVICE_BIN}" associate add "${MT_NAME}/devmgr/device" "${SUID_USER}"  "alice"
+     shell_test::assert_eq   "$("${DEVICE_BIN}" associate list "${MT_NAME}/devmgr/device")" \
+       "alice ${SUID_USER}" "${LINENO}"
+     echo ">> Verify that devicemanager runs as ${DEVMGR_USER}"
+     local -r DPID=$("${DEBUG_BIN}" stats read \
+         "${MT_NAME}/devmgr/__debug/stats/system/pid" \
+         | awk '{print $2}')
+    # ps flags need to be different on linux
+    case "$(uname)" in
+    "Darwin")
+      local -r COMPUTED_DEVMGR_USER=$(ps -ej | \
+          awk '$2 ~'"${DPID}"' { print $1 }')
+      ;;
+    "Linux")
+      local -r COMPUTED_DEVMGR_USER=$(awk '/^Uid:/{print $2}' /proc/${DPID}/status | \
+          xargs getent passwd | awk -F: '{print $1}')
+      ;;
+     esac
+     shell_test::assert_eq "${COMPUTED_DEVMGR_USER}" \
+          "${DEVMGR_USER}" \
+          "${LINENO}"
+  fi
+
+  # Verify the device's default blessing is as expected.
+  shell_test::assert_contains "$("${DEBUG_BIN}" stats read "${MT_NAME}/devmgr/__debug/stats/security/principal/*/blessingstore" | head -1)" \
+    "Default blessings: alice/myworkstation" "${LINENO}"
+
+  # Get the device's profile.
+  local -r DEVICE_PROFILE=$("${DEVICE_BIN}" describe "${MT_NAME}/devmgr/device" | sed -e 's/{Profiles:map\[\(.*\):{}]}/\1/')
+
+  # Start a binary server under the blessing "alice/myworkstation/binaryd" so that
+  # the device ("alice/myworkstation") can talk to it.
+  local -r BINARYD_NAME="binaryd"
+  shell_test::start_server "${VRUN}" --name=myworkstation/binaryd "${BINARYD_BIN}" --name="${BINARYD_NAME}" \
+    --root_dir="${WORKDIR}/binstore" --veyron.tcp.address=127.0.0.1:0 --http=127.0.0.1:0 \
+    || shell_test::fail "line ${LINENO} failed to start binaryd"
+
+  # Upload a binary to the binary server.  The binary we upload is binaryd
+  # itself.
+  local -r SAMPLE_APP_BIN_NAME="${BINARYD_NAME}/testapp"
+  echo ">> Uploading ${SAMPLE_APP_BIN_NAME}"
+  "${BINARY_BIN}" upload "${SAMPLE_APP_BIN_NAME}" "${BINARYD_BIN}"
+
+  # Verify that the binary we uploaded is shown by glob.
+  shell_test::assert_eq "$("${NAMESPACE_BIN}" glob "${SAMPLE_APP_BIN_NAME}")" \
+    "${SAMPLE_APP_BIN_NAME}" "${LINENO}"
+
+  # Start an application server under the blessing "alice/myworkstation/applicationd" so that
+  # the device ("alice/myworkstation") can talk to it.
+  mkdir -p "${WORKDIR}/appstore"
+  shell_test::start_server "${VRUN}" --name=myworkstation/applicationd "${APPLICATIOND_BIN}" --name="${APPLICATIOND_NAME}" \
+    --store="${WORKDIR}/appstore" --veyron.tcp.address=127.0.0.1:0 \
+    || shell_test::fail "line ${LINENO} failed to start applicationd"
+
+  # Upload an envelope for our test app.
+  local -r SAMPLE_APP_NAME="${APPLICATIOND_NAME}/testapp/v0"
+  local -r APP_PUBLISH_NAME="testbinaryd"
+  echo ">> Uploading ${SAMPLE_APP_NAME}"
+  echo "{\"Title\":\"BINARYD\", \"Args\":[\"--name=${APP_PUBLISH_NAME}\", \"--root_dir=./binstore\", \"--veyron.tcp.address=127.0.0.1:0\"], \"Binary\":{\"File\":\"${SAMPLE_APP_BIN_NAME}\"}, \"Env\":[]}" > ./app.envelope
+  "${APPLICATION_BIN}" put "${SAMPLE_APP_NAME}" "${DEVICE_PROFILE}" ./app.envelope
+  rm ./app.envelope
+
+  # Verify that the envelope we uploaded shows up with glob.
+  shell_test::assert_eq "$("${APPLICATION_BIN}" match "${SAMPLE_APP_NAME}" "${DEVICE_PROFILE}" | grep Title | sed -e 's/^.*"Title": "'// | sed -e 's/",//')" \
+    "BINARYD" "${LINENO}"
+
+  # Install the app on the device.
+  echo ">> Installing ${SAMPLE_APP_NAME}"
+  local -r INSTALLATION_NAME=$("${DEVICE_BIN}" install "${MT_NAME}/devmgr/apps" "${SAMPLE_APP_NAME}" | sed -e 's/Successfully installed: "//' | sed -e 's/"//')
+
+  # Verify that the installation shows up when globbing the device manager.
+  shell_test::assert_eq "$("${NAMESPACE_BIN}" glob "${MT_NAME}/devmgr/apps/BINARYD/*")" \
+    "${INSTALLATION_NAME}" "${LINENO}"
+
+  # Start an instance of the app, granting it blessing extension myapp.
+  echo ">> Starting ${INSTALLATION_NAME}"
+  local -r INSTANCE_NAME=$("${DEVICE_BIN}" start "${INSTALLATION_NAME}" myapp | sed -e 's/Successfully started: "//' | sed -e 's/"//')
+  wait_for_mountentry "${NAMESPACE_BIN}" "5" "${MT_NAME}/${APP_PUBLISH_NAME}"
+
+  # Verify that the instance shows up when globbing the device manager.
+  shell_test::assert_eq "$("${NAMESPACE_BIN}" glob "${MT_NAME}/devmgr/apps/BINARYD/*/*")" "${INSTANCE_NAME}" "${LINENO}"
+
+  if [[ "${WITH_SUID}" == "--with_suid" ]]; then
+    echo ">> Verifying that the app is actually running as the associated user"
+     local -r PID=$("${DEBUG_BIN}" stats read "${MT_NAME}/devmgr/apps/BINARYD/*/*/stats/system/pid"  | awk '{print $2}')
+    # ps flags need to be different on linux
+    case "$(uname)" in
+    "Darwin")
+      local -r COMPUTED_SUID_USER=$(ps -ej | awk '$2 ~'"${PID}"' { print $1 }')
+      ;;
+    "Linux")
+        local -r COMPUTED_SUID_USER=$(awk '/^Uid:/{print $2}' /proc/${PID}/status | \
+          xargs getent passwd | awk -F: '{print $1}')
+      ;;
+     esac
+     shell_test::assert_eq "${COMPUTED_SUID_USER}" "${SUID_USER}" "${LINENO}"
+ fi
+
+  # Verify the app's default blessing.
+  shell_test::assert_contains "$("${DEBUG_BIN}" stats read "${INSTANCE_NAME}/stats/security/principal/*/blessingstore" | head -1)" \
+    "Default blessings: alice/myapp/BINARYD" "${LINENO}"
+
+  # Stop the instance.
+  echo ">> Stopping ${INSTANCE_NAME}"
+  "${DEVICE_BIN}" stop "${INSTANCE_NAME}"
+
+  # Verify that logs, but not stats, show up when globbing the stopped instance.
+  shell_test::assert_eq "$("${NAMESPACE_BIN}" glob "${INSTANCE_NAME}/stats/...")" "" "${LINENO}"
+  shell_test::assert_ne "$("${NAMESPACE_BIN}" glob "${INSTANCE_NAME}/logs/...")" "" "${LINENO}"
+
+  # Upload a deviced binary.
+  local -r DEVICED_APP_BIN_NAME="${BINARYD_NAME}/deviced"
+  echo ">> Uploading ${DEVICEMANAGER_BIN}"
+  "${BINARY_BIN}" upload "${DEVICED_APP_BIN_NAME}" "${DEVICEMANAGER_BIN}"
+
+  # Upload a device manager envelope.
+  echo ">> Uploading ${DEVICED_APP_NAME}"
+  echo "{\"Title\":\"device manager\", \"Binary\":{\"File\":\"${DEVICED_APP_BIN_NAME}\"}}" > ./deviced.envelope
+  "${APPLICATION_BIN}" put "${DEVICED_APP_NAME}" "${DEVICE_PROFILE}" ./deviced.envelope
+  rm ./deviced.envelope
+  # Update the device manager.
+  echo ">> Updating device manager"
+  "${DEVICE_BIN}" update "${MT_NAME}/devmgr/device"
+  MT_EP=$(wait_for_mountentry "${NAMESPACE_BIN}" 5 "${MT_NAME}" "${MT_EP}")
+
+  # Verify that device manager's mounttable is still published under the
+  # expected name (hostname).
+  shell_test::assert_ne "$("${NAMESPACE_BIN}" glob "${MT_NAME}")" "" "${LINENO}"
+
+  # Revert the device manager.
+  echo ">> Reverting device manager"
+  "${DEVICE_BIN}" revert "${MT_NAME}/devmgr/device"
+  MT_EP=$(wait_for_mountentry "${NAMESPACE_BIN}" 5 "${MT_NAME}" "${MT_EP}")
+
+  # Verify that device manager's mounttable is still published under the
+  # expected name (hostname).
+  shell_test::assert_ne "$("${NAMESPACE_BIN}" glob "${MT_NAME}")" "" "${LINENO}"
+
+  # Verify that the local mounttable exists, and that the device manager, the
+  # global namespace, and the neighborhood are mounted on it.
+  shell_test::assert_ne $("${NAMESPACE_BIN}" resolve "${MT_EP}/devmgr") "" "${LINENO}"
+  shell_test::assert_eq $("${NAMESPACE_BIN}" resolve "${MT_EP}/global") "[alice/myworkstation]${NAMESPACE_ROOT}" "${LINENO}"
+  shell_test::assert_ne $("${NAMESPACE_BIN}" resolve "${MT_EP}/nh") "" "${LINENO}"
+
+  # Suspend the device manager.
+  "${DEVICE_BIN}" suspend "${MT_NAME}/devmgr/device"
+  wait_for_mountentry "${NAMESPACE_BIN}" "5" "${MT_NAME}" "${MT_EP}"
+
+  # Stop the device manager.
+  "${DEVICE_SCRIPT}" stop
+  wait_for_no_mountentry "${NAMESPACE_BIN}" "5" "${MT_NAME}"
+
+  "${DEVICE_SCRIPT}" uninstall
+  if [[ -n "$(ls -A "${VANADIUM_DEVICE_DIR}" 2>/dev/null)" ]]; then
+    shell_test::fail "${VANADIUM_DEVICE_DIR} is not empty"
+  fi
+  shell_test::pass
+}
+
+main "$@"
diff --git a/cmd/mgmt/v23_test.go b/cmd/mgmt/v23_test.go
new file mode 100644
index 0000000..7f45d21
--- /dev/null
+++ b/cmd/mgmt/v23_test.go
@@ -0,0 +1,25 @@
+// Copyright 2015 The Vanadium 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 file was auto-generated via go generate.
+// DO NOT UPDATE MANUALLY
+package mgmt_test
+
+import "testing"
+import "os"
+
+import "v.io/x/ref/lib/testutil"
+import "v.io/x/ref/lib/testutil/v23tests"
+
+func TestMain(m *testing.M) {
+	testutil.Init()
+	cleanup := v23tests.UseSharedBinDir()
+	r := m.Run()
+	cleanup()
+	os.Exit(r)
+}
+
+func TestV23NodeManager(t *testing.T) {
+	v23tests.RunTest(t, V23TestNodeManager)
+}
diff --git a/cmd/mgmt/vbash b/cmd/mgmt/vbash
new file mode 100755
index 0000000..a1614b9
--- /dev/null
+++ b/cmd/mgmt/vbash
@@ -0,0 +1,200 @@
+#!/bin/bash
+#
+# Starts up a shell running under the security agent.
+#
+# Specifically:
+#
+# 1. Fetches the binaries needed to set up the security environment.  If it
+# can't fetch fresh binaries, it issues a warning but proceeds with existing
+# binaries, if any.
+#
+# 2. Starts a shell under the agent, optionally fetching a remote blessing if
+# the principal is missing.
+#
+# Uses ~/.vbash to store its files, including binaries and the principal.
+#
+# Usage:
+#
+# # Builds and fetches binaries from local repository
+# ./vbash
+#
+# # Gets binaries from local filesystem
+# ./vbash /path/to/binaries
+#
+# # Gets binaries from HTTP server
+# ./vbash http://host/path
+#
+# Limitations:
+# Tested on goobuntu and OS X 10.9.5.
+
+set -e
+
+readonly BIN_PACKAGES=(v.io/x/ref/cmd/principal v.io/x/ref/security/agent/agentd)
+BIN_NAMES=(${BIN_PACKAGES[@]})
+for (( i=0; i<${#BIN_PACKAGES[@]}; i++ )); do
+  BIN_NAMES[$i]=$(basename "${BIN_PACKAGES[$i]}")
+done
+
+# TODO(caprita): share copy_binaries and get_binaries with nminstall.
+
+###############################################################################
+# Copies one binary from source to destination.
+# Arguments:
+#   name of the binary
+#   source dir of binary
+#   destination dir of binary
+# Returns:
+#   None
+###############################################################################
+copy_binary() {
+  local -r BIN_NAME="$1"
+  local -r BIN_SRC_DIR="$2"
+  local -r BIN_DEST_DIR="$3"
+  local -r SOURCE="${BIN_SRC_DIR}/${BIN_NAME}"
+  if [[ -x "${SOURCE}" ]]; then
+    local -r DESTINATION="${BIN_DEST_DIR}/${BIN_NAME}"
+    cp "${SOURCE}" "${DESTINATION}"
+    chmod 700 "${DESTINATION}"
+  else
+    echo "couldn't find ${SOURCE}"
+    return 1
+  fi
+}
+
+###############################################################################
+# Fetches binaries needed by device manager installation.
+# Globals:
+#   BIN_NAMES
+#   BIN_PACKAGES
+#   VANADIUM_ROOT
+# Arguments:
+#   destination for binaries
+#   source of binaries
+# Returns:
+#   None
+###############################################################################
+get_binaries() {
+  local -r BIN_INSTALL="$1"
+  local -r BIN_SOURCE="$2"
+
+  local bin_names_str=""
+  for bin_name in "${BIN_NAMES[@]}"; do
+    bin_names_str+=" ${bin_name}"
+  done
+
+  # If source is not specified, try to build latest version of the binaries and copy
+  # them from the repository.
+  if [[ -z "${BIN_SOURCE}" ]]; then
+    if [[ -z "${VANADIUM_ROOT}" ]]; then
+      echo 'WARNING: VANADIUM_ROOT is not specified, cannot build fresh binaries'
+      return
+    fi
+    local -r REPO_BIN_DIR="${VANADIUM_ROOT}/release/go/bin"
+    echo "Building and Fetching binaries:${bin_names_str} from build repository: ${REPO_BIN_DIR} ..."
+    for package in "${BIN_PACKAGES[@]}"; do
+       local bin_name=$(basename "${package}")
+       v23 go install "${package}" 2> /dev/null || echo "WARNING: Could not build binary: ${bin_name}"
+       # while the build failed, we still try to see if we can copy the binary from the build repository.
+       copy_binary "${bin_name}" "${REPO_BIN_DIR}" "${BIN_INSTALL}" || echo "WARNING: Could not copy binary: ${bin_name} from build repository: ${REPO_BIN_DIR}"
+    done
+    return
+  fi
+
+  # If the source is specified as an existing local filesystem path,
+  # look for the binaries there.
+  if [[ -d "${BIN_SOURCE}" ]]; then
+      echo "Fetching binaries:${bin_names_str} locally from: ${BIN_SOURCE} ..."
+      for bin_name in "${BIN_NAMES[@]}"; do
+        copy_binary "${bin_name}" "${BIN_SOURCE}" "${BIN_INSTALL}" || echo "WARNING: Could not copy binary: ${bin_name} from: ${BIN_SOURCE}"
+      done
+      return
+  fi
+
+  # If the source looks like a URL, use HTTP to fetch.
+  local -r URL_REGEXP='^(https?|ftp|file)://'
+  if [[ "${BIN_SOURCE}" =~ ${URL_REGEXP} ]]; then
+    echo "Fetching binaries:${bin_names_str} remotely from: ${BIN_SOURCE} ..."
+      for bin_name in "${BIN_NAMES[@]}"; do
+        local DEST="${BIN_INSTALL}/${bin_name}"
+        (curl -f -o "${DEST}" "${BIN_SOURCE}/${bin_name}" && chmod 700 "${DEST}") || echo "WARNING: Could not fetch binary: ${bin_name} from HTTP server: ${BIN_SOURCE}/${bin_name}"
+      done
+      return
+  fi
+
+  echo "WARNING: couldn't fetch binaries."
+}
+
+main() {
+  if [[ ! -z "${VBASH_INDICATOR}" ]]; then
+    echo "Disallowing running VBASH within VBASH."
+    echo "https://memegen.googleplex.com/5551020600983552"
+    exit 1
+  fi
+  export VBASH_INDICATOR="1"
+
+  local -r INSTALL_DIR="${HOME}/.vbash"
+  if [[ ! -e "${INSTALL_DIR}" ]]; then
+    mkdir -m 700 "${INSTALL_DIR}"
+  fi
+  if [[ ! -d "${INSTALL_DIR}" ]]; then
+    echo "${INSTALL_DIR} is not a directory!"
+    exit 1
+  fi
+
+  local -r BIN_INSTALL="${INSTALL_DIR}/bin"
+  if [[ ! -e "${BIN_INSTALL}" ]]; then
+    mkdir -m 700 "${BIN_INSTALL}"
+  fi
+  if [[ ! -d "${BIN_INSTALL}" ]]; then
+    echo "${BIN_INSTALL} is not a directory!"
+    exit 1
+  fi
+
+  # Fetch the binaries.
+  local -r BIN_SOURCE="$1"
+  get_binaries "${BIN_INSTALL}" "${BIN_SOURCE}"
+  for bin_name in "${BIN_NAMES[@]}"; do
+    local BINARY="${BIN_INSTALL}/${bin_name}"
+    if [[ ! -s "${BINARY}" ]]; then
+      echo "${BINARY} is empty."
+      exit 1
+    fi
+  done
+  echo "Using binaries in ${BIN_INSTALL}."
+
+  # Set up the script to be run by the agent.  It first optionally seeks a
+  # blessing, and then runs an interactive shell.
+
+  local -r CREDENTIALS_DIR="${INSTALL_DIR}/principal"
+  if [[ ! -d "${CREDENTIALS_DIR}" ]]; then
+    SEEK_BLESSING="1"
+  fi
+
+  # Use a custom rcfile to optionally get a blessing and also to modify the
+  # shell prompt to include the default veyron blessing.
+  cat <<EOF > "${INSTALL_DIR}/rcfile"
+if [[ Darwin == "$(uname)" ]]; then
+  if [[ -r /etc/profile ]]; then . /etc/profile; fi
+  for n in ~/.bash_profile ~/.bash_login ~/.profile; do
+    if [[ -r "$n" ]]; then
+      . "$n"
+      break
+    fi
+  done
+else
+  if [[ -f ~/.bashrc ]]; then . ~/.bashrc; fi
+fi
+if [[ "${SEEK_BLESSING}" -eq "1" ]]; then
+  "${BIN_INSTALL}/principal" seekblessings
+fi
+GREENBOLD="\[\033[1;32m\]"
+default_blessing() {
+  "${BIN_INSTALL}/principal" dump | grep "Default blessings" | sed -e 's/Default blessings: //'
+}
+export PS1="\${PS1}(\${GREENBOLD}\$(default_blessing)\[\033[0m\])$ "
+EOF
+
+VEYRON_CREDENTIALS="${CREDENTIALS_DIR}" exec "${BIN_INSTALL}/agentd" --additional_principals="${CREDENTIALS_DIR}" bash --rcfile "${INSTALL_DIR}/rcfile"
+}
+
+main "$@"
diff --git a/cmd/mounttable/doc.go b/cmd/mounttable/doc.go
new file mode 100644
index 0000000..1a32f91
--- /dev/null
+++ b/cmd/mounttable/doc.go
@@ -0,0 +1,134 @@
+// This file was auto-generated via go generate.
+// DO NOT UPDATE MANUALLY
+
+/*
+The mounttable tool facilitates interaction with a Veyron mount table.
+
+Usage:
+   mounttable <command>
+
+The mounttable commands are:
+   glob        returns all matching entries in the mount table
+   mount       Mounts a server <name> onto a mount table
+   unmount     removes server <name> from the mount table
+   resolvestep takes the next step in resolving a name.
+   help        Display help for commands or topics
+Run "mounttable help [command]" for command usage.
+
+The global flags are:
+ -alsologtostderr=true
+   log to standard error as well as files
+ -log_backtrace_at=:0
+   when logging hits line file:N, emit a stack trace
+ -log_dir=
+   if non-empty, write log files to this directory
+ -logtostderr=false
+   log to standard error instead of files
+ -max_stack_buf_size=4292608
+   max size in bytes of the buffer to use for logging stack traces
+ -stderrthreshold=2
+   logs at or above this threshold go to stderr
+ -v=0
+   log level for V logs
+ -vanadium.i18n_catalogue=
+   18n catalogue files to load, comma separated
+ -veyron.credentials=
+   directory to use for storing security credentials
+ -veyron.namespace.root=[/ns.dev.v.io:8101]
+   local namespace root; can be repeated to provided multiple roots
+ -veyron.proxy=
+   object name of proxy service to use to export services across network
+   boundaries
+ -veyron.tcp.address=
+   address to listen on
+ -veyron.tcp.protocol=tcp
+   protocol to listen with
+ -veyron.vtrace.cache_size=1024
+   The number of vtrace traces to store in memory.
+ -veyron.vtrace.collect_regexp=
+   Spans and annotations that match this regular expression will trigger trace
+   collection.
+ -veyron.vtrace.dump_on_shutdown=true
+   If true, dump all stored traces on runtime shutdown.
+ -veyron.vtrace.sample_rate=0
+   Rate (from 0.0 to 1.0) to sample vtrace traces.
+ -vmodule=
+   comma-separated list of pattern=N settings for file-filtered logging
+
+Mounttable Glob
+
+returns all matching entries in the mount table
+
+Usage:
+   mounttable glob [<mount name>] <pattern>
+
+<mount name> is a mount name on a mount table.  Defaults to namespace root.
+<pattern> is a glob pattern that is matched against all the entries below the
+specified mount name.
+
+Mounttable Mount
+
+Mounts a server <name> onto a mount table
+
+Usage:
+   mounttable mount [flags] <mount name> <name> <ttl> [M|R]
+
+<mount name> is a mount name on a mount table.
+
+<name> is the rooted object name of the server.
+
+<ttl> is the TTL of the new entry. It is a decimal number followed by a unit
+suffix (s, m, h). A value of 0s represents an infinite duration.
+
+[M|R] are mount options. M indicates that <name> is a mounttable. R indicates
+that existing entries should be removed.
+
+The mounttable mount flags are:
+ -blessing_pattern=[]
+   blessing pattern that matches the blessings of the server being mounted. Can
+   be specified multiple times to add multiple patterns. If none is provided,
+   the server will be contacted to determine this value.
+
+Mounttable Unmount
+
+removes server <name> from the mount table
+
+Usage:
+   mounttable unmount <mount name> <name>
+
+<mount name> is a mount name on a mount table. <name> is the rooted object name
+of the server.
+
+Mounttable Resolvestep
+
+takes the next step in resolving a name.
+
+Usage:
+   mounttable resolvestep <mount name>
+
+<mount name> is a mount name on a mount table.
+
+Mounttable Help
+
+Help with no args displays the usage of the parent command.
+
+Help with args displays the usage of the specified sub-command or help topic.
+
+"help ..." recursively displays help for all commands and topics.
+
+The output is formatted to a target width in runes.  The target width is
+determined by checking the environment variable CMDLINE_WIDTH, falling back on
+the terminal width from the OS, falling back on 80 chars.  By setting
+CMDLINE_WIDTH=x, if x > 0 the width is x, if x < 0 the width is unlimited, and
+if x == 0 or is unset one of the fallbacks is used.
+
+Usage:
+   mounttable help [flags] [command/topic ...]
+
+[command/topic ...] optionally identifies a specific sub-command or help topic.
+
+The mounttable help flags are:
+ -style=text
+   The formatting style for help output, either "text" or "godoc".
+*/
+package main
diff --git a/cmd/mounttable/impl.go b/cmd/mounttable/impl.go
new file mode 100644
index 0000000..060d854
--- /dev/null
+++ b/cmd/mounttable/impl.go
@@ -0,0 +1,249 @@
+package main
+
+import (
+	"errors"
+	"fmt"
+	"time"
+
+	"v.io/v23"
+	"v.io/v23/context"
+	"v.io/v23/ipc"
+	"v.io/v23/naming"
+	"v.io/v23/options"
+	"v.io/v23/security"
+	"v.io/x/lib/cmdline"
+	"v.io/x/lib/vlog"
+)
+
+var cmdGlob = &cmdline.Command{
+	Run:      runGlob,
+	Name:     "glob",
+	Short:    "returns all matching entries in the mount table",
+	Long:     "returns all matching entries in the mount table",
+	ArgsName: "[<mount name>] <pattern>",
+	ArgsLong: `
+<mount name> is a mount name on a mount table.  Defaults to namespace root.
+<pattern> is a glob pattern that is matched against all the entries below the
+specified mount name.
+`,
+}
+
+func runGlob(cmd *cmdline.Command, args []string) error {
+	ctx, cancel := context.WithTimeout(gctx, time.Minute)
+	defer cancel()
+
+	if len(args) == 1 {
+		roots := v23.GetNamespace(ctx).Roots()
+		if len(roots) == 0 {
+			return errors.New("no namespace root")
+		}
+		args = append([]string{roots[0]}, args...)
+	}
+	if expected, got := 2, len(args); expected != got {
+		return cmd.UsageErrorf("glob: incorrect number of arguments, expected %d, got %d", expected, got)
+	}
+
+	name, pattern := args[0], args[1]
+	client := v23.GetClient(ctx)
+	call, err := client.StartCall(ctx, name, ipc.GlobMethod, []interface{}{pattern}, options.NoResolve{})
+	if err != nil {
+		return err
+	}
+	for {
+		var gr naming.VDLGlobReply
+		if err := call.Recv(&gr); err != nil {
+			break
+		}
+		switch v := gr.(type) {
+		case naming.VDLGlobReplyEntry:
+			fmt.Fprint(cmd.Stdout(), v.Value.Name)
+			for _, s := range v.Value.Servers {
+				fmt.Fprintf(cmd.Stdout(), " %s (TTL %s)", s.Server, time.Duration(s.TTL)*time.Second)
+			}
+			fmt.Fprintln(cmd.Stdout())
+		}
+	}
+	if err := call.Finish(); err != nil {
+		return err
+	}
+	return nil
+}
+
+type blessingPatterns struct {
+	list []security.BlessingPattern
+}
+
+func (bp *blessingPatterns) Set(s string) error {
+	bp.list = append(bp.list, security.BlessingPattern(s))
+	return nil
+}
+
+func (bp *blessingPatterns) String() string {
+	return fmt.Sprintf("%v", bp.list)
+}
+
+func (bp *blessingPatterns) Get() interface{} { return bp.list }
+
+var flagMountBlessingPatterns blessingPatterns
+
+var cmdMount = &cmdline.Command{
+	Run:      runMount,
+	Name:     "mount",
+	Short:    "Mounts a server <name> onto a mount table",
+	Long:     "Mounts a server <name> onto a mount table",
+	ArgsName: "<mount name> <name> <ttl> [M|R]",
+	ArgsLong: `
+<mount name> is a mount name on a mount table.
+
+<name> is the rooted object name of the server.
+
+<ttl> is the TTL of the new entry. It is a decimal number followed by a unit
+suffix (s, m, h). A value of 0s represents an infinite duration.
+
+[M|R] are mount options. M indicates that <name> is a mounttable. R indicates
+that existing entries should be removed.
+`,
+}
+
+func runMount(cmd *cmdline.Command, args []string) error {
+	got := len(args)
+	if got < 2 || got > 4 {
+		return cmd.UsageErrorf("mount: incorrect number of arguments, expected 2, 3, or 4, got %d", got)
+	}
+	name := args[0]
+	server := args[1]
+	var flags naming.MountFlag
+	var seconds uint32
+	if got >= 3 {
+		ttl, err := time.ParseDuration(args[2])
+		if err != nil {
+			return fmt.Errorf("TTL parse error: %v", err)
+		}
+		seconds = uint32(ttl.Seconds())
+	}
+	if got >= 4 {
+		for _, c := range args[3] {
+			switch c {
+			case 'M':
+				flags |= naming.MountFlag(naming.MT)
+			case 'R':
+				flags |= naming.MountFlag(naming.Replace)
+			}
+		}
+	}
+	ctx, cancel := context.WithTimeout(gctx, time.Minute)
+	defer cancel()
+	client := v23.GetClient(ctx)
+
+	patterns := flagMountBlessingPatterns.list
+	if len(patterns) == 0 {
+		var err error
+		if patterns, err = blessingPatternsFromServer(ctx, server); err != nil {
+			return err
+		}
+		vlog.Infof("Server at %q has blessings %v", name, patterns)
+	}
+	call, err := client.StartCall(ctx, name, "MountX", []interface{}{server, patterns, seconds, flags}, options.NoResolve{})
+	if err != nil {
+		return err
+	}
+	if err := call.Finish(); err != nil {
+		return err
+	}
+	fmt.Fprintln(cmd.Stdout(), "Name mounted successfully.")
+	return nil
+}
+
+var cmdUnmount = &cmdline.Command{
+	Run:      runUnmount,
+	Name:     "unmount",
+	Short:    "removes server <name> from the mount table",
+	Long:     "removes server <name> from the mount table",
+	ArgsName: "<mount name> <name>",
+	ArgsLong: `
+<mount name> is a mount name on a mount table.
+<name> is the rooted object name of the server.
+`,
+}
+
+func runUnmount(cmd *cmdline.Command, args []string) error {
+	if expected, got := 2, len(args); expected != got {
+		return cmd.UsageErrorf("unmount: incorrect number of arguments, expected %d, got %d", expected, got)
+	}
+	ctx, cancel := context.WithTimeout(gctx, time.Minute)
+	defer cancel()
+	client := v23.GetClient(ctx)
+	call, err := client.StartCall(ctx, args[0], "Unmount", []interface{}{args[1]}, options.NoResolve{})
+	if err != nil {
+		return err
+	}
+	if err := call.Finish(); err != nil {
+		return err
+	}
+	fmt.Fprintln(cmd.Stdout(), "Unmount successful or name not mounted.")
+	return nil
+}
+
+var cmdResolveStep = &cmdline.Command{
+	Run:      runResolveStep,
+	Name:     "resolvestep",
+	Short:    "takes the next step in resolving a name.",
+	Long:     "takes the next step in resolving a name.",
+	ArgsName: "<mount name>",
+	ArgsLong: `
+<mount name> is a mount name on a mount table.
+`,
+}
+
+func runResolveStep(cmd *cmdline.Command, args []string) error {
+	if expected, got := 1, len(args); expected != got {
+		return cmd.UsageErrorf("mount: incorrect number of arguments, expected %d, got %d", expected, got)
+	}
+	ctx, cancel := context.WithTimeout(gctx, time.Minute)
+	defer cancel()
+	client := v23.GetClient(ctx)
+	call, err := client.StartCall(ctx, args[0], "ResolveStep", []interface{}{}, options.NoResolve{})
+	if err != nil {
+		return err
+	}
+	var entry naming.VDLMountEntry
+	if err := call.Finish(&entry); err != nil {
+		return err
+	}
+	fmt.Fprintf(cmd.Stdout(), "Servers: %v Suffix: %q MT: %v\n", entry.Servers, entry.Name, entry.MT)
+	return nil
+}
+
+func root() *cmdline.Command {
+	cmdMount.Flags.Var(&flagMountBlessingPatterns, "blessing_pattern", "blessing pattern that matches the blessings of the server being mounted. Can be specified multiple times to add multiple patterns. If none is provided, the server will be contacted to determine this value.")
+	return &cmdline.Command{
+		Name:  "mounttable",
+		Short: "Tool for interacting with a Veyron mount table",
+		Long: `
+The mounttable tool facilitates interaction with a Veyron mount table.
+`,
+		Children: []*cmdline.Command{cmdGlob, cmdMount, cmdUnmount, cmdResolveStep},
+	}
+}
+
+func blessingPatternsFromServer(ctx *context.T, server string) ([]security.BlessingPattern, error) {
+	vlog.Infof("Contacting %q to determine the blessings presented by it", server)
+	ctx, cancel := context.WithTimeout(ctx, time.Minute)
+	defer cancel()
+	call, err := v23.GetClient(ctx).StartCall(ctx, server, ipc.ReservedSignature, nil)
+	if err != nil {
+		return nil, fmt.Errorf("Unable to extract blessings presented by %q: %v", server, err)
+	}
+	blessings, _ := call.RemoteBlessings()
+	if len(blessings) == 0 {
+		return nil, fmt.Errorf("No recognizable blessings presented by %q, it cannot be securely mounted", server)
+	}
+	// This translation between BlessingPattern and string is silly!
+	// Kill the BlessingPatterns type and make methods on that type
+	// functions instead!
+	patterns := make([]security.BlessingPattern, len(blessings))
+	for i := range blessings {
+		patterns[i] = security.BlessingPattern(blessings[i])
+	}
+	return patterns, nil
+}
diff --git a/cmd/mounttable/impl_test.go b/cmd/mounttable/impl_test.go
new file mode 100644
index 0000000..114428d
--- /dev/null
+++ b/cmd/mounttable/impl_test.go
@@ -0,0 +1,160 @@
+package main
+
+import (
+	"bytes"
+	"strings"
+	"testing"
+
+	"v.io/v23"
+	"v.io/v23/context"
+	"v.io/v23/ipc"
+	"v.io/v23/naming"
+	"v.io/v23/security"
+	"v.io/v23/services/mounttable"
+	"v.io/v23/services/security/access"
+	"v.io/x/lib/vlog"
+
+	"v.io/x/ref/lib/testutil"
+	_ "v.io/x/ref/profiles"
+)
+
+type server struct {
+	suffix string
+}
+
+func (s *server) Glob__(ctx ipc.ServerCall, pattern string) (<-chan naming.VDLGlobReply, error) {
+	vlog.VI(2).Infof("Glob() was called. suffix=%v pattern=%q", s.suffix, pattern)
+	ch := make(chan naming.VDLGlobReply, 2)
+	ch <- naming.VDLGlobReplyEntry{naming.VDLMountEntry{"name1", []naming.VDLMountedServer{{"server1", nil, 123}}, false}}
+	ch <- naming.VDLGlobReplyEntry{naming.VDLMountEntry{"name2", []naming.VDLMountedServer{{"server2", nil, 456}, {"server3", nil, 789}}, false}}
+	close(ch)
+	return ch, nil
+}
+
+func (s *server) Mount(_ ipc.ServerCall, server string, ttl uint32, flags naming.MountFlag) error {
+	vlog.VI(2).Infof("Mount() was called. suffix=%v server=%q ttl=%d", s.suffix, server, ttl)
+	return nil
+}
+
+func (s *server) MountX(_ ipc.ServerCall, server string, patterns []security.BlessingPattern, ttl uint32, flags naming.MountFlag) error {
+	vlog.VI(2).Infof("MountX() was called. suffix=%v servers=%q patterns=%v ttl=%d", s.suffix, server, patterns, ttl)
+	return nil
+}
+
+func (s *server) Unmount(_ ipc.ServerCall, server string) error {
+	vlog.VI(2).Infof("Unmount() was called. suffix=%v server=%q", s.suffix, server)
+	return nil
+}
+
+func (s *server) ResolveStep(ipc.ServerCall) (entry naming.VDLMountEntry, err error) {
+	vlog.VI(2).Infof("ResolveStep() was called. suffix=%v", s.suffix)
+	entry.Servers = []naming.VDLMountedServer{{"server1", nil, 123}}
+	entry.Name = s.suffix
+	return
+}
+
+func (s *server) ResolveStepX(ipc.ServerCall) (entry naming.VDLMountEntry, err error) {
+	vlog.VI(2).Infof("ResolveStepX() was called. suffix=%v", s.suffix)
+	entry.Servers = []naming.VDLMountedServer{{"server1", nil, 123}}
+	entry.Name = s.suffix
+	return
+}
+
+func (s *server) Delete(ipc.ServerCall, bool) error {
+	vlog.VI(2).Infof("Delete() was called. suffix=%v", s.suffix)
+	return nil
+}
+func (s *server) SetACL(ipc.ServerCall, access.TaggedACLMap, string) error {
+	vlog.VI(2).Infof("SetACL() was called. suffix=%v", s.suffix)
+	return nil
+}
+
+func (s *server) GetACL(ipc.ServerCall) (access.TaggedACLMap, string, error) {
+	vlog.VI(2).Infof("GetACL() was called. suffix=%v", s.suffix)
+	return nil, "", nil
+}
+
+type dispatcher struct {
+}
+
+func (d *dispatcher) Lookup(suffix string) (interface{}, security.Authorizer, error) {
+	return mounttable.MountTableServer(&server{suffix: suffix}), nil, nil
+}
+
+func startServer(t *testing.T, ctx *context.T) (ipc.Server, naming.Endpoint, error) {
+	dispatcher := new(dispatcher)
+	server, err := v23.NewServer(ctx)
+	if err != nil {
+		t.Errorf("NewServer failed: %v", err)
+		return nil, nil, err
+	}
+	endpoints, err := server.Listen(v23.GetListenSpec(ctx))
+	if err != nil {
+		t.Errorf("Listen failed: %v", err)
+		return nil, nil, err
+	}
+	if err := server.ServeDispatcher("", dispatcher); err != nil {
+		t.Errorf("ServeDispatcher failed: %v", err)
+		return nil, nil, err
+	}
+	return server, endpoints[0], nil
+}
+
+func stopServer(t *testing.T, server ipc.Server) {
+	if err := server.Stop(); err != nil {
+		t.Errorf("server.Stop failed: %v", err)
+	}
+}
+
+func TestMountTableClient(t *testing.T) {
+	var shutdown v23.Shutdown
+	gctx, shutdown = testutil.InitForTest()
+	defer shutdown()
+
+	server, endpoint, err := startServer(t, gctx)
+	if err != nil {
+		return
+	}
+	defer stopServer(t, server)
+	// Setup the command-line.
+	cmd := root()
+	var stdout, stderr bytes.Buffer
+	cmd.Init(nil, &stdout, &stderr)
+
+	// Test the 'glob' command.
+	if err := cmd.Execute([]string{"glob", naming.JoinAddressName(endpoint.String(), ""), "*"}); err != nil {
+		t.Fatalf("%v", err)
+	}
+	if expected, got := "name1 server1 (TTL 2m3s)\nname2 server2 (TTL 7m36s) server3 (TTL 13m9s)", strings.TrimSpace(stdout.String()); got != expected {
+		t.Errorf("Got %q, expected %q", got, expected)
+	}
+	stdout.Reset()
+
+	// Test the 'mount' command.
+	if err := cmd.Execute([]string{"mount", "server", naming.JoinAddressName(endpoint.String(), ""), "123s"}); err != nil {
+		t.Fatalf("%v", err)
+	}
+	if expected, got := "Name mounted successfully.", strings.TrimSpace(stdout.String()); got != expected {
+		t.Errorf("Got %q, expected %q", got, expected)
+	}
+	stdout.Reset()
+
+	// Test the 'unmount' command.
+	if err := cmd.Execute([]string{"unmount", "server", naming.JoinAddressName(endpoint.String(), "")}); err != nil {
+		t.Fatalf("%v", err)
+	}
+	if expected, got := "Unmount successful or name not mounted.", strings.TrimSpace(stdout.String()); got != expected {
+		t.Errorf("Got %q, expected %q", got, expected)
+	}
+	stdout.Reset()
+
+	// Test the 'resolvestep' command.
+	vlog.Infof("resovestep %s", naming.JoinAddressName(endpoint.String(), "name"))
+	if err := cmd.Execute([]string{"resolvestep", naming.JoinAddressName(endpoint.String(), "name")}); err != nil {
+		t.Fatalf("%v", err)
+	}
+	if expected, got := `Servers: [{server1 [] 123}] Suffix: "name" MT: false`, strings.TrimSpace(stdout.String()); got != expected {
+		t.Errorf("Got %q, expected %q", got, expected)
+	}
+	stdout.Reset()
+}
diff --git a/cmd/mounttable/main.go b/cmd/mounttable/main.go
new file mode 100644
index 0000000..03caf91
--- /dev/null
+++ b/cmd/mounttable/main.go
@@ -0,0 +1,23 @@
+// The following enables go generate to generate the doc.go file.
+//go:generate go run $VANADIUM_ROOT/release/go/src/v.io/x/lib/cmdline/testdata/gendoc.go .
+
+package main
+
+import (
+	"os"
+
+	"v.io/v23"
+	"v.io/v23/context"
+
+	_ "v.io/x/ref/profiles"
+)
+
+var gctx *context.T
+
+func main() {
+	var shutdown v23.Shutdown
+	gctx, shutdown = v23.Init()
+	exitCode := root().Main()
+	shutdown()
+	os.Exit(exitCode)
+}
diff --git a/cmd/namespace/doc.go b/cmd/namespace/doc.go
new file mode 100644
index 0000000..d91e249
--- /dev/null
+++ b/cmd/namespace/doc.go
@@ -0,0 +1,153 @@
+// This file was auto-generated via go generate.
+// DO NOT UPDATE MANUALLY
+
+/*
+The namespace tool facilitates interaction with the Veyron namespace.
+
+The namespace roots are set from the command line via veyron.namespace.root
+options or from environment variables that have a name starting with
+NAMESPACE_ROOT, e.g. NAMESPACE_ROOT, NAMESPACE_ROOT_2, NAMESPACE_ROOT_GOOGLE,
+etc. The command line options override the environment.
+
+Usage:
+   namespace <command>
+
+The namespace commands are:
+   glob        Returns all matching entries from the namespace
+   mount       Adds a server to the namespace
+   unmount     Removes a server from the namespace
+   resolve     Translates a object name to its object address(es)
+   resolvetomt Finds the address of the mounttable that holds an object name
+   help        Display help for commands or topics
+Run "namespace help [command]" for command usage.
+
+The global flags are:
+ -alsologtostderr=true
+   log to standard error as well as files
+ -log_backtrace_at=:0
+   when logging hits line file:N, emit a stack trace
+ -log_dir=
+   if non-empty, write log files to this directory
+ -logtostderr=false
+   log to standard error instead of files
+ -max_stack_buf_size=4292608
+   max size in bytes of the buffer to use for logging stack traces
+ -stderrthreshold=2
+   logs at or above this threshold go to stderr
+ -v=0
+   log level for V logs
+ -vanadium.i18n_catalogue=
+   18n catalogue files to load, comma separated
+ -veyron.credentials=
+   directory to use for storing security credentials
+ -veyron.namespace.root=[/ns.dev.v.io:8101]
+   local namespace root; can be repeated to provided multiple roots
+ -veyron.proxy=
+   object name of proxy service to use to export services across network
+   boundaries
+ -veyron.tcp.address=
+   address to listen on
+ -veyron.tcp.protocol=tcp
+   protocol to listen with
+ -veyron.vtrace.cache_size=1024
+   The number of vtrace traces to store in memory.
+ -veyron.vtrace.collect_regexp=
+   Spans and annotations that match this regular expression will trigger trace
+   collection.
+ -veyron.vtrace.dump_on_shutdown=true
+   If true, dump all stored traces on runtime shutdown.
+ -veyron.vtrace.sample_rate=0
+   Rate (from 0.0 to 1.0) to sample vtrace traces.
+ -vmodule=
+   comma-separated list of pattern=N settings for file-filtered logging
+
+Namespace Glob
+
+Returns all matching entries from the namespace.
+
+Usage:
+   namespace glob <pattern>
+
+<pattern> is a glob pattern that is matched against all the names below the
+specified mount name.
+
+Namespace Mount
+
+Adds server <server> to the namespace with name <name>.
+
+Usage:
+   namespace mount [flags] <name> <server> <ttl>
+
+<name> is the name to add to the namespace. <server> is the object address of
+the server to add. <ttl> is the TTL of the new entry. It is a decimal number
+followed by a unit suffix (s, m, h). A value of 0s represents an infinite
+duration.
+
+The namespace mount flags are:
+ -blessing_pattern=[]
+   Blessing pattern that is matched by the blessings of the server being
+   mounted. If none is provided, the server will be contacted to determine this
+   value.
+
+Namespace Unmount
+
+Removes server <server> with name <name> from the namespace.
+
+Usage:
+   namespace unmount <name> <server>
+
+<name> is the name to remove from the namespace. <server> is the object address
+of the server to remove.
+
+Namespace Resolve
+
+Translates a object name to its object address(es).
+
+Usage:
+   namespace resolve [flags] <name>
+
+<name> is the name to resolve.
+
+The namespace resolve flags are:
+ -insecure=false
+   Insecure mode: May return results from untrusted servers and invoke Resolve
+   on untrusted mounttables
+
+Namespace Resolvetomt
+
+Finds the address of the mounttable that holds an object name.
+
+Usage:
+   namespace resolvetomt [flags] <name>
+
+<name> is the name to resolve.
+
+The namespace resolvetomt flags are:
+ -insecure=false
+   Insecure mode: May return results from untrusted servers and invoke Resolve
+   on untrusted mounttables
+
+Namespace Help
+
+Help with no args displays the usage of the parent command.
+
+Help with args displays the usage of the specified sub-command or help topic.
+
+"help ..." recursively displays help for all commands and topics.
+
+The output is formatted to a target width in runes.  The target width is
+determined by checking the environment variable CMDLINE_WIDTH, falling back on
+the terminal width from the OS, falling back on 80 chars.  By setting
+CMDLINE_WIDTH=x, if x > 0 the width is x, if x < 0 the width is unlimited, and
+if x == 0 or is unset one of the fallbacks is used.
+
+Usage:
+   namespace help [flags] [command/topic ...]
+
+[command/topic ...] optionally identifies a specific sub-command or help topic.
+
+The namespace help flags are:
+ -style=text
+   The formatting style for help output, either "text" or "godoc".
+*/
+package main
diff --git a/cmd/namespace/impl.go b/cmd/namespace/impl.go
new file mode 100644
index 0000000..8c6615f
--- /dev/null
+++ b/cmd/namespace/impl.go
@@ -0,0 +1,270 @@
+package main
+
+import (
+	"fmt"
+	"time"
+
+	"v.io/v23"
+	"v.io/v23/context"
+	"v.io/v23/ipc"
+	"v.io/v23/naming"
+	"v.io/v23/options"
+	"v.io/x/lib/cmdline"
+	"v.io/x/lib/vlog"
+)
+
+var (
+	flagInsecureResolve     bool
+	flagInsecureResolveToMT bool
+	flagMountBlessings      listFlag
+)
+
+var cmdGlob = &cmdline.Command{
+	Run:      runGlob,
+	Name:     "glob",
+	Short:    "Returns all matching entries from the namespace",
+	Long:     "Returns all matching entries from the namespace.",
+	ArgsName: "<pattern>",
+	ArgsLong: `
+<pattern> is a glob pattern that is matched against all the names below the
+specified mount name.
+`,
+}
+
+func runGlob(cmd *cmdline.Command, args []string) error {
+	if expected, got := 1, len(args); expected != got {
+		return cmd.UsageErrorf("glob: incorrect number of arguments, expected %d, got %d", expected, got)
+	}
+	pattern := args[0]
+
+	ctx, cancel := context.WithTimeout(gctx, time.Minute)
+	defer cancel()
+
+	ns := v23.GetNamespace(ctx)
+
+	c, err := ns.Glob(ctx, pattern)
+	if err != nil {
+		vlog.Infof("ns.Glob(%q) failed: %v", pattern, err)
+		return err
+	}
+	for res := range c {
+		switch v := res.(type) {
+		case *naming.MountEntry:
+			fmt.Fprint(cmd.Stdout(), v.Name)
+			for _, s := range v.Servers {
+				fmt.Fprintf(cmd.Stdout(), " %v%s (Expires %s)", fmtBlessingPatterns(s.BlessingPatterns), s.Server, s.Expires)
+			}
+			fmt.Fprintln(cmd.Stdout())
+		case *naming.GlobError:
+			fmt.Fprintf(cmd.Stderr(), "Error: %s: %v\n", v.Name, v.Error)
+		}
+	}
+	return nil
+}
+
+var cmdMount = &cmdline.Command{
+	Run:      runMount,
+	Name:     "mount",
+	Short:    "Adds a server to the namespace",
+	Long:     "Adds server <server> to the namespace with name <name>.",
+	ArgsName: "<name> <server> <ttl>",
+	ArgsLong: `
+<name> is the name to add to the namespace.
+<server> is the object address of the server to add.
+<ttl> is the TTL of the new entry. It is a decimal number followed by a unit
+suffix (s, m, h). A value of 0s represents an infinite duration.
+`,
+}
+
+func runMount(cmd *cmdline.Command, args []string) error {
+	if expected, got := 3, len(args); expected != got {
+		return cmd.UsageErrorf("mount: incorrect number of arguments, expected %d, got %d", expected, got)
+	}
+	name := args[0]
+	server := args[1]
+	ttlArg := args[2]
+
+	ttl, err := time.ParseDuration(ttlArg)
+	if err != nil {
+		return fmt.Errorf("TTL parse error: %v", err)
+	}
+
+	ctx, cancel := context.WithTimeout(gctx, time.Minute)
+	defer cancel()
+
+	blessings := flagMountBlessings.list
+	if len(blessings) == 0 {
+		// Obtain the blessings of the running server so it can be mounted with
+		// those blessings.
+		if blessings, err = blessingsOfRunningServer(ctx, server); err != nil {
+			return err
+		}
+		vlog.Infof("Server at %q has blessings %v", server, blessings)
+	}
+	ns := v23.GetNamespace(ctx)
+	if err = ns.Mount(ctx, name, server, ttl, naming.MountedServerBlessingsOpt(blessings)); err != nil {
+		vlog.Infof("ns.Mount(%q, %q, %s, %v) failed: %v", name, server, ttl, blessings, err)
+		return err
+	}
+	fmt.Fprintln(cmd.Stdout(), "Server mounted successfully.")
+	return nil
+}
+
+var cmdUnmount = &cmdline.Command{
+	Run:      runUnmount,
+	Name:     "unmount",
+	Short:    "Removes a server from the namespace",
+	Long:     "Removes server <server> with name <name> from the namespace.",
+	ArgsName: "<name> <server>",
+	ArgsLong: `
+<name> is the name to remove from the namespace.
+<server> is the object address of the server to remove.
+`,
+}
+
+func runUnmount(cmd *cmdline.Command, args []string) error {
+	if expected, got := 2, len(args); expected != got {
+		return cmd.UsageErrorf("unmount: incorrect number of arguments, expected %d, got %d", expected, got)
+	}
+	name := args[0]
+	server := args[1]
+
+	ctx, cancel := context.WithTimeout(gctx, time.Minute)
+	defer cancel()
+
+	ns := v23.GetNamespace(ctx)
+
+	if err := ns.Unmount(ctx, name, server); err != nil {
+		vlog.Infof("ns.Unmount(%q, %q) failed: %v", name, server, err)
+		return err
+	}
+	fmt.Fprintln(cmd.Stdout(), "Server unmounted successfully.")
+	return nil
+}
+
+var cmdResolve = &cmdline.Command{
+	Run:      runResolve,
+	Name:     "resolve",
+	Short:    "Translates a object name to its object address(es)",
+	Long:     "Translates a object name to its object address(es).",
+	ArgsName: "<name>",
+	ArgsLong: "<name> is the name to resolve.",
+}
+
+func runResolve(cmd *cmdline.Command, args []string) error {
+	if expected, got := 1, len(args); expected != got {
+		return cmd.UsageErrorf("resolve: incorrect number of arguments, expected %d, got %d", expected, got)
+	}
+	name := args[0]
+
+	ctx, cancel := context.WithTimeout(gctx, time.Minute)
+	defer cancel()
+
+	ns := v23.GetNamespace(ctx)
+
+	var opts []naming.ResolveOpt
+	if flagInsecureResolve {
+		opts = append(opts, options.SkipResolveAuthorization{})
+	}
+	me, err := ns.Resolve(ctx, name, opts...)
+	if err != nil {
+		vlog.Infof("ns.Resolve(%q) failed: %v", name, err)
+		return err
+	}
+	for i := range me.Servers {
+		fmt.Fprintln(cmd.Stdout(), fmtServer(me, i))
+	}
+	return nil
+}
+
+var cmdResolveToMT = &cmdline.Command{
+	Run:      runResolveToMT,
+	Name:     "resolvetomt",
+	Short:    "Finds the address of the mounttable that holds an object name",
+	Long:     "Finds the address of the mounttable that holds an object name.",
+	ArgsName: "<name>",
+	ArgsLong: "<name> is the name to resolve.",
+}
+
+func runResolveToMT(cmd *cmdline.Command, args []string) error {
+	if expected, got := 1, len(args); expected != got {
+		return cmd.UsageErrorf("resolvetomt: incorrect number of arguments, expected %d, got %d", expected, got)
+	}
+	name := args[0]
+
+	ctx, cancel := context.WithTimeout(gctx, time.Minute)
+	defer cancel()
+
+	ns := v23.GetNamespace(ctx)
+	var opts []naming.ResolveOpt
+	if flagInsecureResolveToMT {
+		opts = append(opts, options.SkipResolveAuthorization{})
+	}
+	e, err := ns.ResolveToMountTable(ctx, name, opts...)
+	if err != nil {
+		vlog.Infof("ns.ResolveToMountTable(%q) failed: %v", name, err)
+		return err
+	}
+	for i := range e.Servers {
+		fmt.Fprintln(cmd.Stdout(), fmtServer(e, i))
+	}
+	return nil
+}
+
+func root() *cmdline.Command {
+	cmdResolve.Flags.BoolVar(&flagInsecureResolve, "insecure", false, "Insecure mode: May return results from untrusted servers and invoke Resolve on untrusted mounttables")
+	cmdResolveToMT.Flags.BoolVar(&flagInsecureResolveToMT, "insecure", false, "Insecure mode: May return results from untrusted servers and invoke Resolve on untrusted mounttables")
+	cmdMount.Flags.Var(&flagMountBlessings, "blessing_pattern", "Blessing pattern that is matched by the blessings of the server being mounted. If none is provided, the server will be contacted to determine this value.")
+	return &cmdline.Command{
+		Name:  "namespace",
+		Short: "Tool for interacting with the Veyron namespace",
+		Long: `
+The namespace tool facilitates interaction with the Veyron namespace.
+
+The namespace roots are set from the command line via veyron.namespace.root options or from environment variables that have a name
+starting with NAMESPACE_ROOT, e.g. NAMESPACE_ROOT, NAMESPACE_ROOT_2,
+NAMESPACE_ROOT_GOOGLE, etc. The command line options override the environment.
+`,
+		Children: []*cmdline.Command{cmdGlob, cmdMount, cmdUnmount, cmdResolve, cmdResolveToMT},
+	}
+}
+
+func fmtServer(m *naming.MountEntry, idx int) string {
+	s := m.Servers[idx]
+	return fmt.Sprintf("%v%s", fmtBlessingPatterns(s.BlessingPatterns), naming.JoinAddressName(s.Server, m.Name))
+}
+
+func fmtBlessingPatterns(p []string) string {
+	if len(p) == 0 {
+		return ""
+	}
+	return fmt.Sprintf("%v", p)
+}
+
+func blessingsOfRunningServer(ctx *context.T, server string) ([]string, error) {
+	vlog.Infof("Contacting %q to determine the blessings presented by it", server)
+	ctx, cancel := context.WithTimeout(ctx, time.Minute)
+	defer cancel()
+	call, err := v23.GetClient(ctx).StartCall(ctx, server, ipc.ReservedSignature, nil)
+	if err != nil {
+		return nil, fmt.Errorf("Unable to extract blessings presented by %q: %v", server, err)
+	}
+	blessings, _ := call.RemoteBlessings()
+	if len(blessings) == 0 {
+		return nil, fmt.Errorf("No recognizable blessings presented by %q, it cannot be securely mounted", server)
+	}
+	return blessings, nil
+}
+
+type listFlag struct {
+	list []string
+}
+
+func (l *listFlag) Set(s string) error {
+	l.list = append(l.list, s)
+	return nil
+}
+
+func (l *listFlag) Get() interface{} { return l.list }
+
+func (l *listFlag) String() string { return fmt.Sprintf("%v", l.list) }
diff --git a/cmd/namespace/main.go b/cmd/namespace/main.go
new file mode 100644
index 0000000..03caf91
--- /dev/null
+++ b/cmd/namespace/main.go
@@ -0,0 +1,23 @@
+// The following enables go generate to generate the doc.go file.
+//go:generate go run $VANADIUM_ROOT/release/go/src/v.io/x/lib/cmdline/testdata/gendoc.go .
+
+package main
+
+import (
+	"os"
+
+	"v.io/v23"
+	"v.io/v23/context"
+
+	_ "v.io/x/ref/profiles"
+)
+
+var gctx *context.T
+
+func main() {
+	var shutdown v23.Shutdown
+	gctx, shutdown = v23.Init()
+	exitCode := root().Main()
+	shutdown()
+	os.Exit(exitCode)
+}
diff --git a/cmd/naming/simulator/commands.go b/cmd/naming/simulator/commands.go
new file mode 100644
index 0000000..30642b2
--- /dev/null
+++ b/cmd/naming/simulator/commands.go
@@ -0,0 +1,260 @@
+package main
+
+import (
+	"bytes"
+	"encoding/json"
+	"fmt"
+	"io"
+	"os"
+	"regexp"
+	"strings"
+
+	"v.io/x/ref/lib/modules"
+)
+
+type builtinCmd func(sh *modules.Shell, state *cmdState, args ...string) (string, error)
+
+var varRE = regexp.MustCompile("(.*?)=(.*)")
+
+var builtins = map[string]*struct {
+	nargs       int // -1 means a variable # of args.
+	usage       string
+	needsHandle bool
+	fn          builtinCmd
+}{
+	"print":       {-1, "print <args>...", false, print},
+	"help":        {-1, "help", false, nil},
+	"set":         {-1, "set <var>=<val>...", false, set},
+	"json_set":    {-1, "<var>...", false, json_set},
+	"json_print":  {0, "", false, json_print},
+	"splitEP":     {-1, "splitEP", false, splitEP},
+	"assert":      {2, "val1 val2", false, assert},
+	"assertOneOf": {-1, "val1 val...", false, assertOneOf},
+	"read":        {-1, "read <handle> [var]", true, read},
+	"eval":        {1, "eval <handle>", true, eval},
+	"wait":        {1, "wait <handle>", true, wait},
+	"stop":        {1, "stop <handle>", true, stop},
+	"stderr":      {1, "stderr <handle>", true, stderr},
+	"list":        {0, "list", false, list},
+	"quit":        {0, "quit", false, quit},
+}
+
+func init() {
+	builtins["help"].fn = help
+}
+
+func print(_ *modules.Shell, _ *cmdState, args ...string) (string, error) {
+	r := strings.Join(args, " ")
+	return r, nil
+}
+
+func splitEP(sh *modules.Shell, _ *cmdState, args ...string) (string, error) {
+	ep := strings.TrimLeft(args[0], "/")
+	ep = strings.TrimRight(ep, "/")
+	ep = strings.TrimLeft(ep, "@")
+	ep = strings.TrimRight(ep, "@")
+	parts := strings.Split(ep, "@")
+	sh.SetVar("PN", fmt.Sprintf("%d", len(parts)))
+	for i, p := range parts {
+		sh.SetVar(fmt.Sprintf("P%d", i), p)
+	}
+	return "", nil
+}
+
+func help(sh *modules.Shell, _ *cmdState, args ...string) (string, error) {
+	r := ""
+	if len(args) == 0 {
+		for k, _ := range builtins {
+			if k == "help" {
+				continue
+			}
+			r += k + ", "
+		}
+		r += sh.String()
+		return r, nil
+	} else {
+		for _, a := range args {
+			if v := builtins[a]; v != nil {
+				r += v.usage + "\n"
+				continue
+			}
+			h := sh.Help(a)
+			if len(h) == 0 {
+				return "", fmt.Errorf("unknown command: %q", a)
+			} else {
+				r += h
+			}
+		}
+	}
+	return r, nil
+}
+
+func parseVar(expr string) (string, string, error) {
+	m := varRE.FindAllStringSubmatch(expr, 1)
+	if len(m) != 1 || len(m[0]) != 3 {
+		return "", "", fmt.Errorf("%q is not an assignment statement", expr)
+	}
+	return m[0][1], m[0][2], nil
+}
+
+func set(sh *modules.Shell, _ *cmdState, args ...string) (string, error) {
+	r := ""
+	if len(args) == 0 {
+		for _, v := range sh.Env() {
+			r += v + "\n"
+		}
+		return r, nil
+	}
+	for _, a := range args {
+		k, v, err := parseVar(a)
+		if err != nil {
+			return "", err
+		}
+		sh.SetVar(k, v)
+	}
+	return "", nil
+}
+
+func assert(sh *modules.Shell, _ *cmdState, args ...string) (string, error) {
+	if args[0] != args[1] {
+		return "", fmt.Errorf("assertion failed: %q != %q", args[0], args[1])
+	}
+	return "", nil
+}
+
+func assertOneOf(sh *modules.Shell, _ *cmdState, args ...string) (string, error) {
+	if len(args) < 2 {
+		return "", fmt.Errorf("missing assertOneOf args")
+	}
+	expected := args[0]
+	for _, a := range args[1:] {
+		if a == expected {
+			return "", nil
+		}
+	}
+	return "", fmt.Errorf("assertion failed: %q not in %v", expected, args[1:])
+}
+
+func stderr(sh *modules.Shell, state *cmdState, args ...string) (string, error) {
+	state.Session.Finish(nil)
+	delete(handles, args[0])
+	return readStderr(state)
+}
+
+func readStderr(state *cmdState) (string, error) {
+	var b bytes.Buffer
+	if err := state.Handle.Shutdown(nil, &b); err != nil && err != io.EOF {
+		return b.String(), err
+	}
+	return b.String(), nil
+}
+
+func handleWrapper(sh *modules.Shell, fn builtinCmd, args ...string) (string, error) {
+	if len(args) < 1 {
+		return "", fmt.Errorf("missing handle argument")
+	}
+	state := handles[args[0]]
+	if state == nil {
+		return "", fmt.Errorf("invalid handle")
+	}
+	errstr := ""
+	r, err := fn(sh, state, args...)
+	if err != nil {
+		errstr, _ = readStderr(state)
+		errstr = strings.TrimSuffix(errstr, "\n")
+		if len(errstr) > 0 {
+			err = fmt.Errorf("%s: %v", errstr, err)
+		}
+	}
+	return r, err
+}
+
+func read(sh *modules.Shell, state *cmdState, args ...string) (string, error) {
+	l := state.Session.ReadLine()
+	for _, a := range args[1:] {
+		sh.SetVar(a, l)
+	}
+	return l, state.Session.OriginalError()
+}
+
+func eval(sh *modules.Shell, state *cmdState, args ...string) (string, error) {
+	l := state.Session.ReadLine()
+	if err := state.Session.OriginalError(); err != nil {
+		return l, err
+	}
+	k, v, err := parseVar(l)
+	if err != nil {
+		return "", err
+	}
+	sh.SetVar(k, v)
+	return l, nil
+}
+
+func stop(sh *modules.Shell, state *cmdState, args ...string) (string, error) {
+	state.Handle.CloseStdin()
+	return wait(sh, state, args...)
+}
+
+func wait(sh *modules.Shell, state *cmdState, args ...string) (string, error) {
+	// Read and return stdout
+	r, err := state.Session.Finish(nil)
+	delete(handles, args[0])
+	if err != nil {
+		return r, err
+	}
+	// Now read and return the contents of stderr as a string
+	if str, err := readStderr(state); err != nil && err != io.EOF {
+		return str, err
+	}
+	return r, nil
+}
+
+func list(sh *modules.Shell, _ *cmdState, args ...string) (string, error) {
+	r := ""
+	for h, v := range handles {
+		r += h + ": " + v.line + "\n"
+	}
+	return r, nil
+}
+
+func quit(sh *modules.Shell, _ *cmdState, args ...string) (string, error) {
+	r := ""
+	for k, h := range handles {
+		if err := h.Handle.Shutdown(os.Stdout, os.Stdout); err != nil {
+			r += fmt.Sprintf("%s: %v\n", k, err)
+		} else {
+			r += fmt.Sprintf("%s: ok\n", k)
+		}
+	}
+	fmt.Fprintf(os.Stdout, "%s\n", r)
+	os.Exit(0)
+	panic("unreachable")
+}
+
+func getLine(sh *modules.Shell, args ...string) (string, error) {
+	handle := handles[args[0]]
+	if handle == nil {
+		return "", fmt.Errorf("invalid handle")
+	}
+	l := handle.Session.ReadLine()
+	return l, handle.Session.Error()
+}
+
+func json_set(sh *modules.Shell, _ *cmdState, args ...string) (string, error) {
+	for _, k := range args {
+		if v, present := sh.GetVar(k); present {
+			jsonDict[k] = v
+		} else {
+			return "", fmt.Errorf("unrecognised variable: %q", k)
+		}
+	}
+	return "", nil
+}
+
+func json_print(sh *modules.Shell, _ *cmdState, args ...string) (string, error) {
+	bytes, err := json.Marshal(jsonDict)
+	if err != nil {
+		return "", err
+	}
+	return string(bytes), nil
+}
diff --git a/cmd/naming/simulator/driver.go b/cmd/naming/simulator/driver.go
new file mode 100644
index 0000000..6c6b33b
--- /dev/null
+++ b/cmd/naming/simulator/driver.go
@@ -0,0 +1,318 @@
+// This app provides a simple scripted environment for running common veyron
+// services as subprocesses and testing interactions between them. It is
+// structured as an interpreter, with global variables and variable
+// expansion, but no control flow. The command set that it supports is
+// extendable by adding new 'commands' that implement the API defined
+// by veyron/lib/modules.
+package main
+
+import (
+	"bufio"
+	"flag"
+	"fmt"
+	"os"
+	"strconv"
+	"strings"
+	"time"
+	"unicode"
+
+	"v.io/v23"
+	"v.io/v23/context"
+
+	"v.io/x/ref/lib/expect"
+	"v.io/x/ref/lib/modules"
+	_ "v.io/x/ref/lib/modules/core"
+	_ "v.io/x/ref/profiles"
+)
+
+type cmdState struct {
+	modules.Handle
+	*expect.Session
+	line string
+}
+
+var (
+	interactive bool
+	filename    string
+	handles     map[string]*cmdState
+	jsonDict    map[string]string
+)
+
+func init() {
+	flag.BoolVar(&interactive, "interactive", true, "set interactive/batch mode")
+	flag.StringVar(&filename, "file", "", "command file")
+	handles = make(map[string]*cmdState)
+	jsonDict = make(map[string]string)
+	flag.Usage = usage
+}
+
+var usage = func() {
+	fmt.Println(
+		`Welcome to this simple shell that lets you run mount tables, a simple server
+and sundry other commands from an interactive command line or as scripts. Type
+'help' at the prompt to see a list of available commands, or 'help command' to
+get specific help about that command. The shell provides environment variables
+with expansion and intrinsic support for managing subprocess, but it does not
+provide any flow control commands.
+
+All commands, except builtin ones (such as help, set, eval etc) are run
+asynchronously in background. That is, the prompt returns as soon as they are
+started and no output is displayed from them unless an error is encountered
+when they are being started. Each input line is numbered and that number is
+used to refer to the standard output of previous started commands. The variable
+_ always contains the number of the immediately preceeding line. It is
+possible to read the output of a command (using the 'read' builtin) and assign
+it that output to an environment variable. The 'eval' builtin parses output of
+the form <var>=<val>. In this way subproccess may be started, their output
+read and used to configure subsequent subprocesses. For example:
+
+1> time
+2> read 1 t
+3> print $t
+
+will print the first line of output from the time command, as will the
+following:
+
+or:
+time
+read $_ t
+print $t
+
+The eval builtin is used to directly to assign to variables specified
+in the output of the command. For example, if the root command
+prints out MT_NAME=foo then eval will set MT_NAME to foo as follows:
+
+root
+eval $_
+print $MT_NAME
+
+will print the value of MT_NAME that is output by the root command.
+`)
+	flag.PrintDefaults()
+}
+
+func prompt(lineno int) {
+	if interactive {
+		fmt.Printf("%d> ", lineno)
+	}
+}
+
+var ctx *context.T
+
+func main() {
+	var shutdown v23.Shutdown
+	ctx, shutdown = v23.Init()
+
+	input := os.Stdin
+	if len(filename) > 0 {
+		f, err := os.Open(filename)
+		if err != nil {
+			fmt.Fprintf(os.Stderr, "unexpected error: %s\n", err)
+			os.Exit(1)
+		}
+		input = f
+		interactive = false
+	}
+
+	// Subprocesses commands are run by fork/execing this binary
+	// so we must test to see if this instance is a subprocess or the
+	// the original command line instance.
+	if modules.IsModulesProcess() {
+		shutdown()
+		// Subprocess, run the requested command.
+		if err := modules.Dispatch(); err != nil {
+			fmt.Fprintf(os.Stderr, "failed: %v\n", err)
+			os.Exit(1)
+		}
+		return
+	}
+	defer shutdown()
+
+	shell, err := modules.NewShell(ctx, nil)
+	if err != nil {
+		fmt.Fprintf(os.Stderr, "unexpected error: %s\n", err)
+		os.Exit(1)
+	}
+	defer shell.Cleanup(os.Stderr, os.Stderr)
+
+	scanner := bufio.NewScanner(input)
+	lineno := 1
+	prompt(lineno)
+	for scanner.Scan() {
+		line := scanner.Text()
+		if !strings.HasPrefix(line, "#") && len(line) > 0 {
+			if line == "eof" {
+				break
+			}
+			if err := process(shell, line, lineno); err != nil {
+				fmt.Printf("ERROR: %d> %q: %v\n", lineno, line, err)
+				if !interactive {
+					os.Exit(1)
+				}
+			}
+		}
+		shell.SetVar("_", strconv.Itoa(lineno))
+		lineno++
+		prompt(lineno)
+	}
+	if err := scanner.Err(); err != nil {
+		fmt.Printf("error reading input: %v\n", err)
+	}
+
+}
+
+func output(lineno int, line string) {
+	if len(line) > 0 {
+		if !interactive {
+			fmt.Printf("%d> ", lineno)
+		}
+		line = strings.TrimSuffix(line, "\n")
+		fmt.Printf("%s\n", line)
+	}
+}
+
+func process(sh *modules.Shell, line string, lineno int) error {
+	fields, err := splitQuotedFields(line)
+	if err != nil {
+		return err
+	}
+	if len(fields) == 0 {
+		return fmt.Errorf("no input")
+	}
+	name := fields[0]
+
+	var args []string
+	if len(fields) > 1 {
+		args = fields[1:]
+	} else {
+		args = []string{}
+	}
+	sub, err := subVariables(sh, args)
+	if err != nil {
+		return err
+	}
+	if cmd := builtins[name]; cmd != nil {
+		if cmd.nargs >= 0 && len(sub) != cmd.nargs {
+			return fmt.Errorf("wrong (%d) # args for %q: usage %s", len(sub), name, cmd.usage)
+		}
+		l := ""
+		var err error
+		if cmd.needsHandle {
+			l, err = handleWrapper(sh, cmd.fn, sub...)
+		} else {
+			l, err = cmd.fn(sh, nil, sub...)
+		}
+		if err != nil {
+			return fmt.Errorf("%s : %s", err, l)
+		}
+		output(lineno, l)
+	} else {
+		handle, err := sh.Start(name, nil, sub...)
+		if err != nil {
+			return err
+		}
+		handles[strconv.Itoa(lineno)] = &cmdState{
+			handle,
+			expect.NewSession(nil, handle.Stdout(), 10*time.Minute),
+			line,
+		}
+		output(lineno, line)
+	}
+	return nil
+}
+
+// splitQuotedFields a line into fields, allowing for quoted strings.
+func splitQuotedFields(line string) ([]string, error) {
+	fields := []string{}
+	inquote := false
+	var field []rune
+	for _, c := range line {
+		switch {
+		case c == '"':
+			if inquote {
+				fields = append(fields, string(field))
+				field = nil
+				inquote = false
+			} else {
+				inquote = true
+			}
+		case unicode.IsSpace(c):
+			if inquote {
+				field = append(field, c)
+			} else {
+				if len(field) > 0 {
+					fields = append(fields, string(field))
+				}
+				field = nil
+			}
+		default:
+			field = append(field, c)
+		}
+	}
+	if inquote {
+		return nil, fmt.Errorf("unterminated quoted input")
+	}
+
+	if len(field) > 0 {
+		fields = append(fields, string(field))
+	}
+	return fields, nil
+}
+
+// subVariables substitutes variables that occur in the string slice
+// args with values from the Shell.
+func subVariables(sh *modules.Shell, args []string) ([]string, error) {
+	var results []string
+	for _, a := range args {
+		if r, err := subVariablesInArgument(sh, a); err != nil {
+			return results, err
+		} else {
+			results = append(results, r)
+		}
+	}
+	return results, nil
+}
+
+// subVariablesInArgument substitutes variables that occur in the string
+// parameter with values from vars.
+//
+// A variable, is introduced by $, terminated by \t, space, / , : or !.
+// Variables may also be enclosed by {} (as in ${VAR}) to allow for embedding
+// within strings.
+func subVariablesInArgument(sh *modules.Shell, a string) (string, error) {
+	first := strings.Index(a, "$")
+	if first < 0 {
+		return a, nil
+	}
+	parts := strings.Split(a, "$")
+	result := parts[0]
+	vn := ""
+	rem := 0
+	for _, p := range parts[1:] {
+		start := 0
+		end := -1
+		if strings.HasPrefix(p, "{") {
+			start = 1
+			end = strings.Index(p, "}")
+			if end < 0 {
+				return "", fmt.Errorf("unterminated variable: %q", p)
+			}
+			rem = end + 1
+		} else {
+			end = strings.IndexAny(p, "\t/,:!= ")
+			if end < 0 {
+				end = len(p)
+			}
+			rem = end
+		}
+		vn = p[start:end]
+		r := p[rem:]
+		v, present := sh.GetVar(vn)
+		if !present {
+			return a, fmt.Errorf("unknown variable: %q", vn)
+		}
+		result += v
+		result += r
+	}
+	return result, nil
+}
diff --git a/cmd/naming/simulator/driver_test.go b/cmd/naming/simulator/driver_test.go
new file mode 100644
index 0000000..f788ddd
--- /dev/null
+++ b/cmd/naming/simulator/driver_test.go
@@ -0,0 +1,88 @@
+package main
+
+import (
+	"fmt"
+	"reflect"
+	"testing"
+
+	"v.io/x/ref/lib/modules"
+)
+
+func TestFields(t *testing.T) {
+	cases := []struct {
+		input  string
+		output []string
+	}{
+		{"", []string{}},
+		{"a", []string{"a"}},
+		{"  z", []string{"z"}},
+		{"  zz  zz", []string{"zz", "zz"}},
+		{"ab", []string{"ab"}},
+		{"a b", []string{"a", "b"}},
+		{`a " b"`, []string{"a", " b"}},
+		{`a "  b  zz"`, []string{"a", "  b  zz"}},
+		{`a "  b		zz"`, []string{"a", "  b		zz"}},
+		{`a " b" cc`, []string{"a", " b", "cc"}},
+		{`a "z b" cc`, []string{"a", "z b", "cc"}},
+	}
+	for i, c := range cases {
+		got, err := splitQuotedFields(c.input)
+		if err != nil {
+			t.Errorf("%d: %q: unexpected error: %v", i, c.input, err)
+		}
+		if !reflect.DeepEqual(got, c.output) {
+			t.Errorf("%d: %q: got %#v, want %#v", i, c.input, got, c.output)
+		}
+	}
+	if _, err := splitQuotedFields(`a b "c`); err == nil {
+		t.Errorf("expected error for unterminated quote")
+	}
+}
+
+func TestVariables(t *testing.T) {
+	sh, err := modules.NewShell(nil, nil)
+	if err != nil {
+		t.Fatalf("unexpected error: %s", err)
+	}
+	defer sh.Cleanup(nil, nil)
+	sh.SetVar("foo", "bar")
+	cases := []struct {
+		input  string
+		output []string
+	}{
+		{"a b", []string{"a", "b"}},
+		{"a $foo", []string{"a", "bar"}},
+		{"$foo a", []string{"bar", "a"}},
+		{`a "$foo "`, []string{"a", "bar "}},
+		{"a xx$foo", []string{"a", "xxbar"}},
+		{"a xx${foo}yy", []string{"a", "xxbaryy"}},
+		{`a "foo"`, []string{"a", "foo"}},
+	}
+	for i, c := range cases {
+		fields, err := splitQuotedFields(c.input)
+		if err != nil {
+			t.Errorf("%d: %q: unexpected error: %v", i, c.input, err)
+		}
+		got, err := subVariables(sh, fields)
+		if err != nil {
+			t.Errorf("%d: %q: unexpected error: %v", i, c.input, err)
+		}
+		if !reflect.DeepEqual(got, c.output) {
+			t.Errorf("%d: %q: got %#v, want %#v", i, c.input, got, c.output)
+		}
+	}
+
+	errors := []struct {
+		input string
+		err   error
+	}{
+		{"$foox", fmt.Errorf("unknown variable: %q", "foox")},
+		{"${fo", fmt.Errorf("unterminated variable: %q", "{fo")},
+	}
+	for i, c := range errors {
+		vars, got := subVariables(sh, []string{c.input})
+		if (got == nil && c.err != nil) || got.Error() != c.err.Error() {
+			t.Errorf("%d: %q: expected error: got %v (with results %#v) want %v", i, c.input, got, vars, c.err)
+		}
+	}
+}
diff --git a/cmd/naming/simulator/shell_functions.go b/cmd/naming/simulator/shell_functions.go
new file mode 100644
index 0000000..935b362
--- /dev/null
+++ b/cmd/naming/simulator/shell_functions.go
@@ -0,0 +1,120 @@
+package main
+
+import (
+	"fmt"
+	"io"
+	"time"
+
+	"v.io/v23"
+	"v.io/v23/context"
+	"v.io/v23/naming"
+
+	"v.io/x/ref/lib/modules"
+)
+
+func init() {
+	modules.RegisterFunction("cache", `on|off
+turns the namespace cache on or off`, namespaceCache)
+	modules.RegisterFunction("mount", `<mountpoint> <server> <ttl> [M][R]
+invokes namespace.Mount(<mountpoint>, <server>, <ttl>)`, mountServer)
+	modules.RegisterFunction("resolve", `<name>
+resolves name to obtain an object server address`, resolveObject)
+	modules.RegisterFunction("resolveMT", `<name>
+resolves name to obtain a mount table address`, resolveMT)
+	modules.RegisterFunction("setRoots", `<name>...
+set the in-process namespace roots to <name>...`, setNamespaceRoots)
+}
+
+// checkArgs checks for the expected number of args in args. A negative
+// value means at least that number of args are expected.
+func checkArgs(args []string, expected int, usage string) error {
+	got := len(args)
+	if expected < 0 {
+		expected = -expected
+		if got < expected {
+			return fmt.Errorf("wrong # args (got %d, expected >=%d) expected: %q got: %v", got, expected, usage, args)
+		}
+	} else {
+		if got != expected {
+			return fmt.Errorf("wrong # args (got %d, expected %d) expected: %q got: %v", got, expected, usage, args)
+		}
+	}
+	return nil
+}
+
+func mountServer(stdin io.Reader, stdout, stderr io.Writer, env map[string]string, args ...string) error {
+	if err := checkArgs(args, -3, "<mount point> <server> <ttl> [M][R]"); err != nil {
+		return err
+	}
+	var opts []naming.MountOpt
+	for _, arg := range args[3:] {
+		for _, c := range arg {
+			switch c {
+			case 'R':
+				opts = append(opts, naming.ReplaceMountOpt(true))
+			case 'M':
+				opts = append(opts, naming.ServesMountTableOpt(true))
+			}
+		}
+	}
+	mp, server, ttlstr := args[0], args[1], args[2]
+	ttl, err := time.ParseDuration(ttlstr)
+	if err != nil {
+		return fmt.Errorf("failed to parse time from %q", ttlstr)
+	}
+	ns := v23.GetNamespace(ctx)
+	if err := ns.Mount(ctx, mp, server, ttl, opts...); err != nil {
+		return err
+	}
+	fmt.Fprintf(stdout, "Mount(%s, %s, %s, %v)\n", mp, server, ttl, opts)
+	return nil
+}
+
+func namespaceCache(stdin io.Reader, stdout, stderr io.Writer, env map[string]string, args ...string) error {
+	if err := checkArgs(args, 1, "on|off"); err != nil {
+		return err
+	}
+	disable := true
+	switch args[0] {
+	case "on":
+		disable = false
+	case "off":
+		disable = true
+	default:
+		return fmt.Errorf("arg must be 'on' or 'off'")
+	}
+	v23.GetNamespace(ctx).CacheCtl(naming.DisableCache(disable))
+	return nil
+}
+
+type resolver func(ctx *context.T, name string, opts ...naming.ResolveOpt) (me *naming.MountEntry, err error)
+
+func resolve(fn resolver, stdin io.Reader, stdout, stderr io.Writer, env map[string]string, args ...string) error {
+	if err := checkArgs(args, 1, "<name>"); err != nil {
+		return err
+	}
+	name := args[0]
+	me, err := fn(ctx, name)
+	if err != nil {
+		fmt.Fprintf(stdout, "RN=0\n")
+		return err
+	}
+	servers := me.Names()
+	fmt.Fprintf(stdout, "RN=%d\n", len(servers))
+	for i, s := range servers {
+		fmt.Fprintf(stdout, "R%d=%s\n", i, s)
+	}
+	return nil
+}
+
+func resolveObject(stdin io.Reader, stdout, stderr io.Writer, env map[string]string, args ...string) error {
+	return resolve(v23.GetNamespace(ctx).Resolve, stdin, stdout, stderr, env, args...)
+}
+
+func resolveMT(stdin io.Reader, stdout, stderr io.Writer, env map[string]string, args ...string) error {
+	return resolve(v23.GetNamespace(ctx).ResolveToMountTable, stdin, stdout, stderr, env, args...)
+}
+
+func setNamespaceRoots(stdin io.Reader, stdout, stderr io.Writer, env map[string]string, args ...string) error {
+	return v23.GetNamespace(ctx).SetRoots(args...)
+}
diff --git a/cmd/naming/simulator/simulator_v23_test.go b/cmd/naming/simulator/simulator_v23_test.go
new file mode 100644
index 0000000..57c86b1
--- /dev/null
+++ b/cmd/naming/simulator/simulator_v23_test.go
@@ -0,0 +1,55 @@
+package main_test
+
+//go:generate v23 test generate .
+
+import (
+	"bytes"
+	"fmt"
+	"io"
+	"io/ioutil"
+	"os"
+	"regexp"
+	"testing"
+
+	"v.io/x/ref/lib/testutil/v23tests"
+)
+
+//go:generate v23 test generate
+
+// HACK: This is a hack to force v23 test generate to generate modules.Dispatch in TestMain.
+// TODO(suharshs,cnicolaou): Find a way to get rid of this dummy subprocesses.
+func dummy(stdin io.Reader, stdout, stderr io.Writer, env map[string]string, args ...string) error {
+	return nil
+}
+
+func V23TestSimulator(t *v23tests.T) {
+	binary := t.BuildGoPkg("v.io/x/ref/cmd/naming/simulator")
+	files, err := ioutil.ReadDir("./testdata")
+	if err != nil {
+		t.Fatal(err)
+	}
+	scripts := []string{}
+	re := regexp.MustCompile(`.*\.scr`)
+	for _, f := range files {
+		if !f.IsDir() && re.MatchString(f.Name()) {
+			scripts = append(scripts, "./testdata/"+f.Name())
+		}
+	}
+	for _, script := range scripts {
+		if testing.Verbose() {
+			fmt.Fprintf(os.Stderr, "Script %v\n", script)
+		}
+		scriptFile, err := os.Open(script)
+		if err != nil {
+			t.Fatalf("Open(%q) failed: %v", script, err)
+		}
+		invocation := binary.WithStdin(scriptFile).Start()
+		var stdout, stderr bytes.Buffer
+		if err := invocation.Wait(&stdout, &stderr); err != nil {
+			fmt.Fprintf(os.Stderr, "Script %v failed\n", script)
+			fmt.Fprintln(os.Stderr, stdout.String())
+			fmt.Fprintln(os.Stderr, stderr.String())
+			t.Error(err)
+		}
+	}
+}
diff --git a/cmd/naming/simulator/testdata/ambiguity.scr b/cmd/naming/simulator/testdata/ambiguity.scr
new file mode 100644
index 0000000..a647015
--- /dev/null
+++ b/cmd/naming/simulator/testdata/ambiguity.scr
@@ -0,0 +1,67 @@
+# This is p's 'ambiguity' example
+#
+# mountMT("/s1/a", "/s2/b")
+# mountMT("/s2/b", "/s3/c")
+# mount("/s3/c", "/s4")
+#
+# Bogdan points out that: 'we will actually have d == "" in the echo example
+# below (since Serve only mounts endpoints without suffixes)'
+#
+# I'm not using any local names because its easier to not get confused
+# this way.
+#
+# resolve("/s1/a") can now have 4 possible objects, the mount point in server1,
+# the mount point in server2, the mount point in server3, and the object d in
+# server4.  When we make a call, like SetACL, how do we tell which one to
+# modify?   If we glob("/s1/a") we get:
+#
+# "s1/a", ["/s2/b"(mountpoint)]
+# "s1/a", ["/s3/c"(mountpoint)]
+# "s1/a", ["/s4"(mountpoint)]
+
+set localaddr="--veyron.tcp.address=127.0.0.1:0"
+
+root $localaddr
+set r=$_
+read $r
+eval $r
+set s1=$MT_NAME
+
+root $localaddr
+set r=$_
+read $r
+eval $r
+set s2=$MT_NAME
+
+root $localaddr
+set r=$_
+read $r
+eval $r
+set s3=$MT_NAME
+
+mount $s1/a $s2/b 1h
+wait $_
+
+mount $s2/b $s3/c 1h
+wait $_
+
+echoServer $localaddr "Echo" $s3/c
+set es_h=$_
+
+# Returns the root and three mounts at s1/a.
+ls $s1/...
+set l=$_
+eval $l
+assert $RN 5
+wait $l
+
+# Returns the mount at s1/a.
+ls $s1/a
+set l=$_
+eval $l
+assert $RN 1
+eval $l
+assert $R0 $s1/a
+wait $l
+
+stop $es_h
diff --git a/cmd/naming/simulator/testdata/echo.scr b/cmd/naming/simulator/testdata/echo.scr
new file mode 100644
index 0000000..4ca7435
--- /dev/null
+++ b/cmd/naming/simulator/testdata/echo.scr
@@ -0,0 +1,63 @@
+# Simple example to show how names work both without and with a mount table
+# and the difference between resolve and resolveMT.
+
+set localaddr=--veyron.tcp.address=127.0.0.1:0
+set ws=--veyron.tcp.protocol=ws
+
+setRoots
+
+# A 'stand-alone' server
+echoServer $localaddr $ws $localaddr "text" ""
+set es=$_
+read $es
+eval $es
+set esName=$NAME
+
+echoClient $esName "test"
+set ec=$_
+read $ec line
+assert $line "text: test"
+
+stop $es
+wait $ec
+
+# now use a nameserver.
+root $localaddr
+set r=$_
+read $r
+eval $r
+set root=$MT_NAME
+
+set NAMESPACE_ROOT=$root
+echoServer $localaddr $ws $localaddr "text2" "a/b"
+set es=$_
+read $es
+eval $es
+set es_name=$NAME
+
+echoClient "a/b" "test 2"
+set ec=$_
+read $ec line
+assert $line "text2: test 2"
+
+# resolve will return the server's address
+setRoots $root
+resolve a/b
+set r=$_
+eval $r
+assert $RN 2
+eval $r
+set ep1=$R0
+eval $r
+set ep2=$R1
+assertOneOf $es_name $ep1 $ep2
+
+# resolveMT will return the mount table's address (the part before the //)
+resolveMT a/b
+set r=$_
+eval $r
+assert $RN 1
+eval $r
+assert $R0 $root/a/b
+
+stop $es
diff --git a/cmd/naming/simulator/testdata/json_example.scr b/cmd/naming/simulator/testdata/json_example.scr
new file mode 100644
index 0000000..d399a30
--- /dev/null
+++ b/cmd/naming/simulator/testdata/json_example.scr
@@ -0,0 +1,29 @@
+
+cache off
+
+set localaddr=--veyron.tcp.address=127.0.0.1:0
+
+root $localaddr
+set root=$_
+eval $root
+set ROOT_PID=$PID
+eval $root
+set ROOT_NAME=$MT_NAME
+json_set ROOT_NAME ROOT_PID
+
+set PROXY_MOUNTPOINT=$ROOT_NAME/proxy/mp
+proxyd $localaddr $PROXY_MOUNTPOINT
+set proxyd=$_
+read $proxyd
+eval $proxyd
+set PROXYD_NAME=$PROXY_NAME
+splitEP $PROXY_NAME
+set PROXY_HOST_ADDR=$P2
+json_set PROXY_MOUNTPOINT PROXY_NAME PROXY_HOST_ADDR
+
+
+json_print
+
+# uncomment wait $root to have the script leave all of the processes running
+#wait $root
+stop $proxyd
diff --git a/cmd/naming/simulator/testdata/mt_complex.scr b/cmd/naming/simulator/testdata/mt_complex.scr
new file mode 100644
index 0000000..e2546eb
--- /dev/null
+++ b/cmd/naming/simulator/testdata/mt_complex.scr
@@ -0,0 +1,278 @@
+# Some more complex uses of mount tables and mounts
+#
+# TODO - list the examples and any issues.
+
+set localaddr="--veyron.tcp.address=127.0.0.1:0"
+set ws=--veyron.tcp.protocol=ws
+
+cache off
+
+root $localaddr
+set root_h=$_
+read $root_h
+eval $root_h
+set root=$MT_NAME
+
+set NAMESPACE_ROOT=$root
+mt $localaddr tl/a
+set m=$_
+set mt_a_h=$m
+read $m
+eval $m
+set mt_a_name=$MT_NAME
+
+mt $localaddr tl/b
+set m=$_
+set mt_b_h=$m
+eval $m
+eval $m
+set mt_b_name=$MT_NAME
+
+setRoots $root
+
+#
+# Using glob 'correctly' takes some care. There are (at least?) three
+# forms that you need to consider...
+#
+
+# ls ... finds all of the mount points, as relative names
+ls ...
+set l=$_
+eval $l
+assert $RN 3
+wait $l
+
+# ls /... is meaningless, finds nothing.
+ls /...
+set l=$_
+eval $l
+assert $RN 0
+wait $l
+
+# a rooted glob finds all of the mount points, include an entry for the root
+# itself. It returns rooted names.
+ls $root/...
+set l=$_
+eval $l
+assert $RN 4
+wait $l
+
+resolve tl/a
+set r=$_
+eval $r
+assert $RN 1
+eval $r
+set ep1=$R0
+assert $mt_a_name $ep1
+wait $r
+
+#
+# Now, let's run some echo servers, invoke rpc's on them, see what
+# glob and resolve do.
+#
+
+# run an echo server on tl.
+echoServer  $localaddr "E1" tl
+set es_E1=$_
+read $es_E1
+eval $es_E1
+set es_E1_name=$NAME
+
+# the echo server above, obscures the mount tables below it.
+# each of the ls (i.e. glob) calls below will lead to 'ipc:unknown method'
+# errors generated by the echo server.
+ls ...
+set l=$_
+eval $l
+assert $RN 2
+
+ls $root/...
+set l=$_
+eval $l
+assert $RN 3
+
+echoClient tl test
+read $_ o
+assert $o "E1: test"
+
+# resolve will find the address of the echo server.
+resolve tl
+set r=$_
+eval $r
+assert $RN 1
+eval $r
+set ep1=$R0
+assert $es_E1_name $ep1
+
+# let's have the echo server shut down
+stop $es_E1
+
+# and now, we can see the mount tables again.
+ls ...
+set l=$_
+eval $l
+assert $RN 3
+wait $l
+
+resolve tl/a
+set r=$_
+eval $r
+assert $RN 1
+eval $r
+set ep1=$R0
+assert $mt_a_name $ep1
+
+# run an echo server on tl/a - note that this currently doesn't seem to
+# have any effect on the mount table - that is, I suspect the mount table
+# refuses the mount.
+echoServer  $localaddr "E2" tl/a
+set es_E2=$_
+read $es_E2
+eval $es_E2
+set es_E2_name=$NAME
+
+# we can invoke the echo server 'E2' just fine, probably because
+# we just get lucky and get the most recently mounted address back first.
+#echoClient "tl/b" bar
+#read $_ o
+#assert $o "E2: bar"
+
+# but, when we resolve it's name, we get back two servers, one for the
+# mount table and another for the server!
+#resolve tl/a
+#set r=$_
+#eval $r
+#assert $RN 2
+#eval $r
+#set ep1=$R0
+#eval $r
+#set ep2=$R1
+#assertOneOf $mt_a_name $ep1 $ep2
+
+# resolveMT correctly returns the root's address.
+resolveMT tl/a
+set r=$_
+eval $r
+assert $RN 1
+eval $r
+assert $R0 $root/tl/a
+
+#
+# NOTE: I propose to fix the above ambiguity by having resolve only
+# ever return non-mountpoint servers. To do so, requires that the mount table
+# can tell them apart, which requires the separate Mount and MountMT calls.
+#
+
+# Mount the same server somewhere else
+# TODO(cnicolaou): $es_E2_name needs to be made into an endpoint.
+mount tl/a/c $es_E2_name 1h
+wait $_
+
+ls ...
+wait $_
+
+# this leads to 1 call of ResolveStep for //c on the echo server.
+resolve tl/a/c
+wait $_
+
+# this leads to 2 calls of ResolveStep for //c on the echo server.
+echoClient tl/a/c baz
+read $_ o
+assert $o "E2: baz"
+
+#
+# Can the spurious calls to ResolveStep above be avoided??
+#
+
+# Mount the same server with a really long name.
+set long_name=tl/b/x/y/z/really/long
+mount $long_name $es_E2_name 1h
+wait $_
+
+echoClient $long_name "long baz"
+read $_ o
+assert $o "E2: long baz"
+
+# This example just creates a 'pointer' into the Echo servers name space.
+# NOTE: do we really need this functionality?
+#
+# ResolveStep is again called on the server for //tl/b/x/y/z/really/long
+mount tl/b/short1 $es_E2_name/$long_name 1h
+wait $_
+
+echoClient tl/b/short1 "short baz"
+read $_ o
+assert $o E2.${long_name}": short baz"
+
+# Create a mount table with a 'long' name
+set long_name=tl/b/some/deep/name/that/is/a/mount/table
+mt $localaddr $long_name
+set m=$_
+read $m
+eval $m
+set mt_l_name=$MT_NAME
+
+
+# Create a second mount table with a 'long' name
+set second_long_name=tl/a/some/deep/name/that/is/a/mount/table
+mt $localaddr $second_long_name
+set m=$_
+read $m
+eval $m
+set mt_l2_name=$MT_NAME
+
+# Run an echo server that uses that mount table
+echoServer $localaddr "E3" $long_name/echo
+set es_E3=$_
+eval $es_E3
+set es_E3_name=$NAME
+
+echoClient $long_name/echo "long E3"
+read $_ o
+assert $o "E3: long E3"
+
+# make sure that the mount table is the one we expect.
+resolveMT $long_name/echo
+set r=$_
+eval $r
+assert $RN 1
+eval $r
+set ep1=$R0
+assert $mt_l_name/echo $ep1
+
+# Now, use mount directly to create a 'symlink'
+set symlink_target=some/deep/name/that/is/a/mount
+mount tl/b/symlink $mt_b_name/$symlink_target 1h M
+wait $_
+
+ls -l tl/b/symlink
+wait $_
+
+resolve tl/b/symlink
+set r=$_
+eval $r
+# used to return nothing since symlink is an 'interior' node.
+#assert $RN 0
+#
+# now we get the 'interior' returned.
+assert $RN 1
+eval $r
+assert $R0 $mt_b_name/$symlink_target
+# don't close or wait for this command since it'll error out.
+
+
+# resolveMT will return the original mount point
+resolveMT tl/b/symlink
+set r=$_
+eval $r
+assert $RN 1
+eval $r
+set ep1=$R0
+assert $mt_b_name/symlink $ep1
+
+stop $es_E3
+stop $es_E2
+
+quit
+
+
diff --git a/cmd/naming/simulator/testdata/mt_simple.scr b/cmd/naming/simulator/testdata/mt_simple.scr
new file mode 100644
index 0000000..9c5b5f2
--- /dev/null
+++ b/cmd/naming/simulator/testdata/mt_simple.scr
@@ -0,0 +1,101 @@
+# Simple example showing multiple mount tables, servers and globing
+
+set localaddr="--veyron.tcp.address=127.0.0.1:0"
+set ws=--veyron.tcp.protocol=ws
+
+root $localaddr
+set m=$_
+read $m
+eval $m
+set root=$MT_NAME
+
+set NAMESPACE_ROOT=$root
+mt $localaddr $ws $localaddr usa
+set m=$_
+read $m
+eval $m
+set usa_mt=$MT_NAME
+mt $localaddr $ws $localaddr uk
+set m=$_
+read $m
+eval $m
+set uk_mt=$MT_NAME
+
+ls $root/...
+set l=$_
+eval $l
+assert $RN 3
+wait $l
+
+set NAMESPACE_ROOT=$usa_mt
+mt $localaddr $ws $localaddr "palo alto"
+set m=$_
+read $m
+eval $m
+set pa_mt=$MT_NAME
+
+set NAMESPACE_ROOT=$uk_mt
+mt $localaddr $ws $localaddr "cambridge"
+set m=$_
+read $m
+eval $m
+set cam_mt=$MT_NAME
+
+ls $root/...
+set l=$_
+eval $l
+assert $RN 7
+wait $l
+
+ls -l $root/...
+wait $_
+
+resolve $root/usa
+set r=$_
+eval $r
+# We get two endpoints back, in arbitrary order
+# one of which is 'ws', the other 'tcp'
+assert $RN 2
+eval $r
+set ep1=$R0
+eval $r
+set ep2=$R1
+assertOneOf $usa_mt $ep1 $ep2
+wait $r
+
+resolve  "$root/usa/palo alto"
+set r=$_
+assert $RN 2
+eval $r
+# this resolves to the mount table hosting palo alto, not the mount table
+# that would host any objects mounted on .../palo alto/...
+# but the uk/cambridge example below seems to behave the opposite way?
+eval $r
+set ep1=$R0
+eval $r
+set ep2=$R1
+assertOneOf $pa_mt $ep1 $ep2
+wait $r
+
+resolve $root/uk
+set r=$_
+eval $r
+assert $RN 2
+eval $r
+set ep1=$R0
+eval $r
+set ep2=$R1
+assertOneOf $uk_mt $ep1 $ep2
+wait $r
+
+resolve "$root/uk/cambridge"
+set r=$_
+eval $r
+assert $RN 2
+eval $r
+set ep1=$R0
+eval $r
+set ep2=$R1
+# this behaves differently to the usa/palo alto case?
+assertOneOf $cam_mt $ep1 $ep2
+wait $r
diff --git a/cmd/naming/simulator/testdata/proxy.scr b/cmd/naming/simulator/testdata/proxy.scr
new file mode 100644
index 0000000..855879e
--- /dev/null
+++ b/cmd/naming/simulator/testdata/proxy.scr
@@ -0,0 +1,120 @@
+cache off
+
+set localaddr=--veyron.tcp.address=127.0.0.1:0
+set ws=--veyron.tcp.protocol=ws
+
+root $localaddr
+set m=$_
+read $m
+eval $m
+set root=$MT_NAME
+set NAMESPACE_ROOT=$root
+setRoots $NAMESPACE_ROOT
+print $NAMESPACE_ROOT
+
+# run a non-proxied echo server
+echoServer $localaddr $ws $localaddr noproxy echo/noproxy
+set esnp=$_
+read $esnp
+eval $esnp
+set NP_ECHOS_NAME=$NAME
+
+echoClient echo/noproxy "ohh"
+set ec=$_
+read $ec l
+assert $l "noproxy: ohh"
+
+# run a proxy server
+proxyd $localaddr $ws $localaddr p1
+set proxy=$_
+read $proxy
+# PROXY_NAME=<name of proxy>
+eval $proxy
+splitEP $PROXY_NAME
+assert $PN 7
+set PROXY_ADDR=$P2
+set PROXY_RID=$P3
+
+
+# TODO(cnicolaou): figure out why ls appears to run slowly when a proxy is
+# running, maybe a problem with the mount table.
+#ls ...
+#set l=$_
+#eval $l
+#assert $RN 3
+#wait $l
+
+echoServer $localaddr $ws $localaddr --veyron.proxy=p1 withproxy echo/withproxy
+set eswp=$_
+read $eswp
+eval $eswp
+set ECHOS_NAME=$NAME
+splitEP $NAME
+set ECHOS_RID=$P3
+
+echoClient echo/withproxy "ahh"
+set ec=$_
+read $ec l
+assert $l "withproxy: ahh"
+
+#ls ...
+#set l=$_
+#eval $l
+#assert $RN 4
+#wait $l
+
+print "root mt:" $NAMESPACE_ROOT
+print "proxy:" $PROXY_ADDR
+print "no proxy: " $NP_ECHOS_NAME
+print "with proxy: " $ECHOS_NAME
+# The ipc.Server implementation publishes the proxy supplied address and
+# the local address in the mount table
+
+resolve echo/withproxy
+set rs=$_
+stop $rs
+quit
+eval $rs
+# This will be 4 when ipc.Listen can return all of the endpoints in use,
+# then the proxy can return more than one address. We only see 3 endpoints
+# because the proxy server only returns one to the echo server.
+assert $RN 3
+
+eval $rs
+set ep1=$R0
+eval $rs
+set ep2=$R1
+eval $rs
+set ep3=$R2
+#eval $rs
+#set ep4=$R3
+
+splitEP $ep1
+assert $PN 7
+set ep1_addr=$P2
+set rid=$P3
+splitEP $ep2
+set ep2_addr=$P2
+splitEP $ep3
+set ep3_addr=$P2
+#splitEP $ep4
+#set ep4_addr=$P2
+
+assertOneOf $PROXY_ADDR $ep1_addr $ep2_addr $ep3_addr
+# $ep4_addr
+assert $rid $ECHOS_RID
+
+ls -l echo/withproxy
+wait $_
+ls -l echo/noproxy
+wait $_
+
+echoClient echo/withproxy "ohh"
+set ec=$_
+read $ec l
+assert $l "withproxy: ohh"
+
+stop $eswp
+stop $esnp
+stop $proxy
+
diff --git a/cmd/naming/simulator/testdata/public_echo.scr b/cmd/naming/simulator/testdata/public_echo.scr
new file mode 100644
index 0000000..5447fad
--- /dev/null
+++ b/cmd/naming/simulator/testdata/public_echo.scr
@@ -0,0 +1,32 @@
+# Simple script to test for interop between the current checked in
+# code here and the running public mount table.
+
+# TODO(cnicolaou): reenable this when our prod services are more reliable.
+quit
+
+set localaddr=--veyron.tcp.address=127.0.0.1:0
+
+# now use the global nameserver.
+
+set global_name=global/echo
+echoServer $localaddr "text2" $global_name
+set es=$_
+read $es
+eval $es
+set es_name=$NAME
+
+echoClient "$global_name" "test 2"
+set ec=$_
+read $ec line
+assert $line "text2: test 2"
+
+# resolve will return the server's address
+resolve $global_name
+set r=$_
+eval $r
+assert $RN 1
+eval $r
+set ep1=$R0
+assert $es_name $ep1
+
+stop $es
diff --git a/cmd/naming/simulator/v23_test.go b/cmd/naming/simulator/v23_test.go
new file mode 100644
index 0000000..491d9f7
--- /dev/null
+++ b/cmd/naming/simulator/v23_test.go
@@ -0,0 +1,39 @@
+// Copyright 2015 The Vanadium 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 file was auto-generated via go generate.
+// DO NOT UPDATE MANUALLY
+package main_test
+
+import "fmt"
+import "testing"
+import "os"
+
+import "v.io/x/ref/lib/modules"
+import "v.io/x/ref/lib/testutil"
+import "v.io/x/ref/lib/testutil/v23tests"
+
+func init() {
+	modules.RegisterChild("dummy", `HACK: This is a hack to force v23 test generate to generate modules.Dispatch in TestMain.
+TODO(suharshs,cnicolaou): Find a way to get rid of this dummy subprocesses.`, dummy)
+}
+
+func TestMain(m *testing.M) {
+	testutil.Init()
+	if modules.IsModulesProcess() {
+		if err := modules.Dispatch(); err != nil {
+			fmt.Fprintf(os.Stderr, "modules.Dispatch failed: %v\n", err)
+			os.Exit(1)
+		}
+		return
+	}
+	cleanup := v23tests.UseSharedBinDir()
+	r := m.Run()
+	cleanup()
+	os.Exit(r)
+}
+
+func TestV23Simulator(t *testing.T) {
+	v23tests.RunTest(t, V23TestSimulator)
+}
diff --git a/cmd/principal/bless.go b/cmd/principal/bless.go
new file mode 100644
index 0000000..bd7e84a
--- /dev/null
+++ b/cmd/principal/bless.go
@@ -0,0 +1,176 @@
+package main
+
+import (
+	"crypto/rand"
+	"encoding/base64"
+	"fmt"
+	"html/template"
+	"net"
+	"net/http"
+	"net/url"
+	"os"
+	"os/exec"
+	"strings"
+	"time"
+
+	"v.io/v23/context"
+	"v.io/v23/options"
+	"v.io/v23/security"
+	"v.io/x/lib/vlog"
+	"v.io/x/ref/services/identity"
+	"v.io/x/ref/services/identity/oauth"
+)
+
+func exchangeMacaroonForBlessing(ctx *context.T, macaroonChan <-chan string) (security.Blessings, error) {
+	service, macaroon, rootKey, err := prepareBlessArgs(ctx, macaroonChan)
+	if err != nil {
+		return security.Blessings{}, err
+	}
+
+	ctx, cancel := context.WithTimeout(ctx, time.Minute)
+	defer cancel()
+
+	// Authorize the server by its public key (obtained from macaroonChan).
+	// Must skip authorization during name resolution because the identity
+	// service is not a trusted root yet.
+	blessings, err := identity.MacaroonBlesserClient(service).Bless(ctx, macaroon, options.SkipResolveAuthorization{}, options.ServerPublicKey{rootKey})
+	if err != nil {
+		return security.Blessings{}, fmt.Errorf("failed to get blessing from %q: %v", service, err)
+	}
+	return blessings, nil
+}
+
+func prepareBlessArgs(ctx *context.T, macaroonChan <-chan string) (service, macaroon string, root security.PublicKey, err error) {
+	macaroon = <-macaroonChan
+	service = <-macaroonChan
+
+	marshalKey, err := base64.URLEncoding.DecodeString(<-macaroonChan)
+	if err != nil {
+		return "", "", nil, fmt.Errorf("failed to decode root key: %v", err)
+	}
+	root, err = security.UnmarshalPublicKey(marshalKey)
+	if err != nil {
+		return "", "", nil, fmt.Errorf("failed to unmarshal root key: %v", err)
+	}
+
+	return service, macaroon, root, nil
+}
+
+func getMacaroonForBlessRPC(blessServerURL string, blessedChan <-chan string, browser bool) (<-chan string, error) {
+	// Setup a HTTP server to recieve a blessing macaroon from the identity server.
+	// Steps:
+	// 1. Generate a state token to be included in the HTTP request
+	//    (though, arguably, the random port assigment for the HTTP server is enough
+	//    for XSRF protection)
+	// 2. Setup a HTTP server which will receive the final blessing macaroon from the id server.
+	// 3. Print out the link (to start the auth flow) for the user to click.
+	// 4. Return the macaroon and the rpc object name(where to make the MacaroonBlesser.Bless RPC call)
+	//    in the "result" channel.
+	var stateBuf [32]byte
+	if _, err := rand.Read(stateBuf[:]); err != nil {
+		return nil, fmt.Errorf("failed to generate state token for OAuth: %v", err)
+	}
+	state := base64.URLEncoding.EncodeToString(stateBuf[:])
+
+	ln, err := net.Listen("tcp", "127.0.0.1:0")
+	if err != nil {
+		return nil, fmt.Errorf("failed to setup authorization code interception server: %v", err)
+	}
+	result := make(chan string)
+
+	redirectURL := fmt.Sprintf("http://%s/macaroon", ln.Addr())
+	http.HandleFunc("/macaroon", func(w http.ResponseWriter, r *http.Request) {
+		w.Header().Set("Content-Type", "text/html")
+		tmplArgs := struct {
+			Blessings, ErrShort, ErrLong string
+		}{}
+		defer func() {
+			if len(tmplArgs.ErrShort) > 0 {
+				w.WriteHeader(http.StatusBadRequest)
+			}
+			if err := tmpl.Execute(w, tmplArgs); err != nil {
+				vlog.Info("Failed to render template:", err)
+			}
+		}()
+
+		toolState := r.FormValue("state")
+		if toolState != state {
+			tmplArgs.ErrShort = "Unexpected request"
+			tmplArgs.ErrLong = "Mismatched state parameter. Possible cross-site-request-forgery?"
+			return
+		}
+		result <- r.FormValue("macaroon")
+		result <- r.FormValue("object_name")
+		result <- r.FormValue("root_key")
+		defer close(result)
+		blessed, ok := <-blessedChan
+		if !ok {
+			tmplArgs.ErrShort = "No blessings received"
+			tmplArgs.ErrLong = "Unable to obtain blessings from the Veyron service"
+			return
+		}
+		tmplArgs.Blessings = blessed
+		ln.Close()
+	})
+	go http.Serve(ln, nil)
+
+	// Print the link to start the flow.
+	url, err := seekBlessingsURL(blessServerURL, redirectURL, state)
+	if err != nil {
+		return nil, fmt.Errorf("failed to create seekBlessingsURL: %s", err)
+	}
+	fmt.Fprintln(os.Stdout, "Please visit the following URL to seek blessings:")
+	fmt.Fprintln(os.Stdout, url)
+	// Make an attempt to start the browser as a convenience.
+	// If it fails, doesn't matter - the client can see the URL printed above.
+	// Use exec.Command().Start instead of exec.Command().Run since there is no
+	// need to wait for the command to return (and indeed on some window managers,
+	// the command will not exit until the browser is closed).
+	if len(openCommand) != 0 && browser {
+		exec.Command(openCommand, url).Start()
+	}
+	return result, nil
+}
+
+func seekBlessingsURL(blessServerURL, redirectURL, state string) (string, error) {
+	baseURL, err := url.Parse(joinURL(blessServerURL, oauth.SeekBlessingsRoute))
+	if err != nil {
+		return "", fmt.Errorf("failed to parse url: %v", err)
+	}
+	params := url.Values{}
+	params.Add("redirect_url", redirectURL)
+	params.Add("state", state)
+	baseURL.RawQuery = params.Encode()
+	return baseURL.String(), nil
+}
+
+func joinURL(baseURL, suffix string) string {
+	if !strings.HasSuffix(baseURL, "/") {
+		baseURL += "/"
+	}
+	return baseURL + suffix
+}
+
+var tmpl = template.Must(template.New("name").Parse(`<!doctype html>
+<html>
+<head>
+<meta charset="UTF-8">
+<title>Veyron Identity: Google</title>
+<meta name="viewport" content="width=device-width, initial-scale=1.0">
+<link rel="stylesheet" href="//netdna.bootstrapcdn.com/bootstrap/3.2.0/css/bootstrap.min.css">
+{{if .Blessings}}
+<!--Attempt to close the window. Though this script does not work on many browser configurations-->
+<script type="text/javascript">window.close();</script>
+{{end}}
+</head>
+<body>
+<div class="container">
+{{if .ErrShort}}
+<h1><span class="label label-danger">error</span>{{.ErrShort}}</h1>
+<div class="well">{{.ErrLong}}</div>
+{{else}}
+<h3>Received blessings: <tt>{{.Blessings}}</tt></h3>
+{{end}}
+</div>
+</body>
+</html>`))
diff --git a/cmd/principal/caveat.go b/cmd/principal/caveat.go
new file mode 100644
index 0000000..867e20c
--- /dev/null
+++ b/cmd/principal/caveat.go
@@ -0,0 +1,156 @@
+package main
+
+import (
+	"fmt"
+	"regexp"
+	"strings"
+
+	"v.io/v23/security"
+	"v.io/v23/vdl"
+	"v.io/x/ref/lib/vdl/build"
+	"v.io/x/ref/lib/vdl/compile"
+)
+
+// caveatsFlag defines a flag.Value for receiving multiple caveat definitions.
+type caveatsFlag struct {
+	caveatInfos []caveatInfo
+}
+
+type caveatInfo struct {
+	pkg, expr, params string
+}
+
+// Implements flag.Value.Get
+func (c caveatsFlag) Get() interface{} {
+	return c.caveatInfos
+}
+
+// Implements flag.Value.Set
+// Set expects s to be of the form:
+// caveatExpr=VDLExpressionOfParam
+func (c *caveatsFlag) Set(s string) error {
+	exprAndParam := strings.SplitN(s, "=", 2)
+	if len(exprAndParam) != 2 {
+		return fmt.Errorf("incorrect caveat format: %s", s)
+	}
+	expr, param := exprAndParam[0], exprAndParam[1]
+
+	// If the caveatExpr is of the form "path/to/package".ConstName we
+	// need to extract the package to later obtain the imports.
+	var pkg string
+	pkgre, err := regexp.Compile(`\A"(.*)"\.`)
+	if err != nil {
+		return fmt.Errorf("failed to parse pkgregex: %v", err)
+	}
+	match := pkgre.FindStringSubmatch(expr)
+	if len(match) == 2 {
+		pkg = match[1]
+	}
+
+	c.caveatInfos = append(c.caveatInfos, caveatInfo{pkg, expr, param})
+	return nil
+}
+
+// Implements flag.Value.String
+func (c caveatsFlag) String() string {
+	return fmt.Sprint(c.caveatInfos)
+}
+
+func (c caveatsFlag) usage() string {
+	return `"package/path".CaveatName:VDLExpressionParam to attach to this blessing`
+}
+
+func (c caveatsFlag) Compile() ([]security.Caveat, error) {
+	if len(c.caveatInfos) == 0 {
+		return nil, nil
+	}
+	var caveats []security.Caveat
+	env := compile.NewEnv(-1)
+
+	var pkgs []string
+	for _, info := range c.caveatInfos {
+		if len(info.pkg) > 0 {
+			pkgs = append(pkgs, info.pkg)
+		}
+	}
+	if err := buildPackages(pkgs, env); err != nil {
+		return nil, err
+	}
+
+	for _, info := range c.caveatInfos {
+		caveat, err := newCaveat(info, env)
+		if err != nil {
+			return nil, err
+		}
+		caveats = append(caveats, caveat)
+	}
+	return caveats, nil
+}
+
+func newCaveat(info caveatInfo, env *compile.Env) (security.Caveat, error) {
+	caveatDesc, err := compileCaveatDesc(info.pkg, info.expr, env)
+	if err != nil {
+		return security.Caveat{}, err
+	}
+	param, err := compileParams(info.params, caveatDesc.ParamType, env)
+	if err != nil {
+		return security.Caveat{}, err
+	}
+
+	return security.NewCaveat(caveatDesc, param)
+}
+
+func compileParams(paramData string, vdlType *vdl.Type, env *compile.Env) (interface{}, error) {
+	params := build.BuildExprs(paramData, []*vdl.Type{vdlType}, env)
+	if err := env.Errors.ToError(); err != nil {
+		return nil, fmt.Errorf("can't parse param data %s:\n%v", paramData, err)
+	}
+
+	return params[0], nil
+}
+
+func buildPackages(packages []string, env *compile.Env) error {
+	pkgs := build.TransitivePackages(packages, build.UnknownPathIsError, build.Opts{}, env.Errors)
+	if !env.Errors.IsEmpty() {
+		return fmt.Errorf("failed to get transitive packages packages: %s", env.Errors)
+	}
+	for _, p := range pkgs {
+		build.BuildPackage(p, env)
+		if !env.Errors.IsEmpty() {
+			return fmt.Errorf("failed to build package(%s): %s", p, env.Errors)
+		}
+	}
+	return nil
+}
+
+func compileCaveatDesc(pkg, expr string, env *compile.Env) (security.CaveatDescriptor, error) {
+	var vdlValue *vdl.Value
+	// In the case that the expr is of the form "path/to/package".ConstName we need to get the
+	// resolve the const name instead of building the expressions.
+	// TODO(suharshs,toddw): We need to fix BuildExprs so that both these cases will work with it.
+	// The issue is that BuildExprs returns a dummy file with no imports instead and errors out instead of
+	// looking at the imports in the env.
+	if len(pkg) > 0 {
+		spl := strings.SplitN(expr, "\".", 2)
+		constdef := env.ResolvePackage(pkg).ResolveConst(spl[1])
+		if constdef == nil {
+			return security.CaveatDescriptor{}, fmt.Errorf("failed to find caveat %v", expr)
+		}
+		vdlValue = constdef.Value
+	} else {
+		vdlValues := build.BuildExprs(expr, []*vdl.Type{vdl.TypeOf(security.CaveatDescriptor{})}, env)
+		if err := env.Errors.ToError(); err != nil {
+			return security.CaveatDescriptor{}, fmt.Errorf("can't build caveat desc %s:\n%v", expr, err)
+		}
+		if len(vdlValues) == 0 {
+			return security.CaveatDescriptor{}, fmt.Errorf("no caveat descriptors were built")
+		}
+		vdlValue = vdlValues[0]
+	}
+
+	var desc security.CaveatDescriptor
+	if err := vdl.Convert(&desc, vdlValue); err != nil {
+		return security.CaveatDescriptor{}, err
+	}
+	return desc, nil
+}
diff --git a/cmd/principal/doc.go b/cmd/principal/doc.go
new file mode 100644
index 0000000..c2799de
--- /dev/null
+++ b/cmd/principal/doc.go
@@ -0,0 +1,417 @@
+// This file was auto-generated via go generate.
+// DO NOT UPDATE MANUALLY
+
+/*
+The principal tool helps create and manage blessings and the set of trusted
+roots bound to a principal.
+
+All objects are printed using base64-VOM-encoding.
+
+Usage:
+   principal <command>
+
+The principal commands are:
+   create        Create a new principal and persist it into a directory
+   fork          Fork a new principal from the principal that this tool is
+                 running as and persist it into a directory
+   seekblessings Seek blessings from a web-based Veyron blessing service
+   recvblessings Receive blessings sent by another principal and use them as the
+                 default
+   dump          Dump out information about the principal
+   dumpblessings Dump out information about the provided blessings
+   blessself     Generate a self-signed blessing
+   bless         Bless another principal
+   store         Manipulate and inspect the principal's blessing store
+   help          Display help for commands or topics
+Run "principal help [command]" for command usage.
+
+The global flags are:
+ -alsologtostderr=true
+   log to standard error as well as files
+ -log_backtrace_at=:0
+   when logging hits line file:N, emit a stack trace
+ -log_dir=
+   if non-empty, write log files to this directory
+ -logtostderr=false
+   log to standard error instead of files
+ -max_stack_buf_size=4292608
+   max size in bytes of the buffer to use for logging stack traces
+ -stderrthreshold=2
+   logs at or above this threshold go to stderr
+ -v=0
+   log level for V logs
+ -vanadium.i18n_catalogue=
+   18n catalogue files to load, comma separated
+ -veyron.acl.file=map[]
+   specify an acl file as <name>:<aclfile>
+ -veyron.acl.literal=
+   explicitly specify the runtime acl as a JSON-encoded access.TaggedACLMap.
+   Overrides all --veyron.acl.file flags.
+ -veyron.credentials=
+   directory to use for storing security credentials
+ -veyron.namespace.root=[/ns.dev.v.io:8101]
+   local namespace root; can be repeated to provided multiple roots
+ -veyron.proxy=
+   object name of proxy service to use to export services across network
+   boundaries
+ -veyron.tcp.address=
+   address to listen on
+ -veyron.tcp.protocol=wsh
+   protocol to listen with
+ -veyron.vtrace.cache_size=1024
+   The number of vtrace traces to store in memory.
+ -veyron.vtrace.collect_regexp=
+   Spans and annotations that match this regular expression will trigger trace
+   collection.
+ -veyron.vtrace.dump_on_shutdown=true
+   If true, dump all stored traces on runtime shutdown.
+ -veyron.vtrace.sample_rate=0
+   Rate (from 0.0 to 1.0) to sample vtrace traces.
+ -vmodule=
+   comma-separated list of pattern=N settings for file-filtered logging
+
+Principal Create
+
+Creates a new principal with a single self-blessed blessing and writes it out to
+the provided directory. The same directory can then be used to set the
+VEYRON_CREDENTIALS environment variable for other veyron applications.
+
+The operation fails if the directory already contains a principal. In this case
+the --overwrite flag can be provided to clear the directory and write out the
+new principal.
+
+Usage:
+   principal create [flags] <directory> <blessing>
+
+	<directory> is the directory to which the new principal will be persisted.
+	<blessing> is the self-blessed blessing that the principal will be setup to use by default.
+
+The principal create flags are:
+ -overwrite=false
+   If true, any existing principal data in the directory will be overwritten
+
+Principal Fork
+
+Creates a new principal with a blessing from the principal specified by the
+environment that this tool is running in, and writes it out to the provided
+directory. The blessing that will be extended is the default one from the
+blesser's store, or specified by the --with flag. Expiration on the blessing are
+controlled via the --for flag. Additional caveats on the blessing are controlled
+with the --caveat flag. The blessing is marked as default and shareable with all
+peers on the new principal's blessing store.
+
+The operation fails if the directory already contains a principal. In this case
+the --overwrite flag can be provided to clear the directory and write out the
+forked principal.
+
+Usage:
+   principal fork [flags] <directory> <extension>
+
+	<directory> is the directory to which the forked principal will be persisted.
+	<extension> is the extension under which the forked principal is blessed.
+
+The principal fork flags are:
+ -caveat=[]
+   "package/path".CaveatName:VDLExpressionParam to attach to this blessing
+ -for=0
+   Duration of blessing validity (zero implies no expiration caveat)
+ -overwrite=false
+   If true, any existing principal data in the directory will be overwritten
+ -require_caveats=true
+   If false, allow blessing without any caveats. This is typically not advised
+   as the principal wielding the blessing will be almost as powerful as its
+   blesser
+ -with=
+   Path to file containing blessing to extend
+
+Principal Seekblessings
+
+Seeks blessings from a web-based Veyron blesser which requires the caller to
+first authenticate with Google using OAuth. Simply run the command to see what
+happens.
+
+The blessings are sought for the principal specified by the environment that
+this tool is running in.
+
+The blessings obtained are set as default, unless the --set_default flag is set
+to true, and are also set for sharing with all peers, unless a more specific
+peer pattern is provided using the --for_peer flag.
+
+Usage:
+   principal seekblessings [flags]
+
+The principal seekblessings flags are:
+ -add_to_roots=true
+   If true, the root certificate of the blessing will be added to the
+   principal's set of recognized root certificates
+ -browser=true
+   If false, the seekblessings command will not open the browser and only print
+   the url to visit.
+ -for_peer=...
+   If non-empty, the blessings obtained will be marked for peers matching this
+   pattern in the store
+ -from=https://auth.dev.v.io:8125/google
+   URL to use to begin the seek blessings process
+ -set_default=true
+   If true, the blessings obtained will be set as the default blessing in the
+   store
+
+Principal Recvblessings
+
+Allow another principal (likely a remote process) to bless this one.
+
+This command sets up the invoker (this process) to wait for a blessing from
+another invocation of this tool (remote process) and prints out the command to
+be run as the remote principal.
+
+The received blessings are set as default, unless the --set_default flag is set
+to true, and are also set for sharing with all peers, unless a more specific
+peer pattern is provided using the --for_peer flag.
+
+TODO(ashankar,cnicolaou): Make this next paragraph possible! Requires the
+ability to obtain the proxied endpoint.
+
+Typically, this command should require no arguments. However, if the sender and
+receiver are on different network domains, it may make sense to use the
+--veyron.proxy flag:
+    principal --veyron.proxy=proxy recvblessings
+
+The command to be run at the sender is of the form:
+    principal bless --remote_key=KEY --remote_token=TOKEN ADDRESS
+
+The --remote_key flag is used to by the sender to "authenticate" the receiver,
+ensuring it blesses the intended recipient and not any attacker that may have
+taken over the address.
+
+The --remote_token flag is used by the sender to authenticate itself to the
+receiver. This helps ensure that the receiver rejects blessings from senders who
+just happened to guess the network address of the 'recvblessings' invocation.
+
+Usage:
+   principal recvblessings [flags]
+
+The principal recvblessings flags are:
+ -for_peer=...
+   If non-empty, the blessings received will be marked for peers matching this
+   pattern in the store
+ -set_default=true
+   If true, the blessings received will be set as the default blessing in the
+   store
+
+Principal Dump
+
+Prints out information about the principal specified by the environment that
+this tool is running in.
+
+Usage:
+   principal dump
+
+Principal Dumpblessings
+
+Prints out information about the blessings (typically obtained from this tool)
+encoded in the provided file.
+
+Usage:
+   principal dumpblessings <file>
+
+<file> is the path to a file containing blessings typically obtained from this
+tool. - is used for STDIN.
+
+Principal Blessself
+
+Returns a blessing with name <name> and self-signed by the principal specified
+by the environment that this tool is running in. Optionally, the blessing can be
+restricted with an expiry caveat specified using the --for flag. Additional
+caveats can be added with the --caveat flag.
+
+Usage:
+   principal blessself [flags] [<name>]
+
+<name> is the name used to create the self-signed blessing. If not specified, a
+name will be generated based on the hostname of the machine and the name of the
+user running this command.
+
+The principal blessself flags are:
+ -caveat=[]
+   "package/path".CaveatName:VDLExpressionParam to attach to this blessing
+ -for=0
+   Duration of blessing validity (zero implies no expiration)
+
+Principal Bless
+
+Bless another principal.
+
+The blesser is obtained from the runtime this tool is using. The blessing that
+will be extended is the default one from the blesser's store, or specified by
+the --with flag. Expiration on the blessing are controlled via the --for flag.
+Additional caveats are controlled with the --caveat flag.
+
+For example, let's say a principal "alice" wants to bless another principal
+"bob" as "alice/friend", the invocation would be:
+    VEYRON_CREDENTIALS=<path to alice> principal bless <path to bob> friend
+and this will dump the blessing to STDOUT.
+
+With the --remote_key and --remote_token flags, this command can be used to
+bless a principal on a remote machine as well. In this case, the blessing is not
+dumped to STDOUT but sent to the remote end. Use 'principal help recvblessings'
+for more details on that.
+
+Usage:
+   principal bless [flags] <principal to bless> <extension>
+
+<principal to bless> represents the principal to be blessed (i.e., whose public
+key will be provided with a name).  This can be either: (a) The directory
+containing credentials for that principal, OR (b) The filename (- for STDIN)
+containing any other blessings of that
+    principal,
+OR (c) The object name produced by the 'recvblessings' command of this tool
+    running on behalf of another principal (if the --remote_key and
+    --remote_token flags are specified).
+
+<extension> is the string extension that will be applied to create the blessing.
+
+The principal bless flags are:
+ -caveat=[]
+   "package/path".CaveatName:VDLExpressionParam to attach to this blessing
+ -for=0
+   Duration of blessing validity (zero implies no expiration caveat)
+ -remote_key=
+   Public key of the remote principal to bless (obtained from the
+   'recvblessings' command run by the remote principal
+ -remote_token=
+   Token provided by principal running the 'recvblessings' command
+ -require_caveats=true
+   If false, allow blessing without any caveats. This is typically not advised
+   as the principal wielding the blessing will be almost as powerful as its
+   blesser
+ -with=
+   Path to file containing blessing to extend
+
+Principal Store
+
+Commands to manipulate and inspect the blessing store of the principal.
+
+All blessings are printed to stdout using base64-VOM-encoding
+
+Usage:
+   principal store <command>
+
+The principal store commands are:
+   default     Return blessings marked as default
+   setdefault  Set provided blessings as default
+   forpeer     Return blessings marked for the provided peer
+   set         Set provided blessings for peer
+   addtoroots  Add provided blessings to root set
+
+Principal Store Default
+
+Returns blessings that are marked as default in the BlessingStore specified by
+the environment that this tool is running in.
+
+Usage:
+   principal store default
+
+Principal Store Setdefault
+
+Sets the provided blessings as default in the BlessingStore specified by the
+environment that this tool is running in.
+
+It is an error to call 'store.setdefault' with blessings whose public key does
+not match the public key of the principal specified by the environment.
+
+Usage:
+   principal store setdefault [flags] <file>
+
+<file> is the path to a file containing a blessing typically obtained from this
+tool. - is used for STDIN.
+
+The principal store setdefault flags are:
+ -add_to_roots=true
+   If true, the root certificate of the blessing will be added to the
+   principal's set of recognized root certificates
+
+Principal Store Forpeer
+
+Returns blessings that are marked for the provided peer in the BlessingStore
+specified by the environment that this tool is running in.
+
+Usage:
+   principal store forpeer [<peer_1> ... <peer_k>]
+
+<peer_1> ... <peer_k> are the (human-readable string) blessings bound to the
+peer. The returned blessings are marked with a pattern that is matched by at
+least one of these. If no arguments are specified, store.forpeer returns the
+blessings that are marked for all peers (i.e., blessings set on the store with
+the "..." pattern).
+
+Principal Store Set
+
+Marks the provided blessings to be shared with the provided peers on the
+BlessingStore specified by the environment that this tool is running in.
+
+'set b pattern' marks the intention to reveal b to peers who present blessings
+of their own matching 'pattern'.
+
+'set nil pattern' can be used to remove the blessings previously associated with
+the pattern (by a prior 'set' command).
+
+It is an error to call 'store.set' with blessings whose public key does not
+match the public key of this principal specified by the environment.
+
+Usage:
+   principal store set [flags] <file> <pattern>
+
+<file> is the path to a file containing a blessing typically obtained from this
+tool. - is used for STDIN.
+
+<pattern> is the BlessingPattern used to identify peers with whom this blessing
+can be shared with.
+
+The principal store set flags are:
+ -add_to_roots=true
+   If true, the root certificate of the blessing will be added to the
+   principal's set of recognized root certificates
+
+Principal Store Addtoroots
+
+Adds the provided blessings to the set of trusted roots for this principal.
+
+'addtoroots b' adds blessings b to the trusted root set.
+
+For example, to make the principal in credentials directory A trust the root of
+the default blessing in credentials directory B:
+  principal -veyron.credentials=B bless A some_extension |
+  principal -veyron.credentials=A store addtoroots -
+
+The extension 'some_extension' has no effect in the command above.
+
+Usage:
+   principal store addtoroots <file>
+
+<file> is the path to a file containing a blessing typically obtained from this
+tool. - is used for STDIN.
+
+Principal Help
+
+Help with no args displays the usage of the parent command.
+
+Help with args displays the usage of the specified sub-command or help topic.
+
+"help ..." recursively displays help for all commands and topics.
+
+The output is formatted to a target width in runes.  The target width is
+determined by checking the environment variable CMDLINE_WIDTH, falling back on
+the terminal width from the OS, falling back on 80 chars.  By setting
+CMDLINE_WIDTH=x, if x > 0 the width is x, if x < 0 the width is unlimited, and
+if x == 0 or is unset one of the fallbacks is used.
+
+Usage:
+   principal help [flags] [command/topic ...]
+
+[command/topic ...] optionally identifies a specific sub-command or help topic.
+
+The principal help flags are:
+ -style=text
+   The formatting style for help output, either "text" or "godoc".
+*/
+package main
diff --git a/cmd/principal/main.go b/cmd/principal/main.go
new file mode 100644
index 0000000..550c270
--- /dev/null
+++ b/cmd/principal/main.go
@@ -0,0 +1,953 @@
+// The following enables go generate to generate the doc.go file.
+//go:generate go run $VANADIUM_ROOT/release/go/src/v.io/x/lib/cmdline/testdata/gendoc.go .
+
+package main
+
+import (
+	"bytes"
+	"crypto/rand"
+	"crypto/subtle"
+	"encoding/base64"
+	"fmt"
+	"io"
+	"os"
+	"os/user"
+	"time"
+
+	"v.io/v23"
+	"v.io/v23/context"
+	"v.io/v23/ipc"
+	"v.io/v23/security"
+	"v.io/v23/vom"
+	"v.io/x/lib/cmdline"
+	_ "v.io/x/ref/profiles/static"
+	vsecurity "v.io/x/ref/security"
+)
+
+var (
+	// Flags for the "blessself" command
+	flagBlessSelfCaveats caveatsFlag
+	flagBlessSelfFor     time.Duration
+
+	// Flags for the "bless" command
+	flagBlessCaveats        caveatsFlag
+	flagBlessFor            time.Duration
+	flagBlessRequireCaveats bool
+	flagBlessWith           string
+	flagBlessRemoteKey      string
+	flagBlessRemoteToken    string
+
+	// Flags for the "fork" command
+	flagForkCaveats        caveatsFlag
+	flagForkFor            time.Duration
+	flagForkRequireCaveats bool
+	flagForkWith           string
+
+	// Flags for the "seekblessings" command
+	flagSeekBlessingsFrom       string
+	flagSeekBlessingsSetDefault bool
+	flagSeekBlessingsForPeer    string
+	flagSeekBlessingsBrowser    bool
+
+	// Flags common to many commands
+	flagAddToRoots      bool
+	flagCreateOverwrite bool
+
+	// Flags for the "recvblessings" command
+	flagRecvBlessingsSetDefault bool
+	flagRecvBlessingsForPeer    string
+
+	errNoCaveats = fmt.Errorf("no caveats provided: it is generally dangerous to bless another principal without any caveats as that gives them almost unrestricted access to the blesser's credentials. If you really want to do this, set --require_caveats=false")
+	cmdDump      = &cmdline.Command{
+		Name:  "dump",
+		Short: "Dump out information about the principal",
+		Long: `
+Prints out information about the principal specified by the environment
+that this tool is running in.
+`,
+		Run: func(cmd *cmdline.Command, args []string) error {
+			ctx, shutdown := v23.Init()
+			defer shutdown()
+
+			p := v23.GetPrincipal(ctx)
+			fmt.Printf("Public key : %v\n", p.PublicKey())
+			fmt.Println("---------------- BlessingStore ----------------")
+			fmt.Printf("%v", p.BlessingStore().DebugString())
+			fmt.Println("---------------- BlessingRoots ----------------")
+			fmt.Printf("%v", p.Roots().DebugString())
+			return nil
+		},
+	}
+
+	cmdDumpBlessings = &cmdline.Command{
+		Name:  "dumpblessings",
+		Short: "Dump out information about the provided blessings",
+		Long: `
+Prints out information about the blessings (typically obtained from this tool)
+encoded in the provided file.
+`,
+		ArgsName: "<file>",
+		ArgsLong: `
+<file> is the path to a file containing blessings typically obtained from
+this tool. - is used for STDIN.
+`,
+		Run: func(cmd *cmdline.Command, args []string) error {
+			if len(args) != 1 {
+				return fmt.Errorf("requires exactly one argument, <file>, provided %d", len(args))
+			}
+			blessings, err := decodeBlessings(args[0])
+			if err != nil {
+				return fmt.Errorf("failed to decode provided blessings: %v", err)
+			}
+			wire, err := blessings2wire(blessings)
+			if err != nil {
+				return fmt.Errorf("failed to decode certificate chains: %v", err)
+			}
+			fmt.Printf("Blessings          : %v\n", blessings)
+			fmt.Printf("PublicKey          : %v\n", blessings.PublicKey())
+			fmt.Printf("Certificate chains : %d\n", len(wire.CertificateChains))
+			for idx, chain := range wire.CertificateChains {
+				fmt.Printf("Chain #%d (%d certificates). Root certificate public key: %v\n", idx, len(chain), rootkey(chain))
+				for certidx, cert := range chain {
+					fmt.Printf("  Certificate #%d: %v with ", certidx, cert.Extension)
+					switch n := len(cert.Caveats); n {
+					case 1:
+						fmt.Printf("1 caveat")
+					default:
+						fmt.Printf("%d caveats", n)
+					}
+					fmt.Println("")
+					for cavidx, cav := range cert.Caveats {
+						fmt.Printf("    (%d) %v\n", cavidx, &cav)
+					}
+				}
+			}
+			return nil
+		},
+	}
+
+	cmdBlessSelf = &cmdline.Command{
+		Name:  "blessself",
+		Short: "Generate a self-signed blessing",
+		Long: `
+Returns a blessing with name <name> and self-signed by the principal specified
+by the environment that this tool is running in. Optionally, the blessing can
+be restricted with an expiry caveat specified using the --for flag. Additional
+caveats can be added with the --caveat flag.
+`,
+		ArgsName: "[<name>]",
+		ArgsLong: `
+<name> is the name used to create the self-signed blessing. If not
+specified, a name will be generated based on the hostname of the
+machine and the name of the user running this command.
+`,
+		Run: func(cmd *cmdline.Command, args []string) error {
+			var name string
+			switch len(args) {
+			case 0:
+				name = defaultBlessingName()
+			case 1:
+				name = args[0]
+			default:
+				return fmt.Errorf("requires at most one argument, provided %d", len(args))
+			}
+
+			caveats, err := caveatsFromFlags(flagBlessSelfFor, &flagBlessSelfCaveats)
+			if err != nil {
+				return err
+			}
+			ctx, shutdown := v23.Init()
+			defer shutdown()
+			principal := v23.GetPrincipal(ctx)
+			blessing, err := principal.BlessSelf(name, caveats...)
+			if err != nil {
+				return fmt.Errorf("failed to create self-signed blessing for name %q: %v", name, err)
+			}
+
+			return dumpBlessings(blessing)
+		},
+	}
+
+	cmdBless = &cmdline.Command{
+		Name:  "bless",
+		Short: "Bless another principal",
+		Long: `
+Bless another principal.
+
+The blesser is obtained from the runtime this tool is using. The blessing that
+will be extended is the default one from the blesser's store, or specified by
+the --with flag. Expiration on the blessing are controlled via the --for flag.
+Additional caveats are controlled with the --caveat flag.
+
+For example, let's say a principal "alice" wants to bless another principal "bob"
+as "alice/friend", the invocation would be:
+    VEYRON_CREDENTIALS=<path to alice> principal bless <path to bob> friend
+and this will dump the blessing to STDOUT.
+
+With the --remote_key and --remote_token flags, this command can be used to
+bless a principal on a remote machine as well. In this case, the blessing is
+not dumped to STDOUT but sent to the remote end. Use 'principal help
+recvblessings' for more details on that.
+`,
+		ArgsName: "<principal to bless> <extension>",
+		ArgsLong: `
+<principal to bless> represents the principal to be blessed (i.e., whose public
+key will be provided with a name).  This can be either:
+(a) The directory containing credentials for that principal,
+OR
+(b) The filename (- for STDIN) containing any other blessings of that
+    principal,
+OR
+(c) The object name produced by the 'recvblessings' command of this tool
+    running on behalf of another principal (if the --remote_key and
+    --remote_token flags are specified).
+
+<extension> is the string extension that will be applied to create the
+blessing.
+	`,
+		Run: func(cmd *cmdline.Command, args []string) error {
+			if len(args) != 2 {
+				return fmt.Errorf("require exactly two arguments, provided %d", len(args))
+			}
+
+			ctx, shutdown := v23.Init()
+			defer shutdown()
+
+			p := v23.GetPrincipal(ctx)
+
+			var (
+				err  error
+				with security.Blessings
+			)
+			if len(flagBlessWith) > 0 {
+				if with, err = decodeBlessings(flagBlessWith); err != nil {
+					return fmt.Errorf("failed to read blessings from --with=%q: %v", flagBlessWith, err)
+				}
+			} else {
+				with = p.BlessingStore().Default()
+			}
+			caveats, err := caveatsFromFlags(flagBlessFor, &flagBlessCaveats)
+			if err != nil {
+				return err
+			}
+			if !flagBlessRequireCaveats && len(caveats) == 0 {
+				caveats = []security.Caveat{security.UnconstrainedUse()}
+			}
+			if len(caveats) == 0 {
+				return errNoCaveats
+			}
+			tobless, extension := args[0], args[1]
+			if (len(flagBlessRemoteKey) == 0) != (len(flagBlessRemoteToken) == 0) {
+				return fmt.Errorf("either both --remote_key and --remote_token should be set, or neither should")
+			}
+			if len(flagBlessRemoteKey) > 0 {
+				// Send blessings to a "server" started by a "recvblessings" command
+				granter := &granter{p, with, extension, caveats, flagBlessRemoteKey}
+				return sendBlessings(ctx, tobless, granter, flagBlessRemoteToken)
+			}
+			// Blessing a principal whose key is available locally.
+			var key security.PublicKey
+			if finfo, err := os.Stat(tobless); err == nil && finfo.IsDir() {
+				other, err := vsecurity.LoadPersistentPrincipal(tobless, nil)
+				if err != nil {
+					if other, err = vsecurity.CreatePersistentPrincipal(tobless, nil); err != nil {
+						return fmt.Errorf("failed to read principal in directory %q: %v", tobless, err)
+					}
+				}
+				key = other.PublicKey()
+			} else if other, err := decodeBlessings(tobless); err != nil {
+				return fmt.Errorf("failed to decode blessings in %q: %v", tobless, err)
+			} else {
+				key = other.PublicKey()
+			}
+			blessings, err := p.Bless(key, with, extension, caveats[0], caveats[1:]...)
+			if err != nil {
+				return fmt.Errorf("Bless(%v, %v, %q, ...) failed: %v", key, with, extension, err)
+			}
+			return dumpBlessings(blessings)
+		},
+	}
+
+	cmdStoreForPeer = &cmdline.Command{
+		Name:  "forpeer",
+		Short: "Return blessings marked for the provided peer",
+		Long: `
+Returns blessings that are marked for the provided peer in the
+BlessingStore specified by the environment that this tool is
+running in.
+`,
+		ArgsName: "[<peer_1> ... <peer_k>]",
+		ArgsLong: `
+<peer_1> ... <peer_k> are the (human-readable string) blessings bound
+to the peer. The returned blessings are marked with a pattern that is
+matched by at least one of these. If no arguments are specified,
+store.forpeer returns the blessings that are marked for all peers (i.e.,
+blessings set on the store with the "..." pattern).
+`,
+		Run: func(cmd *cmdline.Command, args []string) error {
+			ctx, shutdown := v23.Init()
+			defer shutdown()
+			principal := v23.GetPrincipal(ctx)
+			return dumpBlessings(principal.BlessingStore().ForPeer(args...))
+		},
+	}
+
+	cmdStoreDefault = &cmdline.Command{
+		Name:  "default",
+		Short: "Return blessings marked as default",
+		Long: `
+Returns blessings that are marked as default in the BlessingStore specified by
+the environment that this tool is running in.
+`,
+		Run: func(cmd *cmdline.Command, args []string) error {
+			ctx, shutdown := v23.Init()
+			defer shutdown()
+			principal := v23.GetPrincipal(ctx)
+			return dumpBlessings(principal.BlessingStore().Default())
+		},
+	}
+
+	cmdStoreSet = &cmdline.Command{
+		Name:  "set",
+		Short: "Set provided blessings for peer",
+		Long: `
+Marks the provided blessings to be shared with the provided peers on the
+BlessingStore specified by the environment that this tool is running in.
+
+'set b pattern' marks the intention to reveal b to peers who
+present blessings of their own matching 'pattern'.
+
+'set nil pattern' can be used to remove the blessings previously
+associated with the pattern (by a prior 'set' command).
+
+It is an error to call 'store.set' with blessings whose public
+key does not match the public key of this principal specified
+by the environment.
+`,
+		ArgsName: "<file> <pattern>",
+		ArgsLong: `
+<file> is the path to a file containing a blessing typically obtained
+from this tool. - is used for STDIN.
+
+<pattern> is the BlessingPattern used to identify peers with whom this
+blessing can be shared with.
+`,
+		Run: func(cmd *cmdline.Command, args []string) error {
+			if len(args) != 2 {
+				return fmt.Errorf("requires exactly two arguments <file>, <pattern>, provided %d", len(args))
+			}
+			blessings, err := decodeBlessings(args[0])
+			if err != nil {
+				return fmt.Errorf("failed to decode provided blessings: %v", err)
+			}
+			pattern := security.BlessingPattern(args[1])
+
+			ctx, shutdown := v23.Init()
+			defer shutdown()
+
+			p := v23.GetPrincipal(ctx)
+			if _, err := p.BlessingStore().Set(blessings, pattern); err != nil {
+				return fmt.Errorf("failed to set blessings %v for peers %v: %v", blessings, pattern, err)
+			}
+			if flagAddToRoots {
+				if err := p.AddToRoots(blessings); err != nil {
+					return fmt.Errorf("AddToRoots failed: %v", err)
+				}
+			}
+			return nil
+		},
+	}
+
+	cmdStoreAddToRoots = &cmdline.Command{
+		Name:  "addtoroots",
+		Short: "Add provided blessings to root set",
+		Long: `
+Adds the provided blessings to the set of trusted roots for this principal.
+
+'addtoroots b' adds blessings b to the trusted root set.
+
+For example, to make the principal in credentials directory A trust the
+root of the default blessing in credentials directory B:
+  principal -veyron.credentials=B bless A some_extension |
+  principal -veyron.credentials=A store addtoroots -
+
+The extension 'some_extension' has no effect in the command above.
+`,
+		ArgsName: "<file>",
+		ArgsLong: `
+<file> is the path to a file containing a blessing typically obtained
+from this tool. - is used for STDIN.
+`,
+		Run: func(cmd *cmdline.Command, args []string) error {
+			if len(args) != 1 {
+				return fmt.Errorf("requires exactly one argument <file>, provided %d", len(args))
+			}
+			blessings, err := decodeBlessings(args[0])
+			if err != nil {
+				return fmt.Errorf("failed to decode provided blessings: %v", err)
+			}
+
+			ctx, shutdown := v23.Init()
+			defer shutdown()
+
+			p := v23.GetPrincipal(ctx)
+			if err := p.AddToRoots(blessings); err != nil {
+				return fmt.Errorf("AddToRoots failed: %v", err)
+			}
+			return nil
+		},
+	}
+
+	cmdStoreSetDefault = &cmdline.Command{
+		Name:  "setdefault",
+		Short: "Set provided blessings as default",
+		Long: `
+Sets the provided blessings as default in the BlessingStore specified by the
+environment that this tool is running in.
+
+It is an error to call 'store.setdefault' with blessings whose public key does
+not match the public key of the principal specified by the environment.
+`,
+		ArgsName: "<file>",
+		ArgsLong: `
+<file> is the path to a file containing a blessing typically obtained from
+this tool. - is used for STDIN.
+`,
+		Run: func(cmd *cmdline.Command, args []string) error {
+			if len(args) != 1 {
+				return fmt.Errorf("requires exactly one argument, <file>, provided %d", len(args))
+			}
+			blessings, err := decodeBlessings(args[0])
+			if err != nil {
+				return fmt.Errorf("failed to decode provided blessings: %v", err)
+			}
+
+			ctx, shutdown := v23.Init()
+			defer shutdown()
+
+			p := v23.GetPrincipal(ctx)
+			if err := p.BlessingStore().SetDefault(blessings); err != nil {
+				return fmt.Errorf("failed to set blessings %v as default: %v", blessings, err)
+			}
+			if flagAddToRoots {
+				if err := p.AddToRoots(blessings); err != nil {
+					return fmt.Errorf("AddToRoots failed: %v", err)
+				}
+			}
+			return nil
+		},
+	}
+
+	cmdCreate = &cmdline.Command{
+		Name:  "create",
+		Short: "Create a new principal and persist it into a directory",
+		Long: `
+Creates a new principal with a single self-blessed blessing and writes it out
+to the provided directory. The same directory can then be used to set the
+VEYRON_CREDENTIALS environment variable for other veyron applications.
+
+The operation fails if the directory already contains a principal. In this case
+the --overwrite flag can be provided to clear the directory and write out the
+new principal.
+`,
+		ArgsName: "<directory> <blessing>",
+		ArgsLong: `
+	<directory> is the directory to which the new principal will be persisted.
+	<blessing> is the self-blessed blessing that the principal will be setup to use by default.
+	`,
+		Run: func(cmd *cmdline.Command, args []string) error {
+			if len(args) != 2 {
+				return fmt.Errorf("requires exactly two arguments: <directory> and <blessing>, provided %d", len(args))
+			}
+			dir, name := args[0], args[1]
+			if flagCreateOverwrite {
+				if err := os.RemoveAll(dir); err != nil {
+					return err
+				}
+			}
+			p, err := vsecurity.CreatePersistentPrincipal(dir, nil)
+			if err != nil {
+				return err
+			}
+			blessings, err := p.BlessSelf(name)
+			if err != nil {
+				return fmt.Errorf("BlessSelf(%q) failed: %v", name, err)
+			}
+			if err := vsecurity.SetDefaultBlessings(p, blessings); err != nil {
+				return fmt.Errorf("could not set blessings %v as default: %v", blessings, err)
+			}
+			return nil
+		},
+	}
+
+	cmdFork = &cmdline.Command{
+		Name:  "fork",
+		Short: "Fork a new principal from the principal that this tool is running as and persist it into a directory",
+		Long: `
+Creates a new principal with a blessing from the principal specified by the
+environment that this tool is running in, and writes it out to the provided
+directory. The blessing that will be extended is the default one from the
+blesser's store, or specified by the --with flag. Expiration on the blessing
+are controlled via the --for flag. Additional caveats on the blessing are
+controlled with the --caveat flag. The blessing is marked as default and
+shareable with all peers on the new principal's blessing store.
+
+The operation fails if the directory already contains a principal. In this case
+the --overwrite flag can be provided to clear the directory and write out the
+forked principal.
+`,
+		ArgsName: "<directory> <extension>",
+		ArgsLong: `
+	<directory> is the directory to which the forked principal will be persisted.
+	<extension> is the extension under which the forked principal is blessed.
+	`,
+		Run: func(cmd *cmdline.Command, args []string) error {
+			if len(args) != 2 {
+				return fmt.Errorf("requires exactly two arguments: <directory> and <extension>, provided %d", len(args))
+			}
+			dir, extension := args[0], args[1]
+
+			ctx, shutdown := v23.Init()
+			defer shutdown()
+
+			caveats, err := caveatsFromFlags(flagForkFor, &flagForkCaveats)
+			if err != nil {
+				return err
+			}
+			if !flagForkRequireCaveats && len(caveats) == 0 {
+				caveats = []security.Caveat{security.UnconstrainedUse()}
+			}
+			if len(caveats) == 0 {
+				return errNoCaveats
+			}
+			var with security.Blessings
+			if len(flagForkWith) > 0 {
+				if with, err = decodeBlessings(flagForkWith); err != nil {
+					return fmt.Errorf("failed to read blessings from --with=%q: %v", flagForkWith, err)
+				}
+			} else {
+				with = v23.GetPrincipal(ctx).BlessingStore().Default()
+			}
+
+			if flagCreateOverwrite {
+				if err := os.RemoveAll(dir); err != nil {
+					return err
+				}
+			}
+			p, err := vsecurity.CreatePersistentPrincipal(dir, nil)
+			if err != nil {
+				return err
+			}
+
+			key := p.PublicKey()
+			rp := v23.GetPrincipal(ctx)
+			blessings, err := rp.Bless(key, with, extension, caveats[0], caveats[1:]...)
+			if err != nil {
+				return fmt.Errorf("Bless(%v, %v, %q, ...) failed: %v", key, with, extension, err)
+			}
+			if err := vsecurity.SetDefaultBlessings(p, blessings); err != nil {
+				return fmt.Errorf("could not set blessings %v as default: %v", blessings, err)
+			}
+			return nil
+		},
+	}
+
+	cmdSeekBlessings = &cmdline.Command{
+		Name:  "seekblessings",
+		Short: "Seek blessings from a web-based Veyron blessing service",
+		Long: `
+Seeks blessings from a web-based Veyron blesser which
+requires the caller to first authenticate with Google using OAuth. Simply
+run the command to see what happens.
+
+The blessings are sought for the principal specified by the environment that
+this tool is running in.
+
+The blessings obtained are set as default, unless the --set_default flag is
+set to true, and are also set for sharing with all peers, unless a more
+specific peer pattern is provided using the --for_peer flag.
+`,
+		Run: func(cmd *cmdline.Command, args []string) error {
+			// Initialize the runtime first so that any local errors are reported
+			// before the HTTP roundtrips for obtaining the macaroon begin.
+			ctx, shutdown := v23.Init()
+			defer shutdown()
+
+			blessedChan := make(chan string)
+			defer close(blessedChan)
+			macaroonChan, err := getMacaroonForBlessRPC(flagSeekBlessingsFrom, blessedChan, flagSeekBlessingsBrowser)
+			if err != nil {
+				return fmt.Errorf("failed to get macaroon from Veyron blesser: %v", err)
+			}
+
+			blessings, err := exchangeMacaroonForBlessing(ctx, macaroonChan)
+			if err != nil {
+				return err
+			}
+			blessedChan <- fmt.Sprint(blessings)
+			// Wait for getTokenForBlessRPC to clean up:
+			<-macaroonChan
+
+			p := v23.GetPrincipal(ctx)
+
+			if flagSeekBlessingsSetDefault {
+				if err := p.BlessingStore().SetDefault(blessings); err != nil {
+					return fmt.Errorf("failed to set blessings %v as default: %v", blessings, err)
+				}
+			}
+			if pattern := security.BlessingPattern(flagSeekBlessingsForPeer); len(pattern) > 0 {
+				if _, err := p.BlessingStore().Set(blessings, pattern); err != nil {
+					return fmt.Errorf("failed to set blessings %v for peers %v: %v", blessings, pattern, err)
+				}
+			}
+			if flagAddToRoots {
+				if err := p.AddToRoots(blessings); err != nil {
+					return fmt.Errorf("AddToRoots failed: %v", err)
+				}
+			}
+			fmt.Fprintf(cmd.Stdout(), "Received blessings: %v\n", blessings)
+			return nil
+		},
+	}
+
+	cmdRecvBlessings = &cmdline.Command{
+		Name:  "recvblessings",
+		Short: "Receive blessings sent by another principal and use them as the default",
+		Long: `
+Allow another principal (likely a remote process) to bless this one.
+
+This command sets up the invoker (this process) to wait for a blessing
+from another invocation of this tool (remote process) and prints out the
+command to be run as the remote principal.
+
+The received blessings are set as default, unless the --set_default flag is
+set to true, and are also set for sharing with all peers, unless a more
+specific peer pattern is provided using the --for_peer flag.
+
+TODO(ashankar,cnicolaou): Make this next paragraph possible! Requires
+the ability to obtain the proxied endpoint.
+
+Typically, this command should require no arguments.
+However, if the sender and receiver are on different network domains, it may
+make sense to use the --veyron.proxy flag:
+    principal --veyron.proxy=proxy recvblessings
+
+The command to be run at the sender is of the form:
+    principal bless --remote_key=KEY --remote_token=TOKEN ADDRESS
+
+The --remote_key flag is used to by the sender to "authenticate" the receiver,
+ensuring it blesses the intended recipient and not any attacker that may have
+taken over the address.
+
+The --remote_token flag is used by the sender to authenticate itself to the
+receiver. This helps ensure that the receiver rejects blessings from senders
+who just happened to guess the network address of the 'recvblessings'
+invocation.
+`,
+		Run: func(cmd *cmdline.Command, args []string) error {
+			if len(args) != 0 {
+				return fmt.Errorf("command accepts no arguments")
+			}
+
+			ctx, shutdown := v23.Init()
+			defer shutdown()
+
+			server, err := v23.NewServer(ctx)
+			if err != nil {
+				return fmt.Errorf("failed to create server to listen for blessings: %v", err)
+			}
+			defer server.Stop()
+			eps, err := server.Listen(v23.GetListenSpec(ctx))
+			if err != nil {
+				return fmt.Errorf("failed to setup listening: %v", err)
+			}
+			var token [24]byte
+			if _, err := rand.Read(token[:]); err != nil {
+				return fmt.Errorf("unable to generate token: %v", err)
+			}
+
+			p := v23.GetPrincipal(ctx)
+			service := &recvBlessingsService{
+				principal: p,
+				token:     base64.URLEncoding.EncodeToString(token[:]),
+				notify:    make(chan error),
+			}
+			if err := server.Serve("", service, allowAnyone{}); err != nil {
+				return fmt.Errorf("failed to setup service: %v", err)
+			}
+			// Proposed name:
+			extension := fmt.Sprintf("extension%d", int(token[0])<<16|int(token[1])<<8|int(token[2]))
+			fmt.Println("Run the following command on behalf of the principal that will send blessings:")
+			fmt.Println("You may want to adjust flags affecting the caveats on this blessing, for example using")
+			fmt.Println("the --for flag, or change the extension to something more meaningful")
+			fmt.Println()
+			fmt.Printf("principal bless --remote_key=%v --remote_token=%v %v %v\n", p.PublicKey(), service.token, eps[0].Name(), extension)
+			fmt.Println()
+			fmt.Println("...waiting for sender..")
+			return <-service.notify
+		},
+	}
+)
+
+func main() {
+	cmdBlessSelf.Flags.Var(&flagBlessSelfCaveats, "caveat", flagBlessSelfCaveats.usage())
+	cmdBlessSelf.Flags.DurationVar(&flagBlessSelfFor, "for", 0, "Duration of blessing validity (zero implies no expiration)")
+
+	cmdFork.Flags.BoolVar(&flagCreateOverwrite, "overwrite", false, "If true, any existing principal data in the directory will be overwritten")
+	cmdFork.Flags.Var(&flagForkCaveats, "caveat", flagForkCaveats.usage())
+	cmdFork.Flags.DurationVar(&flagForkFor, "for", 0, "Duration of blessing validity (zero implies no expiration caveat)")
+	cmdFork.Flags.BoolVar(&flagForkRequireCaveats, "require_caveats", true, "If false, allow blessing without any caveats. This is typically not advised as the principal wielding the blessing will be almost as powerful as its blesser")
+	cmdFork.Flags.StringVar(&flagForkWith, "with", "", "Path to file containing blessing to extend")
+
+	cmdBless.Flags.Var(&flagBlessCaveats, "caveat", flagBlessCaveats.usage())
+	cmdBless.Flags.DurationVar(&flagBlessFor, "for", 0, "Duration of blessing validity (zero implies no expiration caveat)")
+	cmdBless.Flags.BoolVar(&flagBlessRequireCaveats, "require_caveats", true, "If false, allow blessing without any caveats. This is typically not advised as the principal wielding the blessing will be almost as powerful as its blesser")
+	cmdBless.Flags.StringVar(&flagBlessWith, "with", "", "Path to file containing blessing to extend")
+	cmdBless.Flags.StringVar(&flagBlessRemoteKey, "remote_key", "", "Public key of the remote principal to bless (obtained from the 'recvblessings' command run by the remote principal")
+	cmdBless.Flags.StringVar(&flagBlessRemoteToken, "remote_token", "", "Token provided by principal running the 'recvblessings' command")
+
+	cmdSeekBlessings.Flags.StringVar(&flagSeekBlessingsFrom, "from", "https://auth.dev.v.io:8125/google", "URL to use to begin the seek blessings process")
+	cmdSeekBlessings.Flags.BoolVar(&flagSeekBlessingsSetDefault, "set_default", true, "If true, the blessings obtained will be set as the default blessing in the store")
+	cmdSeekBlessings.Flags.StringVar(&flagSeekBlessingsForPeer, "for_peer", string(security.AllPrincipals), "If non-empty, the blessings obtained will be marked for peers matching this pattern in the store")
+	cmdSeekBlessings.Flags.BoolVar(&flagSeekBlessingsBrowser, "browser", true, "If false, the seekblessings command will not open the browser and only print the url to visit.")
+	cmdSeekBlessings.Flags.BoolVar(&flagAddToRoots, "add_to_roots", true, "If true, the root certificate of the blessing will be added to the principal's set of recognized root certificates")
+
+	cmdStoreSet.Flags.BoolVar(&flagAddToRoots, "add_to_roots", true, "If true, the root certificate of the blessing will be added to the principal's set of recognized root certificates")
+
+	cmdStoreSetDefault.Flags.BoolVar(&flagAddToRoots, "add_to_roots", true, "If true, the root certificate of the blessing will be added to the principal's set of recognized root certificates")
+
+	cmdCreate.Flags.BoolVar(&flagCreateOverwrite, "overwrite", false, "If true, any existing principal data in the directory will be overwritten")
+
+	cmdRecvBlessings.Flags.BoolVar(&flagRecvBlessingsSetDefault, "set_default", true, "If true, the blessings received will be set as the default blessing in the store")
+	cmdRecvBlessings.Flags.StringVar(&flagRecvBlessingsForPeer, "for_peer", string(security.AllPrincipals), "If non-empty, the blessings received will be marked for peers matching this pattern in the store")
+
+	cmdStore := &cmdline.Command{
+		Name:  "store",
+		Short: "Manipulate and inspect the principal's blessing store",
+		Long: `
+Commands to manipulate and inspect the blessing store of the principal.
+
+All blessings are printed to stdout using base64-VOM-encoding
+`,
+		Children: []*cmdline.Command{cmdStoreDefault, cmdStoreSetDefault, cmdStoreForPeer, cmdStoreSet, cmdStoreAddToRoots},
+	}
+
+	root := &cmdline.Command{
+		Name:  "principal",
+		Short: "Create and manage veyron principals",
+		Long: `
+The principal tool helps create and manage blessings and the set of trusted
+roots bound to a principal.
+
+All objects are printed using base64-VOM-encoding.
+`,
+		Children: []*cmdline.Command{cmdCreate, cmdFork, cmdSeekBlessings, cmdRecvBlessings, cmdDump, cmdDumpBlessings, cmdBlessSelf, cmdBless, cmdStore},
+	}
+	os.Exit(root.Main())
+}
+
+func decodeBlessings(fname string) (security.Blessings, error) {
+	var b security.Blessings
+	err := decode(fname, &b)
+	return b, err
+}
+
+func dumpBlessings(blessings security.Blessings) error {
+	if blessings.IsZero() {
+		return fmt.Errorf("no blessings found")
+	}
+	str, err := base64VomEncode(blessings)
+	if err != nil {
+		return fmt.Errorf("base64-VOM encoding failed: %v", err)
+	}
+	fmt.Println(str)
+	return nil
+}
+
+func read(fname string) (string, error) {
+	if len(fname) == 0 {
+		return "", nil
+	}
+	f := os.Stdin
+	if fname != "-" {
+		var err error
+		if f, err = os.Open(fname); err != nil {
+			return "", fmt.Errorf("failed to open %q: %v", fname, err)
+		}
+	}
+	defer f.Close()
+	var buf bytes.Buffer
+	if _, err := io.Copy(&buf, f); err != nil {
+		return "", fmt.Errorf("failed to read %q: %v", fname, err)
+	}
+	return buf.String(), nil
+}
+
+func decode(fname string, val interface{}) error {
+	str, err := read(fname)
+	if err != nil {
+		return err
+	}
+	if err := base64VomDecode(str, val); err != nil || val == nil {
+		return fmt.Errorf("failed to decode %q: %v", fname, err)
+	}
+	return nil
+}
+
+func defaultBlessingName() string {
+	var name string
+	if user, _ := user.Current(); user != nil && len(user.Username) > 0 {
+		name = user.Username
+	} else {
+		name = "anonymous"
+	}
+	if host, _ := os.Hostname(); len(host) > 0 {
+		name = name + "@" + host
+	}
+	return name
+}
+
+func rootkey(chain []security.Certificate) string {
+	if len(chain) == 0 {
+		return "<empty certificate chain>"
+	}
+	key, err := security.UnmarshalPublicKey(chain[0].PublicKey)
+	if err != nil {
+		return fmt.Sprintf("<invalid PublicKey: %v>", err)
+	}
+	return fmt.Sprintf("%v", key)
+}
+
+func base64VomEncode(i interface{}) (string, error) {
+	buf := &bytes.Buffer{}
+	closer := base64.NewEncoder(base64.URLEncoding, buf)
+	enc, err := vom.NewEncoder(closer)
+	if err != nil {
+		return "", err
+	}
+	if err := enc.Encode(i); err != nil {
+		return "", err
+	}
+	// Must close the base64 encoder to flush out any partially written
+	// blocks.
+	if err := closer.Close(); err != nil {
+		return "", err
+	}
+	return buf.String(), nil
+}
+
+func base64VomDecode(s string, i interface{}) error {
+	b, err := base64.URLEncoding.DecodeString(s)
+	if err != nil {
+		return err
+	}
+	dec, err := vom.NewDecoder(bytes.NewBuffer(b))
+	if err != nil {
+		return err
+	}
+	return dec.Decode(i)
+}
+
+type recvBlessingsService struct {
+	principal security.Principal
+	notify    chan error
+	token     string
+}
+
+func (r *recvBlessingsService) Grant(call ipc.StreamServerCall, token string) error {
+	b := call.GrantedBlessings()
+	if b.IsZero() {
+		return fmt.Errorf("no blessings granted by sender")
+	}
+	if len(token) != len(r.token) {
+		// A timing attack can be used to figure out the length
+		// of the token, but then again, so can looking at the
+		// source code. So, it's okay.
+		return fmt.Errorf("blessings received from unexpected sender")
+	}
+	if subtle.ConstantTimeCompare([]byte(token), []byte(r.token)) != 1 {
+		return fmt.Errorf("blessings received from unexpected sender")
+	}
+	if flagRecvBlessingsSetDefault {
+		if err := r.principal.BlessingStore().SetDefault(b); err != nil {
+			return fmt.Errorf("failed to set blessings %v as default: %v", b, err)
+		}
+	}
+	if pattern := security.BlessingPattern(flagRecvBlessingsForPeer); len(pattern) > 0 {
+		if _, err := r.principal.BlessingStore().Set(b, pattern); err != nil {
+			return fmt.Errorf("failed to set blessings %v for peers %v: %v", b, pattern, err)
+		}
+	}
+	if flagAddToRoots {
+		if err := r.principal.AddToRoots(b); err != nil {
+			return fmt.Errorf("failed to add blessings to recognized roots: %v", err)
+		}
+	}
+	fmt.Println("Received blessings:", b)
+	r.notify <- nil
+	return nil
+}
+
+type allowAnyone struct{}
+
+func (allowAnyone) Authorize(security.Call) error { return nil }
+
+type granter struct {
+	p         security.Principal
+	with      security.Blessings
+	extension string
+	caveats   []security.Caveat
+	serverKey string
+}
+
+func (g *granter) Grant(server security.Blessings) (security.Blessings, error) {
+	if got := fmt.Sprintf("%v", server.PublicKey()); got != g.serverKey {
+		// If the granter returns an error, the IPC framework should
+		// abort the RPC before sending the request to the server.
+		// Thus, there is no concern about leaking the token to an
+		// imposter server.
+		return security.Blessings{}, fmt.Errorf("key mismatch: Remote end has public key %v, want %v", got, g.serverKey)
+	}
+	return g.p.Bless(server.PublicKey(), g.with, g.extension, g.caveats[0], g.caveats[1:]...)
+}
+func (*granter) IPCCallOpt() {}
+
+func sendBlessings(ctx *context.T, object string, granter *granter, remoteToken string) error {
+	client := v23.GetClient(ctx)
+	call, err := client.StartCall(ctx, object, "Grant", []interface{}{remoteToken}, granter)
+	if err != nil {
+		return fmt.Errorf("failed to start RPC to %q: %v", object, err)
+	}
+	if err := call.Finish(); err != nil {
+		return fmt.Errorf("failed to finish RPC to %q: %v", object, err)
+	}
+	return nil
+}
+
+func caveatsFromFlags(expiry time.Duration, caveatsFlag *caveatsFlag) ([]security.Caveat, error) {
+	caveats, err := caveatsFlag.Compile()
+	if err != nil {
+		return nil, fmt.Errorf("failed to parse caveats: %v", err)
+	}
+	if expiry > 0 {
+		ecav, err := security.ExpiryCaveat(time.Now().Add(expiry))
+		if err != nil {
+			return nil, fmt.Errorf("failed to create expiration caveat: %v", err)
+		}
+		caveats = append(caveats, ecav)
+	}
+	return caveats, nil
+}
+
+// Circuitous route to get to the certificate chains.
+// See comments on why security.MarshalBlessings is discouraged.
+// Though, a better alternative is worth looking into.
+func blessings2wire(b security.Blessings) (security.WireBlessings, error) {
+	var wire security.WireBlessings
+	data, err := vom.Encode(b)
+	if err != nil {
+		return wire, err
+	}
+	err = vom.Decode(data, &wire)
+	return wire, err
+}
diff --git a/cmd/principal/main_darwin.go b/cmd/principal/main_darwin.go
new file mode 100644
index 0000000..bceafd2
--- /dev/null
+++ b/cmd/principal/main_darwin.go
@@ -0,0 +1,5 @@
+// +build darwin
+
+package main
+
+const openCommand = "open"
diff --git a/cmd/principal/main_linux.go b/cmd/principal/main_linux.go
new file mode 100644
index 0000000..cb73c65
--- /dev/null
+++ b/cmd/principal/main_linux.go
@@ -0,0 +1,5 @@
+// +build linux
+
+package main
+
+const openCommand = "xdg-open"
diff --git a/cmd/principal/main_nacl.go b/cmd/principal/main_nacl.go
new file mode 100644
index 0000000..9dea3b5
--- /dev/null
+++ b/cmd/principal/main_nacl.go
@@ -0,0 +1,3 @@
+package main
+
+const openCommand = ""
diff --git a/cmd/principal/principal_v23_test.go b/cmd/principal/principal_v23_test.go
new file mode 100644
index 0000000..d093ef6
--- /dev/null
+++ b/cmd/principal/principal_v23_test.go
@@ -0,0 +1,477 @@
+package main_test
+
+import (
+	"bytes"
+	"io"
+	"io/ioutil"
+	"os"
+	"path/filepath"
+	"regexp"
+	"strings"
+	"syscall"
+
+	"v.io/x/ref/lib/testutil/v23tests"
+)
+
+//go:generate v23 test generate
+
+// HACK: This is a hack to force v23 test generate to generate modules.Dispatch in TestMain.
+// TODO(suharshs,cnicolaou): Find a way to get rid of this dummy subprocesses.
+func dummy(stdin io.Reader, stdout, stderr io.Writer, env map[string]string, args ...string) error {
+	return nil
+}
+
+// redirect redirects the stdout of the given invocation to the file at the
+// given path.
+func redirect(t *v23tests.T, inv *v23tests.Invocation, path string) {
+	if err := ioutil.WriteFile(path, []byte(inv.Output()), 0600); err != nil {
+		t.Fatalf("WriteFile(%q) failed: %v\n", path, err)
+	}
+}
+
+// removePublicKeys replaces public keys (16 hex bytes, :-separated) with
+// XX:....  This substitution enables comparison with golden output even when
+// keys are freshly minted by the "principal create" command.
+func removePublicKeys(input string) string {
+	return regexp.MustCompile("([0-9a-f]{2}:){15}[0-9a-f]{2}").ReplaceAllString(input, "XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX")
+}
+
+func removeCaveats(input string) string {
+	input = regexp.MustCompile(`0xa64c2d0119fba3348071feeb2f308000\(time\.Time=.*\)`).ReplaceAllString(input, "ExpiryCaveat")
+	input = regexp.MustCompile(`0x54a676398137187ecdb26d2d69ba0003\(\[]string=.*\)`).ReplaceAllString(input, "MethodCaveat")
+	input = regexp.MustCompile(`0x00000000000000000000000000000000\(bool=true\)`).ReplaceAllString(input, "Unconstrained")
+	return input
+}
+
+func V23TestBlessSelf(t *v23tests.T) {
+	var (
+		outputDir         = t.NewTempDir()
+		aliceDir          = filepath.Join(outputDir, "alice")
+		aliceBlessingFile = filepath.Join(outputDir, "aliceself")
+	)
+
+	bin := t.BuildGoPkg("v.io/x/ref/cmd/principal")
+	bin.Start("create", aliceDir, "alice").WaitOrDie(os.Stdout, os.Stderr)
+
+	bin = bin.WithEnv("VEYRON_CREDENTIALS=" + aliceDir)
+	redirect(t, bin.Start("blessself", "alicereborn"), aliceBlessingFile)
+	got := removePublicKeys(bin.Start("dumpblessings", aliceBlessingFile).Output())
+	want := `Blessings          : alicereborn
+PublicKey          : XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX
+Certificate chains : 1
+Chain #0 (1 certificates). Root certificate public key: XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX
+  Certificate #0: alicereborn with 0 caveats
+`
+	if want != got {
+		t.Fatalf("unexpected output, wanted \n%s, got\n%s", want, got)
+	}
+}
+
+func V23TestStore(t *v23tests.T) {
+	var (
+		outputDir   = t.NewTempDir()
+		bin         = t.BuildGoPkg("v.io/x/ref/cmd/principal")
+		aliceDir    = filepath.Join(outputDir, "alice")
+		aliceFriend = filepath.Join(outputDir, "alice.bless")
+		bobDir      = filepath.Join(outputDir, "bob")
+		bobForPeer  = filepath.Join(outputDir, "bob.store.forpeer")
+	)
+
+	// Create two principals: alice and bob.
+	bin.Start("create", aliceDir, "alice").WaitOrDie(os.Stdout, os.Stderr)
+	bin.Start("create", bobDir, "bob").WaitOrDie(os.Stdout, os.Stderr)
+
+	// Bless Alice with Bob's principal.
+	blessEnv := []string{"VEYRON_CREDENTIALS=" + aliceDir}
+	redirect(t, bin.WithEnv(blessEnv...).Start("bless", "--for=1m", bobDir, "friend"), aliceFriend)
+
+	// Run store forpeer on bob.
+	bin.Start("--veyron.credentials="+bobDir, "store", "set", aliceFriend, "alice").WaitOrDie(os.Stdout, os.Stderr)
+	redirect(t, bin.WithEnv(blessEnv...).Start("--veyron.credentials="+bobDir, "store", "forpeer", "alice/server"), bobForPeer)
+
+	got := removeCaveats(removePublicKeys(bin.Start("dumpblessings", bobForPeer).Output()))
+	want := `Blessings          : bob#alice/friend
+PublicKey          : XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX
+Certificate chains : 2
+Chain #0 (1 certificates). Root certificate public key: XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX
+  Certificate #0: bob with 0 caveats
+Chain #1 (2 certificates). Root certificate public key: XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX
+  Certificate #0: alice with 0 caveats
+  Certificate #1: friend with 1 caveat
+    (0) ExpiryCaveat
+`
+	if want != got {
+		t.Fatalf("unexpected output, got\n%s, wanted\n%s", got, want)
+	}
+}
+
+func V23TestDump(t *v23tests.T) {
+	var (
+		outputDir = t.NewTempDir()
+		bin       = t.BuildGoPkg("v.io/x/ref/cmd/principal")
+		aliceDir  = filepath.Join(outputDir, "alice")
+	)
+
+	bin.Start("create", aliceDir, "alice").WaitOrDie(os.Stdout, os.Stderr)
+
+	blessEnv := []string{"VEYRON_CREDENTIALS=" + aliceDir}
+	got := removePublicKeys(bin.WithEnv(blessEnv...).Start("dump").Output())
+	want := `Public key : XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX
+---------------- BlessingStore ----------------
+Default blessings: alice
+Peer pattern                   : Blessings
+...                            : alice
+---------------- BlessingRoots ----------------
+Public key                                      : Pattern
+XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX : [alice]
+`
+	if want != got {
+		t.Fatalf("unexpected output, got\n%s, wanted\n%s", got, want)
+	}
+}
+
+// Given an invocation of "principal recvblessings", this function returns the
+// arguments to provide to "principal bless" provided by the "recvblessings"
+// invocation.
+//
+// For example,
+// principal recvblessings
+// would typically print something like:
+//    principal bless --remote_key=<some_public_key> --remote_token=<some_token> extensionfoo
+// as an example of command line to use to send the blessings over.
+//
+// In that case, this method would return:
+// { "--remote_key=<some_public_key>", "--remote_token=<some_token>", "extensionfoo"}
+func blessArgsFromRecvBlessings(inv *v23tests.Invocation) []string {
+	cmd := inv.ExpectSetEventuallyRE("(^principal bless .*$)")[0][0]
+	return strings.Split(cmd, " ")[2:]
+}
+
+func V23TestRecvBlessings(t *v23tests.T) {
+	var (
+		outputDir = t.NewTempDir()
+		bin       = t.BuildGoPkg("v.io/x/ref/cmd/principal")
+		aliceDir  = filepath.Join(outputDir, "alice")
+		carolDir  = filepath.Join(outputDir, "carol")
+	)
+
+	// Generate principals for alice and carol.
+	bin.Start("create", aliceDir, "alice").WaitOrDie(os.Stdout, os.Stderr)
+	bin.Start("create", carolDir, "carol").WaitOrDie(os.Stdout, os.Stderr)
+
+	// Run recvblessings on carol, and have alice send blessings over
+	// (blessings received must be set as default and shareable with all peers).
+	var args []string
+	{
+		inv := bin.Start("--veyron.credentials="+carolDir, "--veyron.tcp.address=127.0.0.1:0", "recvblessings")
+		defer inv.Kill(syscall.SIGTERM)
+		args = append([]string{"bless", "--require_caveats=false"}, blessArgsFromRecvBlessings(inv)...)
+		// Replace the random extension suggested by recvblessings with "friend/carol"
+		args[len(args)-1] = "friend/carol"
+	}
+
+	bin.WithEnv("VEYRON_CREDENTIALS="+aliceDir).Start(args...).WaitOrDie(os.Stdout, os.Stderr)
+
+	// Run recvblessings on carol, and have alice send blessings over
+	// (blessings received must be set as shareable with peers matching 'alice/...'.)
+	{
+		inv := bin.Start("--veyron.credentials="+carolDir, "--veyron.tcp.address=127.0.0.1:0", "recvblessings", "--for_peer=alice", "--set_default=false")
+		defer inv.Kill(syscall.SIGTERM)
+		// recvblessings suggests a random extension, find the extension and replace it with friend/carol/foralice.
+		args = append([]string{"bless", "--require_caveats=false"}, blessArgsFromRecvBlessings(inv)...)
+		args[len(args)-1] = "friend/carol/foralice"
+	}
+	bin.WithEnv("VEYRON_CREDENTIALS="+aliceDir).Start(args...).WaitOrDie(os.Stdout, os.Stderr)
+
+	listenerInv := bin.Start("--veyron.credentials="+carolDir, "--veyron.tcp.address=127.0.0.1:0", "recvblessings", "--for_peer=alice/...", "--set_default=false", "--vmodule=*=2", "--logtostderr")
+	defer listenerInv.Kill(syscall.SIGTERM)
+
+	args = append([]string{"bless", "--require_caveats=false"}, blessArgsFromRecvBlessings(listenerInv)...)
+
+	{
+		// Mucking around with remote_key should fail.
+		cpy := strings.Split(regexp.MustCompile("remote_key=").ReplaceAllString(strings.Join(args, " "), "remote_key=BAD"), " ")
+		var buf bytes.Buffer
+		if bin.WithEnv("VEYRON_CREDENTIALS="+aliceDir).Start(cpy...).Wait(os.Stdout, &buf) == nil {
+			t.Fatalf("%v should have failed, but did not", cpy)
+		}
+
+		if want, got := "key mismatch", buf.String(); !strings.Contains(got, want) {
+			t.Fatalf("expected %q to be contained within\n%s\n, but was not", want, got)
+		}
+	}
+
+	{
+		var buf bytes.Buffer
+		// Mucking around with the token should fail.
+		cpy := strings.Split(regexp.MustCompile("remote_token=").ReplaceAllString(strings.Join(args, " "), "remote_token=BAD"), " ")
+		if bin.WithEnv("VEYRON_CREDENTIALS="+aliceDir).Start(cpy...).Wait(os.Stdout, &buf) == nil {
+			t.Fatalf("%v should have failed, but did not", cpy)
+		}
+
+		if want, got := "blessings received from unexpected sender", buf.String(); !strings.Contains(got, want) {
+			t.Fatalf("expected %q to be contained within\n%s\n, but was not", want, got)
+		}
+	}
+
+	// Dump carol out, the only blessing that survives should be from the
+	// first "bless" command. (alice/friend/carol).
+	got := removePublicKeys(bin.Start("--veyron.credentials="+carolDir, "dump").Output())
+	want := `Public key : XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX
+---------------- BlessingStore ----------------
+Default blessings: alice/friend/carol
+Peer pattern                   : Blessings
+...                            : alice/friend/carol
+alice                          : alice/friend/carol/foralice
+---------------- BlessingRoots ----------------
+Public key                                      : Pattern
+XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX : [alice]
+XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX : [carol]
+`
+	if want != got {
+		t.Fatalf("unexpected output, got\n%s, wanted\n%s", got, want)
+	}
+}
+
+func V23TestFork(t *v23tests.T) {
+	var (
+		outputDir             = t.NewTempDir()
+		bin                   = t.BuildGoPkg("v.io/x/ref/cmd/principal")
+		aliceDir              = filepath.Join(outputDir, "alice")
+		alicePhoneDir         = filepath.Join(outputDir, "alice-phone")
+		alicePhoneCalendarDir = filepath.Join(outputDir, "alice-phone-calendar")
+		tmpfile               = filepath.Join(outputDir, "tmpfile")
+	)
+
+	// Generate principals for alice.
+	bin.Start("create", aliceDir, "alice").WaitOrDie(os.Stdout, os.Stderr)
+
+	// Run fork to setup up credentials for alice/phone that are
+	// blessed by alice under the extension "phone".
+	bin.Start("--veyron.credentials="+aliceDir, "fork", "--for", "1h", alicePhoneDir, "phone").WaitOrDie(os.Stdout, os.Stderr)
+
+	// Dump alice-phone out, the only blessings it has must be from alice (alice/phone).
+	{
+		got := removePublicKeys(bin.Start("--veyron.credentials="+alicePhoneDir, "dump").Output())
+		want := `Public key : XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX
+---------------- BlessingStore ----------------
+Default blessings: alice/phone
+Peer pattern                   : Blessings
+...                            : alice/phone
+---------------- BlessingRoots ----------------
+Public key                                      : Pattern
+XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX : [alice]
+`
+		if want != got {
+			t.Fatalf("unexpected output, got\n%s, wanted\n%s", got, want)
+		}
+	}
+	// And it should have an expiry caveat
+	{
+		redirect(t, bin.Start("--veyron.credentials", alicePhoneDir, "store", "default"), tmpfile)
+		got := removeCaveats(removePublicKeys(bin.Start("dumpblessings", tmpfile).Output()))
+		want := `Blessings          : alice/phone
+PublicKey          : XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX
+Certificate chains : 1
+Chain #0 (2 certificates). Root certificate public key: XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX
+  Certificate #0: alice with 0 caveats
+  Certificate #1: phone with 1 caveat
+    (0) ExpiryCaveat
+`
+		if want != got {
+			t.Fatalf("unexpected output, got\n%s, wanted\n%s", got, want)
+		}
+	}
+
+	// Run fork to setup up credentials for alice/phone/calendar that are
+	// blessed by alice/phone under the extension "calendar".
+	bin.Start("--veyron.credentials="+alicePhoneDir, "fork", "--for", "1h", alicePhoneCalendarDir, "calendar").WaitOrDie(os.Stdout, os.Stderr)
+	{
+		got := removePublicKeys(bin.Start("--veyron.credentials="+alicePhoneCalendarDir, "dump").Output())
+		want := `Public key : XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX
+---------------- BlessingStore ----------------
+Default blessings: alice/phone/calendar
+Peer pattern                   : Blessings
+...                            : alice/phone/calendar
+---------------- BlessingRoots ----------------
+Public key                                      : Pattern
+XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX : [alice]
+`
+		if want != got {
+			t.Fatalf("unexpected output, got\n%s, wanted\n%s", got, want)
+		}
+	}
+	{
+		redirect(t, bin.Start("--veyron.credentials", alicePhoneCalendarDir, "store", "default"), tmpfile)
+		got := removeCaveats(removePublicKeys(bin.Start("dumpblessings", tmpfile).Output()))
+		want := `Blessings          : alice/phone/calendar
+PublicKey          : XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX
+Certificate chains : 1
+Chain #0 (3 certificates). Root certificate public key: XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX
+  Certificate #0: alice with 0 caveats
+  Certificate #1: phone with 1 caveat
+    (0) ExpiryCaveat
+  Certificate #2: calendar with 1 caveat
+    (0) ExpiryCaveat
+`
+		if want != got {
+			t.Fatalf("unexpected output, got\n%s, wanted\n%s", got, want)
+		}
+	}
+}
+
+func V23TestCreate(t *v23tests.T) {
+	var (
+		outputDir = t.NewTempDir()
+		bin       = t.BuildGoPkg("v.io/x/ref/cmd/principal")
+		aliceDir  = filepath.Join(outputDir, "alice")
+	)
+
+	// Creating a principal should succeed the first time.
+	bin.Start("create", aliceDir, "alice").WaitOrDie(os.Stdout, os.Stderr)
+
+	// The second time should fail (the create command won't override an existing principal).
+	if bin.Start("create", aliceDir, "alice").Wait(os.Stdout, os.Stderr) == nil {
+		t.Fatalf("principal creation should have failed, but did not")
+	}
+
+	// If we specify -overwrite, it will.
+	bin.Start("create", "--overwrite", aliceDir, "alice").WaitOrDie(os.Stdout, os.Stderr)
+}
+
+func V23TestCaveats(t *v23tests.T) {
+	var (
+		outputDir         = t.NewTempDir()
+		aliceDir          = filepath.Join(outputDir, "alice")
+		aliceBlessingFile = filepath.Join(outputDir, "aliceself")
+	)
+
+	bin := t.BuildGoPkg("v.io/x/ref/cmd/principal")
+	bin.Start("create", aliceDir, "alice").WaitOrDie(os.Stdout, os.Stderr)
+
+	bin = bin.WithEnv("VEYRON_CREDENTIALS=" + aliceDir)
+	args := []string{
+		"blessself",
+		"--caveat=\"v.io/v23/security\".MethodCaveatX={\"method\"}",
+		"--caveat={{0x54,0xa6,0x76,0x39,0x81,0x37,0x18,0x7e,0xcd,0xb2,0x6d,0x2d,0x69,0xba,0x0,0x3},typeobject([]string)}={\"method\"}",
+		"alicereborn",
+	}
+	redirect(t, bin.Start(args...), aliceBlessingFile)
+	got := removeCaveats(removePublicKeys(bin.Start("dumpblessings", aliceBlessingFile).Output()))
+	want := `Blessings          : alicereborn
+PublicKey          : XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX
+Certificate chains : 1
+Chain #0 (1 certificates). Root certificate public key: XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX
+  Certificate #0: alicereborn with 2 caveats
+    (0) MethodCaveat
+    (1) MethodCaveat
+`
+	if want != got {
+		t.Fatalf("unexpected output, wanted \n%s, got\n%s", want, got)
+	}
+}
+
+func V23TestForkWithoutVDLPATH(t *v23tests.T) {
+	var (
+		parent = t.NewTempDir()
+		bin    = t.BuildGoPkg("v.io/x/ref/cmd/principal").WithEnv("VANADIUM_ROOT=''", "VDLPATH=''")
+	)
+	if err := bin.Start("create", parent, "parent").Wait(os.Stdout, os.Stderr); err != nil {
+		t.Fatalf("create %q failed: %v", parent, err)
+	}
+	if err := bin.Start("--veyron.credentials="+parent, "fork", "--for=1s", t.NewTempDir(), "child").Wait(os.Stdout, os.Stderr); err != nil {
+		t.Errorf("fork failed: %v", err)
+	}
+}
+
+func V23TestForkWithoutCaveats(t *v23tests.T) {
+	var (
+		parent = t.NewTempDir()
+		child  = t.NewTempDir()
+		bin    = t.BuildGoPkg("v.io/x/ref/cmd/principal")
+		buf    bytes.Buffer
+	)
+	if err := bin.Start("create", parent, "parent").Wait(os.Stdout, os.Stderr); err != nil {
+		t.Fatalf("create %q failed: %v", parent, err)
+	}
+	if err := bin.Start("--veyron.credentials", parent, "fork", child, "child").Wait(os.Stdout, &buf); err == nil {
+		t.Errorf("fork should have failed without any caveats, but did not")
+	} else if got, want := buf.String(), "ERROR: no caveats provided"; !strings.Contains(got, want) {
+		t.Errorf("fork returned error: %q, expected error to contain %q", got, want)
+	}
+	if err := bin.Start("--veyron.credentials", parent, "fork", "--for=0", child, "child").Wait(os.Stdout, &buf); err == nil {
+		t.Errorf("fork should have failed without any caveats, but did not")
+	} else if got, want := buf.String(), "ERROR: no caveats provided"; !strings.Contains(got, want) {
+		t.Errorf("fork returned error: %q, expected error to contain %q", got, want)
+	}
+	if err := bin.Start("--veyron.credentials", parent, "fork", "--require_caveats=false", child, "child").Wait(os.Stdout, os.Stderr); err != nil {
+		t.Errorf("fork --require_caveats=false failed with: %v", err)
+	}
+}
+
+func V23TestBless(t *v23tests.T) {
+	var (
+		bin      = t.BuildGoPkg("v.io/x/ref/cmd/principal")
+		dir      = t.NewTempDir()
+		aliceDir = filepath.Join(dir, "alice")
+		bobDir   = filepath.Join(dir, "bob")
+		tmpfile  = filepath.Join(dir, "tmpfile")
+	)
+	// Create two principals: alice and bob
+	bin.Start("create", aliceDir, "alice").WaitOrDie(os.Stdout, os.Stderr)
+	bin.Start("create", bobDir, "bob").WaitOrDie(os.Stdout, os.Stderr)
+
+	// All blessings will be done by "alice"
+	bin = bin.WithEnv("VEYRON_CREDENTIALS=" + aliceDir)
+
+	{
+		// "alice" should fail to bless "bob" without any caveats
+		var buf bytes.Buffer
+		if err := bin.Start("bless", bobDir, "friend").Wait(os.Stdout, &buf); err == nil {
+			t.Errorf("bless should have failed when no caveats are specified")
+		} else if got, want := buf.String(), "ERROR: no caveats provided"; !strings.Contains(got, want) {
+			t.Errorf("got error %q, expected to match %q", got, want)
+		}
+	}
+	{
+		// But succeed if --require_caveats=false is specified
+		redirect(t, bin.Start("bless", "--require_caveats=false", bobDir, "friend"), tmpfile)
+		got := removeCaveats(removePublicKeys(bin.Start("dumpblessings", tmpfile).Output()))
+		want := `Blessings          : alice/friend
+PublicKey          : XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX
+Certificate chains : 1
+Chain #0 (2 certificates). Root certificate public key: XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX
+  Certificate #0: alice with 0 caveats
+  Certificate #1: friend with 1 caveat
+    (0) Unconstrained
+`
+		if got != want {
+			t.Errorf("Got\n%vWant\n%v", got, want)
+		}
+	}
+	{
+		// And succeed if --for is specified
+		redirect(t, bin.Start("bless", "--for=1m", bobDir, "friend"), tmpfile)
+		got := removeCaveats(removePublicKeys(bin.Start("dumpblessings", tmpfile).Output()))
+		want := `Blessings          : alice/friend
+PublicKey          : XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX
+Certificate chains : 1
+Chain #0 (2 certificates). Root certificate public key: XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX
+  Certificate #0: alice with 0 caveats
+  Certificate #1: friend with 1 caveat
+    (0) ExpiryCaveat
+`
+		if got != want {
+			t.Errorf("Got\n%vWant\n%v", got, want)
+		}
+	}
+	{
+		// But not if --for=0
+		var buf bytes.Buffer
+		if err := bin.Start("bless", "--for=0", bobDir, "friend").Wait(os.Stdout, &buf); err == nil {
+			t.Errorf("bless should have failed when no caveats are specified")
+		} else if got, want := buf.String(), "ERROR: no caveats provided"; !strings.Contains(got, want) {
+			t.Errorf("got error %q, expected to match %q", got, want)
+		}
+	}
+}
diff --git a/cmd/principal/v23_test.go b/cmd/principal/v23_test.go
new file mode 100644
index 0000000..810095e
--- /dev/null
+++ b/cmd/principal/v23_test.go
@@ -0,0 +1,75 @@
+// Copyright 2015 The Vanadium 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 file was auto-generated via go generate.
+// DO NOT UPDATE MANUALLY
+package main_test
+
+import "fmt"
+import "testing"
+import "os"
+
+import "v.io/x/ref/lib/modules"
+import "v.io/x/ref/lib/testutil"
+import "v.io/x/ref/lib/testutil/v23tests"
+
+func init() {
+	modules.RegisterChild("dummy", `HACK: This is a hack to force v23 test generate to generate modules.Dispatch in TestMain.
+TODO(suharshs,cnicolaou): Find a way to get rid of this dummy subprocesses.`, dummy)
+}
+
+func TestMain(m *testing.M) {
+	testutil.Init()
+	if modules.IsModulesProcess() {
+		if err := modules.Dispatch(); err != nil {
+			fmt.Fprintf(os.Stderr, "modules.Dispatch failed: %v\n", err)
+			os.Exit(1)
+		}
+		return
+	}
+	cleanup := v23tests.UseSharedBinDir()
+	r := m.Run()
+	cleanup()
+	os.Exit(r)
+}
+
+func TestV23BlessSelf(t *testing.T) {
+	v23tests.RunTest(t, V23TestBlessSelf)
+}
+
+func TestV23Store(t *testing.T) {
+	v23tests.RunTest(t, V23TestStore)
+}
+
+func TestV23Dump(t *testing.T) {
+	v23tests.RunTest(t, V23TestDump)
+}
+
+func TestV23RecvBlessings(t *testing.T) {
+	v23tests.RunTest(t, V23TestRecvBlessings)
+}
+
+func TestV23Fork(t *testing.T) {
+	v23tests.RunTest(t, V23TestFork)
+}
+
+func TestV23Create(t *testing.T) {
+	v23tests.RunTest(t, V23TestCreate)
+}
+
+func TestV23Caveats(t *testing.T) {
+	v23tests.RunTest(t, V23TestCaveats)
+}
+
+func TestV23ForkWithoutVDLPATH(t *testing.T) {
+	v23tests.RunTest(t, V23TestForkWithoutVDLPATH)
+}
+
+func TestV23ForkWithoutCaveats(t *testing.T) {
+	v23tests.RunTest(t, V23TestForkWithoutCaveats)
+}
+
+func TestV23Bless(t *testing.T) {
+	v23tests.RunTest(t, V23TestBless)
+}
diff --git a/cmd/profile/doc.go b/cmd/profile/doc.go
new file mode 100644
index 0000000..e50d9cb
--- /dev/null
+++ b/cmd/profile/doc.go
@@ -0,0 +1,127 @@
+// This file was auto-generated via go generate.
+// DO NOT UPDATE MANUALLY
+
+/*
+The profile tool facilitates interaction with the veyron profile repository.
+
+Usage:
+   profile <command>
+
+The profile commands are:
+   label         Shows a human-readable profile key for the profile.
+   description   Shows a human-readable profile description for the profile.
+   specification Shows the specification of the profile.
+   put           Sets a placeholder specification for the profile.
+   remove        removes the profile specification for the profile.
+   help          Display help for commands or topics
+Run "profile help [command]" for command usage.
+
+The global flags are:
+ -alsologtostderr=true
+   log to standard error as well as files
+ -log_backtrace_at=:0
+   when logging hits line file:N, emit a stack trace
+ -log_dir=
+   if non-empty, write log files to this directory
+ -logtostderr=false
+   log to standard error instead of files
+ -max_stack_buf_size=4292608
+   max size in bytes of the buffer to use for logging stack traces
+ -stderrthreshold=2
+   logs at or above this threshold go to stderr
+ -v=0
+   log level for V logs
+ -vanadium.i18n_catalogue=
+   18n catalogue files to load, comma separated
+ -veyron.credentials=
+   directory to use for storing security credentials
+ -veyron.namespace.root=[/ns.dev.v.io:8101]
+   local namespace root; can be repeated to provided multiple roots
+ -veyron.proxy=
+   object name of proxy service to use to export services across network
+   boundaries
+ -veyron.tcp.address=
+   address to listen on
+ -veyron.tcp.protocol=tcp
+   protocol to listen with
+ -veyron.vtrace.cache_size=1024
+   The number of vtrace traces to store in memory.
+ -veyron.vtrace.collect_regexp=
+   Spans and annotations that match this regular expression will trigger trace
+   collection.
+ -veyron.vtrace.dump_on_shutdown=true
+   If true, dump all stored traces on runtime shutdown.
+ -veyron.vtrace.sample_rate=0
+   Rate (from 0.0 to 1.0) to sample vtrace traces.
+ -vmodule=
+   comma-separated list of pattern=N settings for file-filtered logging
+
+Profile Label
+
+Shows a human-readable profile key for the profile.
+
+Usage:
+   profile label <profile>
+
+<profile> is the full name of the profile.
+
+Profile Description
+
+Shows a human-readable profile description for the profile.
+
+Usage:
+   profile description <profile>
+
+<profile> is the full name of the profile.
+
+Profile Specification
+
+Shows the specification of the profile.
+
+Usage:
+   profile specification <profile>
+
+<profile> is the full name of the profile.
+
+Profile Put
+
+Sets a placeholder specification for the profile.
+
+Usage:
+   profile put <profile>
+
+<profile> is the full name of the profile.
+
+Profile Remove
+
+removes the profile specification for the profile.
+
+Usage:
+   profile remove <profile>
+
+<profile> is the full name of the profile.
+
+Profile Help
+
+Help with no args displays the usage of the parent command.
+
+Help with args displays the usage of the specified sub-command or help topic.
+
+"help ..." recursively displays help for all commands and topics.
+
+The output is formatted to a target width in runes.  The target width is
+determined by checking the environment variable CMDLINE_WIDTH, falling back on
+the terminal width from the OS, falling back on 80 chars.  By setting
+CMDLINE_WIDTH=x, if x > 0 the width is x, if x < 0 the width is unlimited, and
+if x == 0 or is unset one of the fallbacks is used.
+
+Usage:
+   profile help [flags] [command/topic ...]
+
+[command/topic ...] optionally identifies a specific sub-command or help topic.
+
+The profile help flags are:
+ -style=text
+   The formatting style for help output, either "text" or "godoc".
+*/
+package main
diff --git a/cmd/profile/impl.go b/cmd/profile/impl.go
new file mode 100644
index 0000000..92b1fc9
--- /dev/null
+++ b/cmd/profile/impl.go
@@ -0,0 +1,156 @@
+package main
+
+import (
+	"fmt"
+	"time"
+
+	"v.io/v23/context"
+	"v.io/v23/services/mgmt/build"
+	"v.io/x/lib/cmdline"
+	"v.io/x/ref/services/mgmt/profile"
+	"v.io/x/ref/services/mgmt/repository"
+)
+
+var cmdLabel = &cmdline.Command{
+	Run:      runLabel,
+	Name:     "label",
+	Short:    "Shows a human-readable profile key for the profile.",
+	Long:     "Shows a human-readable profile key for the profile.",
+	ArgsName: "<profile>",
+	ArgsLong: "<profile> is the full name of the profile.",
+}
+
+func runLabel(cmd *cmdline.Command, args []string) error {
+	if expected, got := 1, len(args); expected != got {
+		return cmd.UsageErrorf("label: incorrect number of arguments, expected %d, got %d", expected, got)
+	}
+	name := args[0]
+	p := repository.ProfileClient(name)
+	ctx, cancel := context.WithTimeout(gctx, time.Minute)
+	defer cancel()
+	label, err := p.Label(ctx)
+	if err != nil {
+		return err
+	}
+	fmt.Fprintln(cmd.Stdout(), label)
+	return nil
+}
+
+var cmdDescription = &cmdline.Command{
+	Run:      runDescription,
+	Name:     "description",
+	Short:    "Shows a human-readable profile description for the profile.",
+	Long:     "Shows a human-readable profile description for the profile.",
+	ArgsName: "<profile>",
+	ArgsLong: "<profile> is the full name of the profile.",
+}
+
+func runDescription(cmd *cmdline.Command, args []string) error {
+	if expected, got := 1, len(args); expected != got {
+		return cmd.UsageErrorf("description: incorrect number of arguments, expected %d, got %d", expected, got)
+	}
+	name := args[0]
+	p := repository.ProfileClient(name)
+	ctx, cancel := context.WithTimeout(gctx, time.Minute)
+	defer cancel()
+	desc, err := p.Description(ctx)
+	if err != nil {
+		return err
+	}
+	fmt.Fprintln(cmd.Stdout(), desc)
+	return nil
+}
+
+var cmdSpecification = &cmdline.Command{
+	Run:      runSpecification,
+	Name:     "specification",
+	Short:    "Shows the specification of the profile.",
+	Long:     "Shows the specification of the profile.",
+	ArgsName: "<profile>",
+	ArgsLong: "<profile> is the full name of the profile.",
+}
+
+func runSpecification(cmd *cmdline.Command, args []string) error {
+	if expected, got := 1, len(args); expected != got {
+		return cmd.UsageErrorf("specification: incorrect number of arguments, expected %d, got %d", expected, got)
+	}
+	name := args[0]
+	p := repository.ProfileClient(name)
+	ctx, cancel := context.WithTimeout(gctx, time.Minute)
+	defer cancel()
+	spec, err := p.Specification(ctx)
+	if err != nil {
+		return err
+	}
+	fmt.Fprintf(cmd.Stdout(), "%#v\n", spec)
+	return nil
+}
+
+var cmdPut = &cmdline.Command{
+	Run:      runPut,
+	Name:     "put",
+	Short:    "Sets a placeholder specification for the profile.",
+	Long:     "Sets a placeholder specification for the profile.",
+	ArgsName: "<profile>",
+	ArgsLong: "<profile> is the full name of the profile.",
+}
+
+func runPut(cmd *cmdline.Command, args []string) error {
+	if expected, got := 1, len(args); expected != got {
+		return cmd.UsageErrorf("put: incorrect number of arguments, expected %d, got %d", expected, got)
+	}
+	name := args[0]
+	p := repository.ProfileClient(name)
+
+	// TODO(rthellend): Read an actual specification from a file.
+	spec := profile.Specification{
+		Arch:        build.AMD64,
+		Description: "Example profile to test the profile manager implementation.",
+		Format:      build.ELF,
+		Libraries:   map[profile.Library]struct{}{profile.Library{Name: "foo", MajorVersion: "1", MinorVersion: "0"}: struct{}{}},
+		Label:       "example",
+		OS:          build.Linux,
+	}
+	ctx, cancel := context.WithTimeout(gctx, time.Minute)
+	defer cancel()
+	if err := p.Put(ctx, spec); err != nil {
+		return err
+	}
+	fmt.Fprintln(cmd.Stdout(), "Profile added successfully.")
+	return nil
+}
+
+var cmdRemove = &cmdline.Command{
+	Run:      runRemove,
+	Name:     "remove",
+	Short:    "removes the profile specification for the profile.",
+	Long:     "removes the profile specification for the profile.",
+	ArgsName: "<profile>",
+	ArgsLong: "<profile> is the full name of the profile.",
+}
+
+func runRemove(cmd *cmdline.Command, args []string) error {
+	if expected, got := 1, len(args); expected != got {
+		return cmd.UsageErrorf("remove: incorrect number of arguments, expected %d, got %d", expected, got)
+	}
+	name := args[0]
+	p := repository.ProfileClient(name)
+	ctx, cancel := context.WithTimeout(gctx, time.Minute)
+	defer cancel()
+	if err := p.Remove(ctx); err != nil {
+		return err
+	}
+	fmt.Fprintln(cmd.Stdout(), "Profile removed successfully.")
+	return nil
+}
+
+func root() *cmdline.Command {
+	return &cmdline.Command{
+		Name:  "profile",
+		Short: "Tool for interacting with the veyron profile repository",
+		Long: `
+The profile tool facilitates interaction with the veyron profile repository.
+`,
+		Children: []*cmdline.Command{cmdLabel, cmdDescription, cmdSpecification, cmdPut, cmdRemove},
+	}
+}
diff --git a/cmd/profile/impl_test.go b/cmd/profile/impl_test.go
new file mode 100644
index 0000000..e07af3c
--- /dev/null
+++ b/cmd/profile/impl_test.go
@@ -0,0 +1,171 @@
+package main
+
+import (
+	"bytes"
+	"fmt"
+	"strings"
+	"testing"
+
+	"v.io/v23"
+	"v.io/v23/context"
+	"v.io/v23/ipc"
+	"v.io/v23/naming"
+	"v.io/v23/security"
+	"v.io/v23/services/mgmt/build"
+	"v.io/x/lib/vlog"
+
+	"v.io/x/ref/lib/testutil"
+	_ "v.io/x/ref/profiles"
+	"v.io/x/ref/services/mgmt/profile"
+	"v.io/x/ref/services/mgmt/repository"
+)
+
+var (
+	// spec is an example profile specification used throughout the test.
+	spec = profile.Specification{
+		Arch:        build.AMD64,
+		Description: "Example profile to test the profile repository implementation.",
+		Format:      build.ELF,
+		Libraries:   map[profile.Library]struct{}{profile.Library{Name: "foo", MajorVersion: "1", MinorVersion: "0"}: struct{}{}},
+		Label:       "example",
+		OS:          build.Linux,
+	}
+)
+
+type server struct {
+	suffix string
+}
+
+func (s *server) Label(ipc.ServerCall) (string, error) {
+	vlog.VI(2).Infof("%v.Label() was called", s.suffix)
+	if s.suffix != "exists" {
+		return "", fmt.Errorf("profile doesn't exist: %v", s.suffix)
+	}
+	return spec.Label, nil
+}
+
+func (s *server) Description(ipc.ServerCall) (string, error) {
+	vlog.VI(2).Infof("%v.Description() was called", s.suffix)
+	if s.suffix != "exists" {
+		return "", fmt.Errorf("profile doesn't exist: %v", s.suffix)
+	}
+	return spec.Description, nil
+}
+
+func (s *server) Specification(ipc.ServerCall) (profile.Specification, error) {
+	vlog.VI(2).Infof("%v.Specification() was called", s.suffix)
+	if s.suffix != "exists" {
+		return profile.Specification{}, fmt.Errorf("profile doesn't exist: %v", s.suffix)
+	}
+	return spec, nil
+}
+
+func (s *server) Put(_ ipc.ServerCall, _ profile.Specification) error {
+	vlog.VI(2).Infof("%v.Put() was called", s.suffix)
+	return nil
+}
+
+func (s *server) Remove(ipc.ServerCall) error {
+	vlog.VI(2).Infof("%v.Remove() was called", s.suffix)
+	if s.suffix != "exists" {
+		return fmt.Errorf("profile doesn't exist: %v", s.suffix)
+	}
+	return nil
+}
+
+type dispatcher struct {
+}
+
+func NewDispatcher() ipc.Dispatcher {
+	return &dispatcher{}
+}
+
+func (d *dispatcher) Lookup(suffix string) (interface{}, security.Authorizer, error) {
+	return repository.ProfileServer(&server{suffix: suffix}), nil, nil
+}
+
+func startServer(t *testing.T, ctx *context.T) (ipc.Server, naming.Endpoint, error) {
+	server, err := v23.NewServer(ctx)
+	if err != nil {
+		t.Errorf("NewServer failed: %v", err)
+		return nil, nil, err
+	}
+	endpoints, err := server.Listen(v23.GetListenSpec(ctx))
+	if err != nil {
+		t.Errorf("Listen failed: %v", err)
+		return nil, nil, err
+	}
+	if err := server.ServeDispatcher("", NewDispatcher()); err != nil {
+		t.Errorf("ServeDispatcher failed: %v", err)
+		return nil, nil, err
+	}
+	return server, endpoints[0], nil
+}
+
+func stopServer(t *testing.T, server ipc.Server) {
+	if err := server.Stop(); err != nil {
+		t.Errorf("server.Stop failed: %v", err)
+	}
+}
+
+func TestProfileClient(t *testing.T) {
+	var shutdown v23.Shutdown
+	gctx, shutdown = testutil.InitForTest()
+	defer shutdown()
+
+	server, endpoint, err := startServer(t, gctx)
+	if err != nil {
+		return
+	}
+	defer stopServer(t, server)
+	// Setup the command-line.
+	cmd := root()
+	var stdout, stderr bytes.Buffer
+	cmd.Init(nil, &stdout, &stderr)
+	exists := naming.JoinAddressName(endpoint.String(), "exists")
+
+	// Test the 'label' command.
+	if err := cmd.Execute([]string{"label", exists}); err != nil {
+		t.Fatalf("%v", err)
+	}
+	if expected, got := spec.Label, strings.TrimSpace(stdout.String()); got != expected {
+		t.Errorf("Got %q, expected %q", got, expected)
+	}
+	stdout.Reset()
+
+	// Test the 'description' command.
+	if err := cmd.Execute([]string{"description", exists}); err != nil {
+		t.Fatalf("%v", err)
+	}
+	if expected, got := spec.Description, strings.TrimSpace(stdout.String()); got != expected {
+		t.Errorf("Got %q, expected %q", got, expected)
+	}
+	stdout.Reset()
+
+	// Test the 'spec' command.
+	if err := cmd.Execute([]string{"specification", exists}); err != nil {
+		t.Fatalf("%v", err)
+	}
+	if expected, got := fmt.Sprintf("%#v", spec), strings.TrimSpace(stdout.String()); got != expected {
+		t.Errorf("Got %q, expected %q", got, expected)
+	}
+	stdout.Reset()
+
+	// Test the 'put' command.
+	if err := cmd.Execute([]string{"put", exists}); err != nil {
+		t.Fatalf("%v", err)
+	}
+	if expected, got := "Profile added successfully.", strings.TrimSpace(stdout.String()); got != expected {
+		t.Errorf("Got %q, expected %q", got, expected)
+	}
+	stdout.Reset()
+
+	// Test the 'remove' command.
+	if err := cmd.Execute([]string{"remove", exists}); err != nil {
+		t.Fatalf("%v", err)
+	}
+	if expected, got := "Profile removed successfully.", strings.TrimSpace(stdout.String()); got != expected {
+		t.Errorf("Got %q, expected %q", got, expected)
+	}
+	stdout.Reset()
+}
diff --git a/cmd/profile/main.go b/cmd/profile/main.go
new file mode 100644
index 0000000..03caf91
--- /dev/null
+++ b/cmd/profile/main.go
@@ -0,0 +1,23 @@
+// The following enables go generate to generate the doc.go file.
+//go:generate go run $VANADIUM_ROOT/release/go/src/v.io/x/lib/cmdline/testdata/gendoc.go .
+
+package main
+
+import (
+	"os"
+
+	"v.io/v23"
+	"v.io/v23/context"
+
+	_ "v.io/x/ref/profiles"
+)
+
+var gctx *context.T
+
+func main() {
+	var shutdown v23.Shutdown
+	gctx, shutdown = v23.Init()
+	exitCode := root().Main()
+	shutdown()
+	os.Exit(exitCode)
+}
diff --git a/cmd/servicerunner/main.go b/cmd/servicerunner/main.go
new file mode 100644
index 0000000..4da36de
--- /dev/null
+++ b/cmd/servicerunner/main.go
@@ -0,0 +1,104 @@
+// This binary starts several services (mount table, proxy, wspr), then prints a
+// JSON map with their vars to stdout (as a single line), then waits forever.
+package main
+
+import (
+	"encoding/json"
+	"fmt"
+	"os"
+	"strings"
+	"time"
+
+	"v.io/v23"
+
+	"v.io/x/ref/lib/expect"
+	"v.io/x/ref/lib/flags/consts"
+	"v.io/x/ref/lib/modules"
+	"v.io/x/ref/lib/modules/core"
+	_ "v.io/x/ref/profiles"
+)
+
+func panicOnError(err error) {
+	if err != nil {
+		panic(err)
+	}
+}
+
+// updateVars captures the vars from the given Handle's stdout and adds them to
+// the given vars map, overwriting existing entries.
+func updateVars(h modules.Handle, vars map[string]string, varNames ...string) error {
+	varsToAdd := map[string]bool{}
+	for _, v := range varNames {
+		varsToAdd[v] = true
+	}
+	numLeft := len(varsToAdd)
+
+	s := expect.NewSession(nil, h.Stdout(), 30*time.Second)
+	for {
+		l := s.ReadLine()
+		if err := s.OriginalError(); err != nil {
+			return err // EOF or otherwise
+		}
+		parts := strings.Split(l, "=")
+		if len(parts) != 2 {
+			return fmt.Errorf("Unexpected line: %s", l)
+		}
+		if _, ok := varsToAdd[parts[0]]; ok {
+			numLeft--
+			vars[parts[0]] = parts[1]
+			if numLeft == 0 {
+				break
+			}
+		}
+	}
+	return nil
+}
+
+func main() {
+	if modules.IsModulesProcess() {
+		panicOnError(modules.Dispatch())
+		return
+	}
+
+	ctx, shutdown := v23.Init()
+	defer shutdown()
+
+	vars := map[string]string{}
+
+	sh, err := modules.NewShell(ctx, nil)
+	if err != nil {
+		panic(fmt.Sprintf("modules.NewShell: %s", err))
+	}
+	defer sh.Cleanup(os.Stderr, os.Stderr)
+
+	h, err := sh.Start(core.RootMTCommand, nil, "--veyron.tcp.protocol=ws", "--veyron.tcp.address=127.0.0.1:0")
+	panicOnError(err)
+	panicOnError(updateVars(h, vars, "MT_NAME"))
+
+	// Set consts.NamespaceRootPrefix env var, consumed downstream by proxyd
+	// among others.
+	// NOTE(sadovsky): If this var is not set, proxyd takes several seconds to
+	// start; if it is set, proxyd starts instantly. Confusing.
+	sh.SetVar(consts.NamespaceRootPrefix, vars["MT_NAME"])
+
+	// NOTE(sadovsky): The proxyd binary requires --protocol and --address flags
+	// while the proxyd command instead uses ListenSpec flags.
+	h, err = sh.Start(core.ProxyServerCommand, nil, "--veyron.tcp.protocol=ws", "--veyron.tcp.address=127.0.0.1:0", "test/proxy")
+	panicOnError(err)
+	panicOnError(updateVars(h, vars, "PROXY_NAME"))
+
+	h, err = sh.Start(core.WSPRCommand, nil, "--veyron.tcp.protocol=ws", "--veyron.tcp.address=127.0.0.1:0", "--veyron.proxy=test/proxy", "--identd=test/identd")
+	panicOnError(err)
+	panicOnError(updateVars(h, vars, "WSPR_ADDR"))
+
+	h, err = sh.Start(core.TestIdentitydCommand, nil, "--veyron.tcp.protocol=ws", "--veyron.tcp.address=127.0.0.1:0", "--veyron.proxy=test/proxy", "--host=localhost", "--httpaddr=localhost:0")
+	panicOnError(err)
+	panicOnError(updateVars(h, vars, "TEST_IDENTITYD_NAME", "TEST_IDENTITYD_HTTP_ADDR"))
+
+	bytes, err := json.Marshal(vars)
+	panicOnError(err)
+	fmt.Println(string(bytes))
+
+	// Wait to be killed.
+	select {}
+}
diff --git a/cmd/servicerunner/servicerunner_test.go b/cmd/servicerunner/servicerunner_test.go
new file mode 100644
index 0000000..6f8d0da
--- /dev/null
+++ b/cmd/servicerunner/servicerunner_test.go
@@ -0,0 +1,68 @@
+// Runs the servicerunner binary and checks that it outputs a JSON line to
+// stdout with the expected variables.
+package main
+
+import (
+	"bufio"
+	"encoding/json"
+	"fmt"
+	"io/ioutil"
+	"os"
+	"os/exec"
+	"path"
+	"testing"
+
+	"v.io/x/ref/lib/testutil"
+)
+
+func TestMain(t *testing.T) {
+	testutil.UnsetPrincipalEnvVars()
+	tmpdir, err := ioutil.TempDir("", "servicerunner_test")
+	if err != nil {
+		t.Fatal(err)
+	}
+	defer os.RemoveAll(tmpdir)
+	os.Setenv("TMPDIR", tmpdir)
+
+	bin := path.Join(tmpdir, "servicerunner")
+	fmt.Println("Building", bin)
+	err = exec.Command("v23", "go", "build", "-o", bin, "v.io/x/ref/cmd/servicerunner").Run()
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	cmd := exec.Command(bin)
+	stdout, err := cmd.StdoutPipe()
+	if err != nil {
+		t.Fatal(err)
+	}
+	if err = cmd.Start(); err != nil {
+		t.Fatal(err)
+	}
+
+	line, err := bufio.NewReader(stdout).ReadBytes('\n')
+	if err != nil {
+		t.Fatal(err)
+	}
+	vars := map[string]string{}
+	if err = json.Unmarshal(line, &vars); err != nil {
+		t.Fatal(err)
+	}
+	fmt.Println(vars)
+	expectedVars := []string{
+		"MT_NAME",
+		"PROXY_NAME",
+		"WSPR_ADDR",
+		"TEST_IDENTITYD_NAME",
+		"TEST_IDENTITYD_HTTP_ADDR",
+	}
+	for _, name := range expectedVars {
+		if _, ok := vars[name]; !ok {
+			t.Error("Missing", name)
+		}
+	}
+
+	if err != cmd.Process.Kill() {
+		t.Fatal(err)
+	}
+}
diff --git a/cmd/uniqueid/doc.go b/cmd/uniqueid/doc.go
new file mode 100644
index 0000000..5fdbcab
--- /dev/null
+++ b/cmd/uniqueid/doc.go
@@ -0,0 +1,57 @@
+// This file was auto-generated via go generate.
+// DO NOT UPDATE MANUALLY
+
+/*
+The uniqueid tool generates unique ids. It also has an option of automatically
+substituting unique ids with placeholders in files.
+
+Usage:
+   uniqueid <command>
+
+The uniqueid commands are:
+   generate    Generates UniqueIds
+   inject      Injects UniqueIds into existing files
+   help        Display help for commands or topics
+Run "uniqueid help [command]" for command usage.
+
+Uniqueid Generate
+
+Generates unique ids and outputs them to standard out.
+
+Usage:
+   uniqueid generate
+
+Uniqueid Inject
+
+Injects UniqueIds into existing files. Strings of the form "$UNIQUEID$" will be
+replaced with generated ids.
+
+Usage:
+   uniqueid inject <filenames>
+
+<filenames> List of files to inject unique ids into
+
+Uniqueid Help
+
+Help with no args displays the usage of the parent command.
+
+Help with args displays the usage of the specified sub-command or help topic.
+
+"help ..." recursively displays help for all commands and topics.
+
+The output is formatted to a target width in runes.  The target width is
+determined by checking the environment variable CMDLINE_WIDTH, falling back on
+the terminal width from the OS, falling back on 80 chars.  By setting
+CMDLINE_WIDTH=x, if x > 0 the width is x, if x < 0 the width is unlimited, and
+if x == 0 or is unset one of the fallbacks is used.
+
+Usage:
+   uniqueid help [flags] [command/topic ...]
+
+[command/topic ...] optionally identifies a specific sub-command or help topic.
+
+The uniqueid help flags are:
+ -style=text
+   The formatting style for help output, either "text" or "godoc".
+*/
+package main
diff --git a/cmd/uniqueid/main.go b/cmd/uniqueid/main.go
new file mode 100644
index 0000000..efc4c41
--- /dev/null
+++ b/cmd/uniqueid/main.go
@@ -0,0 +1,110 @@
+// The following enables go generate to generate the doc.go file.
+//go:generate go run $VANADIUM_ROOT/release/go/src/v.io/x/lib/cmdline/testdata/gendoc.go .
+
+package main
+
+import (
+	"bytes"
+	"fmt"
+	"io/ioutil"
+	"os"
+	"regexp"
+
+	"v.io/v23/uniqueid"
+	"v.io/x/lib/cmdline"
+)
+
+func main() {
+	os.Exit(cmdUniqueId.Main())
+}
+
+var cmdUniqueId = &cmdline.Command{
+	Name:  "uniqueid",
+	Short: "Generates UniqueIds.",
+	Long: `
+The uniqueid tool generates unique ids.
+It also has an option of automatically substituting unique ids with placeholders in files.
+`,
+	Children: []*cmdline.Command{cmdGenerate, cmdInject},
+	Topics:   []cmdline.Topic{},
+}
+
+var cmdGenerate = &cmdline.Command{
+	Run:   runGenerate,
+	Name:  "generate",
+	Short: "Generates UniqueIds",
+	Long: `
+Generates unique ids and outputs them to standard out.
+`,
+	ArgsName: "",
+	ArgsLong: "",
+}
+
+var cmdInject = &cmdline.Command{
+	Run:   runInject,
+	Name:  "inject",
+	Short: "Injects UniqueIds into existing files",
+	Long: `
+Injects UniqueIds into existing files.
+Strings of the form "$UNIQUEID$" will be replaced with generated ids.
+`,
+	ArgsName: "<filenames>",
+	ArgsLong: "<filenames> List of files to inject unique ids into",
+}
+
+// runGenerate implements the generate command which outputs generated ids to stdout.
+func runGenerate(command *cmdline.Command, args []string) error {
+	if len(args) > 0 {
+		return command.UsageErrorf("expected 0 args, got %d", len(args))
+	}
+	id, err := uniqueid.Random()
+	if err != nil {
+		return err
+	}
+	fmt.Printf("%#v\n", id)
+	return nil
+}
+
+// runInject implements the inject command which replaces $UNIQUEID$ strings with generated ids.
+func runInject(command *cmdline.Command, args []string) error {
+	if len(args) == 0 {
+		return command.UsageErrorf("expected at least one file arg, got 0")
+	}
+	for _, arg := range args {
+		if err := injectIntoFile(arg); err != nil {
+			return err
+		}
+	}
+	return nil
+}
+
+// injectIntoFile replaces $UNIQUEID$ strings when they exist in the specified file.
+func injectIntoFile(filename string) error {
+	inbytes, err := ioutil.ReadFile(filename)
+	if err != nil {
+		return err
+	}
+
+	// Replace $UNIQUEID$ with generated ids.
+	re, err := regexp.Compile("[$]UNIQUEID")
+	if err != nil {
+		return err
+	}
+	replaced := re.ReplaceAllFunc(inbytes, func(match []byte) []byte {
+		id, randErr := uniqueid.Random()
+		if randErr != nil {
+			err = randErr
+		}
+		return []byte(fmt.Sprintf("%#v", id))
+	})
+	if err != nil {
+		return err
+	}
+
+	// If the file with injections is different, write it to disk.
+	if !bytes.Equal(inbytes, replaced) {
+		fmt.Printf("Updated: %s\n", filename)
+		return ioutil.WriteFile(filename, replaced, 0)
+	}
+	return nil
+}
diff --git a/cmd/vdl/arith_test.go b/cmd/vdl/arith_test.go
new file mode 100644
index 0000000..ddc636e
--- /dev/null
+++ b/cmd/vdl/arith_test.go
@@ -0,0 +1,481 @@
+package main_test
+
+// This test assumes the vdl packages under veyron2/vdl/testdata have been
+// compiled using the vdl binary, and runs end-to-end ipc tests against the
+// generated output.  It's meant as a final sanity check of the vdl compiler; by
+// using the compiled results we're behaving as an end-user would behave.
+
+import (
+	"errors"
+	"math"
+	"reflect"
+	"testing"
+
+	"v.io/v23"
+	"v.io/v23/context"
+	"v.io/v23/ipc"
+	"v.io/v23/vdl"
+	"v.io/x/ref/lib/vdl/testdata/arith"
+	"v.io/x/ref/lib/vdl/testdata/base"
+
+	"v.io/x/ref/lib/testutil"
+	_ "v.io/x/ref/profiles"
+)
+
+var generatedError = errors.New("generated error")
+
+func newServer(ctx *context.T) ipc.Server {
+	s, err := v23.NewServer(ctx)
+	if err != nil {
+		panic(err)
+	}
+	return s
+}
+
+// serverArith implements the arith.Arith interface.
+type serverArith struct{}
+
+func (*serverArith) Add(_ ipc.ServerCall, A, B int32) (int32, error) {
+	return A + B, nil
+}
+
+func (*serverArith) DivMod(_ ipc.ServerCall, A, B int32) (int32, int32, error) {
+	return A / B, A % B, nil
+}
+
+func (*serverArith) Sub(_ ipc.ServerCall, args base.Args) (int32, error) {
+	return args.A - args.B, nil
+}
+
+func (*serverArith) Mul(_ ipc.ServerCall, nestedArgs base.NestedArgs) (int32, error) {
+	return nestedArgs.Args.A * nestedArgs.Args.B, nil
+}
+
+func (*serverArith) Count(ctx arith.ArithCountContext, start int32) error {
+	const kNum = 1000
+	for i := int32(0); i < kNum; i++ {
+		if err := ctx.SendStream().Send(start + i); err != nil {
+			return err
+		}
+	}
+	return nil
+}
+
+func (*serverArith) StreamingAdd(ctx arith.ArithStreamingAddContext) (int32, error) {
+	var total int32
+	for ctx.RecvStream().Advance() {
+		value := ctx.RecvStream().Value()
+		total += value
+		ctx.SendStream().Send(total)
+	}
+	return total, ctx.RecvStream().Err()
+}
+
+func (*serverArith) GenError(_ ipc.ServerCall) error {
+	return generatedError
+}
+
+func (*serverArith) QuoteAny(_ ipc.ServerCall, any *vdl.Value) (*vdl.Value, error) {
+	return vdl.StringValue(any.String()), nil
+}
+
+type serverCalculator struct {
+	serverArith
+}
+
+func (*serverCalculator) Sine(_ ipc.ServerCall, angle float64) (float64, error) {
+	return math.Sin(angle), nil
+}
+
+func (*serverCalculator) Cosine(_ ipc.ServerCall, angle float64) (float64, error) {
+	return math.Cos(angle), nil
+}
+
+func (*serverCalculator) Exp(_ ipc.ServerCall, x float64) (float64, error) {
+	return math.Exp(x), nil
+}
+
+func (*serverCalculator) On(_ ipc.ServerCall) error {
+	return nil
+}
+
+func (*serverCalculator) Off(_ ipc.ServerCall) error {
+	return nil
+}
+
+func TestCalculator(t *testing.T) {
+	ctx, shutdown := testutil.InitForTest()
+	defer shutdown()
+
+	server := newServer(ctx)
+	eps, err := server.Listen(v23.GetListenSpec(ctx))
+	if err := server.Serve("", arith.CalculatorServer(&serverCalculator{}), nil); err != nil {
+		t.Fatal(err)
+	}
+	root := eps[0].Name()
+	// Synchronous calls
+	calculator := arith.CalculatorClient(root)
+	sine, err := calculator.Sine(ctx, 0)
+	if err != nil {
+		t.Errorf("Sine: got %q but expected no error", err)
+	}
+	if sine != 0 {
+		t.Errorf("Sine: expected 0 got %f", sine)
+	}
+	cosine, err := calculator.Cosine(ctx, 0)
+	if err != nil {
+		t.Errorf("Cosine: got %q but expected no error", err)
+	}
+	if cosine != 1 {
+		t.Errorf("Cosine: expected 1 got %f", cosine)
+	}
+
+	ar := arith.ArithClient(root)
+	sum, err := ar.Add(ctx, 7, 8)
+	if err != nil {
+		t.Errorf("Add: got %q but expected no error", err)
+	}
+	if sum != 15 {
+		t.Errorf("Add: expected 15 got %d", sum)
+	}
+	ar = calculator
+	sum, err = ar.Add(ctx, 7, 8)
+	if err != nil {
+		t.Errorf("Add: got %q but expected no error", err)
+	}
+	if sum != 15 {
+		t.Errorf("Add: expected 15 got %d", sum)
+	}
+
+	trig := arith.TrigonometryClient(root)
+	cosine, err = trig.Cosine(ctx, 0)
+	if err != nil {
+		t.Errorf("Cosine: got %q but expected no error", err)
+	}
+	if cosine != 1 {
+		t.Errorf("Cosine: expected 1 got %f", cosine)
+	}
+
+	// Test auto-generated methods.
+	serverStub := arith.CalculatorServer(&serverCalculator{})
+	expectDesc(t, serverStub.Describe__(), []ipc.InterfaceDesc{
+		{
+			Name:    "Calculator",
+			PkgPath: "v.io/x/ref/lib/vdl/testdata/arith",
+			Embeds: []ipc.EmbedDesc{
+				{
+					Name:    "Arith",
+					PkgPath: "v.io/x/ref/lib/vdl/testdata/arith",
+				},
+				{
+					Name:    "AdvancedMath",
+					PkgPath: "v.io/x/ref/lib/vdl/testdata/arith",
+				},
+			},
+			Methods: []ipc.MethodDesc{
+				{Name: "On"},
+				{Name: "Off", Tags: []*vdl.Value{vdl.StringValue("offtag")}},
+			},
+		},
+		{
+			Name:    "Arith",
+			PkgPath: "v.io/x/ref/lib/vdl/testdata/arith",
+			Methods: []ipc.MethodDesc{
+				{
+					Name:    "Add",
+					InArgs:  []ipc.ArgDesc{{Name: "a"}, {Name: "b"}},
+					OutArgs: []ipc.ArgDesc{{}},
+				},
+				{
+					Name:    "DivMod",
+					InArgs:  []ipc.ArgDesc{{Name: "a"}, {Name: "b"}},
+					OutArgs: []ipc.ArgDesc{{Name: "quot"}, {Name: "rem"}},
+				},
+				{
+					Name:    "Sub",
+					InArgs:  []ipc.ArgDesc{{Name: "args"}},
+					OutArgs: []ipc.ArgDesc{{}},
+				},
+				{
+					Name:    "Mul",
+					InArgs:  []ipc.ArgDesc{{Name: "nested"}},
+					OutArgs: []ipc.ArgDesc{{}},
+				},
+				{
+					Name: "GenError",
+					Tags: []*vdl.Value{vdl.StringValue("foo"), vdl.StringValue("barz"), vdl.StringValue("hello"), vdl.Int32Value(129), vdl.Uint64Value(0x24)},
+				},
+				{
+					Name:   "Count",
+					InArgs: []ipc.ArgDesc{{Name: "start"}},
+				},
+				{
+					Name:    "StreamingAdd",
+					OutArgs: []ipc.ArgDesc{{Name: "total"}},
+				},
+				{
+					Name:    "QuoteAny",
+					InArgs:  []ipc.ArgDesc{{Name: "a"}},
+					OutArgs: []ipc.ArgDesc{{}},
+				},
+			},
+		},
+		{
+			Name:    "AdvancedMath",
+			PkgPath: "v.io/x/ref/lib/vdl/testdata/arith",
+			Embeds: []ipc.EmbedDesc{
+				{
+					Name:    "Trigonometry",
+					PkgPath: "v.io/x/ref/lib/vdl/testdata/arith",
+				},
+				{
+					Name:    "Exp",
+					PkgPath: "v.io/x/ref/lib/vdl/testdata/arith/exp",
+				}},
+		},
+		{
+			Name:    "Trigonometry",
+			PkgPath: "v.io/x/ref/lib/vdl/testdata/arith",
+			Doc:     "// Trigonometry is an interface that specifies a couple trigonometric functions.",
+			Methods: []ipc.MethodDesc{
+				{
+					Name: "Sine",
+					InArgs: []ipc.ArgDesc{
+						{"angle", ``}, // float64
+					},
+					OutArgs: []ipc.ArgDesc{
+						{"", ``}, // float64
+					},
+				},
+				{
+					Name: "Cosine",
+					InArgs: []ipc.ArgDesc{
+						{"angle", ``}, // float64
+					},
+					OutArgs: []ipc.ArgDesc{
+						{"", ``}, // float64
+					},
+				},
+			},
+		},
+		{
+			Name:    "Exp",
+			PkgPath: "v.io/x/ref/lib/vdl/testdata/arith/exp",
+			Methods: []ipc.MethodDesc{
+				{
+					Name: "Exp",
+					InArgs: []ipc.ArgDesc{
+						{"x", ``}, // float64
+					},
+					OutArgs: []ipc.ArgDesc{
+						{"", ``}, // float64
+					},
+				},
+			},
+		},
+	})
+}
+
+func TestArith(t *testing.T) {
+	ctx, shutdown := testutil.InitForTest()
+	defer shutdown()
+
+	// TODO(bprosnitz) Split this test up -- it is quite long and hard to debug.
+
+	// We try a few types of dispatchers on the server side, to verify that
+	// anything dispatching to Arith or an interface embedding Arith (like
+	// Calculator) works for a client looking to talk to an Arith service.
+	objects := []interface{}{
+		arith.ArithServer(&serverArith{}),
+		arith.ArithServer(&serverCalculator{}),
+		arith.CalculatorServer(&serverCalculator{}),
+	}
+
+	for i, obj := range objects {
+		server := newServer(ctx)
+		defer server.Stop()
+		eps, err := server.Listen(v23.GetListenSpec(ctx))
+		if err != nil {
+			t.Fatal(err)
+		}
+		root := eps[0].Name()
+		if err := server.Serve("", obj, nil); err != nil {
+			t.Fatalf("%d: %v", i, err)
+		}
+		// Synchronous calls
+		ar := arith.ArithClient(root)
+		sum, err := ar.Add(ctx, 7, 8)
+		if err != nil {
+			t.Errorf("Add: got %q but expected no error", err)
+		}
+		if sum != 15 {
+			t.Errorf("Add: expected 15 got %d", sum)
+		}
+		q, r, err := ar.DivMod(ctx, 7, 3)
+		if err != nil {
+			t.Errorf("DivMod: got %q but expected no error", err)
+		}
+		if q != 2 || r != 1 {
+			t.Errorf("DivMod: expected (2,1) got (%d,%d)", q, r)
+		}
+		diff, err := ar.Sub(ctx, base.Args{7, 8})
+		if err != nil {
+			t.Errorf("Sub: got %q but expected no error", err)
+		}
+		if diff != -1 {
+			t.Errorf("Sub: got %d, expected -1", diff)
+		}
+		prod, err := ar.Mul(ctx, base.NestedArgs{base.Args{7, 8}})
+		if err != nil {
+			t.Errorf("Mul: got %q, but expected no error", err)
+		}
+		if prod != 56 {
+			t.Errorf("Sub: got %d, expected 56", prod)
+		}
+		stream, err := ar.Count(ctx, 35)
+		if err != nil {
+			t.Fatalf("error while executing Count %v", err)
+		}
+
+		countIterator := stream.RecvStream()
+		for i := int32(0); i < 1000; i++ {
+			if !countIterator.Advance() {
+				t.Errorf("Error getting value %v", countIterator.Err())
+			}
+			val := countIterator.Value()
+			if val != 35+i {
+				t.Errorf("Expected value %d, got %d", 35+i, val)
+			}
+		}
+		if countIterator.Advance() || countIterator.Err() != nil {
+			t.Errorf("Reply stream should have been closed %v", countIterator.Err())
+		}
+
+		if err := stream.Finish(); err != nil {
+			t.Errorf("Count failed with %v", err)
+		}
+
+		addStream, err := ar.StreamingAdd(ctx)
+
+		go func() {
+			sender := addStream.SendStream()
+			for i := int32(0); i < 100; i++ {
+				if err := sender.Send(i); err != nil {
+					t.Errorf("Send error %v", err)
+				}
+			}
+			if err := sender.Close(); err != nil {
+				t.Errorf("Close error %v", err)
+			}
+		}()
+
+		var expectedSum int32
+		rStream := addStream.RecvStream()
+		for i := int32(0); i < 100; i++ {
+			expectedSum += i
+			if !rStream.Advance() {
+				t.Errorf("Error getting value %v", rStream.Err())
+			}
+			value := rStream.Value()
+			if value != expectedSum {
+				t.Errorf("Got %d but expected %d", value, expectedSum)
+			}
+		}
+
+		if rStream.Advance() || rStream.Err() != nil {
+			t.Errorf("Reply stream should have been closed %v", rStream.Err())
+		}
+
+		total, err := addStream.Finish()
+
+		if err != nil {
+			t.Errorf("Count failed with %v", err)
+		}
+
+		if total != expectedSum {
+			t.Errorf("Got %d but expexted %d", total, expectedSum)
+		}
+
+		if err := ar.GenError(ctx); err == nil {
+			t.Errorf("GenError: got %v but expected %v", err, generatedError)
+		}
+
+		// Server-side stubs
+
+		serverStub := arith.ArithServer(&serverArith{})
+		expectDesc(t, serverStub.Describe__(), []ipc.InterfaceDesc{
+			{
+				Name:    "Arith",
+				PkgPath: "v.io/x/ref/lib/vdl/testdata/arith",
+				Methods: []ipc.MethodDesc{
+					{
+						Name:    "Add",
+						InArgs:  []ipc.ArgDesc{{Name: "a"}, {Name: "b"}},
+						OutArgs: []ipc.ArgDesc{{}},
+					},
+					{
+						Name:    "DivMod",
+						InArgs:  []ipc.ArgDesc{{Name: "a"}, {Name: "b"}},
+						OutArgs: []ipc.ArgDesc{{Name: "quot"}, {Name: "rem"}},
+					},
+					{
+						Name:    "Sub",
+						InArgs:  []ipc.ArgDesc{{Name: "args"}},
+						OutArgs: []ipc.ArgDesc{{}},
+					},
+					{
+						Name:    "Mul",
+						InArgs:  []ipc.ArgDesc{{Name: "nested"}},
+						OutArgs: []ipc.ArgDesc{{}},
+					},
+					{
+						Name: "GenError",
+						Tags: []*vdl.Value{vdl.StringValue("foo"), vdl.StringValue("barz"), vdl.StringValue("hello"), vdl.Int32Value(129), vdl.Uint64Value(0x24)},
+					},
+					{
+						Name:   "Count",
+						InArgs: []ipc.ArgDesc{{Name: "start"}},
+					},
+					{
+						Name:    "StreamingAdd",
+						OutArgs: []ipc.ArgDesc{{Name: "total"}},
+					},
+					{
+						Name:    "QuoteAny",
+						InArgs:  []ipc.ArgDesc{{Name: "a"}},
+						OutArgs: []ipc.ArgDesc{{}},
+					},
+				},
+			},
+		})
+	}
+}
+
+func expectDesc(t *testing.T, got, want []ipc.InterfaceDesc) {
+	stripDesc(got)
+	stripDesc(want)
+	if !reflect.DeepEqual(got, want) {
+		t.Errorf("Describe__ got %#v, want %#v", got, want)
+	}
+}
+
+func stripDesc(desc []ipc.InterfaceDesc) {
+	// Don't bother testing the documentation, to avoid spurious changes.
+	for i := range desc {
+		desc[i].Doc = ""
+		for j := range desc[i].Embeds {
+			desc[i].Embeds[j].Doc = ""
+		}
+		for j := range desc[i].Methods {
+			desc[i].Methods[j].Doc = ""
+			for k := range desc[i].Methods[j].InArgs {
+				desc[i].Methods[j].InArgs[k].Doc = ""
+			}
+			for k := range desc[i].Methods[j].OutArgs {
+				desc[i].Methods[j].OutArgs[k].Doc = ""
+			}
+			desc[i].Methods[j].InStream.Doc = ""
+			desc[i].Methods[j].OutStream.Doc = ""
+		}
+	}
+}
diff --git a/cmd/vdl/doc.go b/cmd/vdl/doc.go
new file mode 100644
index 0000000..e4831a0
--- /dev/null
+++ b/cmd/vdl/doc.go
@@ -0,0 +1,279 @@
+// This file was auto-generated via go generate.
+// DO NOT UPDATE MANUALLY
+
+/*
+The vdl tool manages veyron VDL source code.  It's similar to the go tool used
+for managing Go source code.
+
+Usage:
+   vdl [flags] <command>
+
+The vdl commands are:
+   generate    Compile packages and dependencies, and generate code
+   compile     Compile packages and dependencies, but don't generate code
+   audit       Check if any packages are stale and need generation
+   list        List package and dependency info in transitive order
+   help        Display help for commands or topics
+Run "vdl help [command]" for command usage.
+
+The vdl additional help topics are:
+   packages    Description of package lists
+   vdlpath     Description of VDLPATH environment variable
+   vdlroot     Description of VDLROOT environment variable
+   vdl.config  Description of vdl.config files
+Run "vdl help [topic]" for topic details.
+
+The vdl flags are:
+ -exts=.vdl
+   Comma-separated list of valid VDL file name extensions.
+ -ignore_unknown=false
+   Ignore unknown packages provided on the command line.
+ -max_errors=-1
+   Stop processing after this many errors, or -1 for unlimited.
+ -v=false
+   Turn on verbose logging.
+ -vdl.config=vdl.config
+   Basename of the optional per-package config file.
+
+Vdl Generate
+
+Generate compiles packages and their transitive dependencies, and generates code
+in the specified languages.
+
+Usage:
+   vdl generate [flags] <packages>
+
+<packages> are a list of packages to process, similar to the standard go tool.
+For more information, run "vdl help packages".
+
+The vdl generate flags are:
+ -go_out_dir=
+   Go output directory.  There are three modes:
+      ""                     : Generate output in-place in the source tree
+      "dir"                  : Generate output rooted at dir
+      "src->dst[,s2->d2...]" : Generate output using translation rules
+   Assume your source tree is organized as follows:
+      VDLPATH=/home/vdl
+         /home/vdl/src/veyron/test_base/base1.vdl
+         /home/vdl/src/veyron/test_base/base2.vdl
+   Here's example output under the different modes:
+      --go_out_dir=""
+         /home/vdl/src/veyron/test_base/base1.vdl.go
+         /home/vdl/src/veyron/test_base/base2.vdl.go
+      --go_out_dir="/tmp/foo"
+         /tmp/foo/veyron/test_base/base1.vdl.go
+         /tmp/foo/veyron/test_base/base2.vdl.go
+      --go_out_dir="vdl/src->foo/bar/src"
+         /home/foo/bar/src/veyron/test_base/base1.vdl.go
+         /home/foo/bar/src/veyron/test_base/base2.vdl.go
+   When the src->dst form is used, src must match the suffix of the path just
+   before the package path, and dst is the replacement for src.  Use commas to
+   separate multiple rules; the first rule matching src is used.  The special
+   dst SKIP indicates matching packages are skipped.
+ -java_out_dir=go/src->java/src/vdl/java
+   Same semantics as --go_out_dir but applies to java code generation.
+ -java_out_pkg=v.io->io/v
+   Java output package translation rules.  Must be of the form:
+      "src->dst[,s2->d2...]"
+   If a VDL package has a prefix src, the prefix will be replaced with dst.  Use
+   commas to separate multiple rules; the first rule matching src is used, and
+   if there are no matching rules, the package remains unchanged.  The special
+   dst SKIP indicates matching packages are skipped.
+ -js_out_dir=release/go/src->release/javascript/core/src,roadmap/go/src->release/javascript/core/src,third_party/go/src->SKIP,tools/go/src->SKIP,release/go/src/v.io/v23/vdlroot->SKIP
+   Same semantics as --go_out_dir but applies to js code generation.
+ -js_relative_path_to_core=
+   If set, this is the relative path from js_out_dir to the root of the JS core
+ -lang=Go,Java
+   Comma-separated list of languages to generate, currently supporting
+   Go,Java,Javascript
+ -status=true
+   Show package names as they are updated
+
+Vdl Compile
+
+Compile compiles packages and their transitive dependencies, but does not
+generate code.  This is useful to sanity-check that your VDL files are valid.
+
+Usage:
+   vdl compile [flags] <packages>
+
+<packages> are a list of packages to process, similar to the standard go tool.
+For more information, run "vdl help packages".
+
+The vdl compile flags are:
+ -status=true
+   Show package names while we compile
+
+Vdl Audit
+
+Audit runs the same logic as generate, but doesn't write out generated files.
+Returns a 0 exit code if all packages are up-to-date, otherwise returns a non-0
+exit code indicating some packages need generation.
+
+Usage:
+   vdl audit [flags] <packages>
+
+<packages> are a list of packages to process, similar to the standard go tool.
+For more information, run "vdl help packages".
+
+The vdl audit flags are:
+ -go_out_dir=
+   Go output directory.  There are three modes:
+      ""                     : Generate output in-place in the source tree
+      "dir"                  : Generate output rooted at dir
+      "src->dst[,s2->d2...]" : Generate output using translation rules
+   Assume your source tree is organized as follows:
+      VDLPATH=/home/vdl
+         /home/vdl/src/veyron/test_base/base1.vdl
+         /home/vdl/src/veyron/test_base/base2.vdl
+   Here's example output under the different modes:
+      --go_out_dir=""
+         /home/vdl/src/veyron/test_base/base1.vdl.go
+         /home/vdl/src/veyron/test_base/base2.vdl.go
+      --go_out_dir="/tmp/foo"
+         /tmp/foo/veyron/test_base/base1.vdl.go
+         /tmp/foo/veyron/test_base/base2.vdl.go
+      --go_out_dir="vdl/src->foo/bar/src"
+         /home/foo/bar/src/veyron/test_base/base1.vdl.go
+         /home/foo/bar/src/veyron/test_base/base2.vdl.go
+   When the src->dst form is used, src must match the suffix of the path just
+   before the package path, and dst is the replacement for src.  Use commas to
+   separate multiple rules; the first rule matching src is used.  The special
+   dst SKIP indicates matching packages are skipped.
+ -java_out_dir=go/src->java/src/vdl/java
+   Same semantics as --go_out_dir but applies to java code generation.
+ -java_out_pkg=v.io->io/v
+   Java output package translation rules.  Must be of the form:
+      "src->dst[,s2->d2...]"
+   If a VDL package has a prefix src, the prefix will be replaced with dst.  Use
+   commas to separate multiple rules; the first rule matching src is used, and
+   if there are no matching rules, the package remains unchanged.  The special
+   dst SKIP indicates matching packages are skipped.
+ -js_out_dir=release/go/src->release/javascript/core/src,roadmap/go/src->release/javascript/core/src,third_party/go/src->SKIP,tools/go/src->SKIP,release/go/src/v.io/v23/vdlroot->SKIP
+   Same semantics as --go_out_dir but applies to js code generation.
+ -js_relative_path_to_core=
+   If set, this is the relative path from js_out_dir to the root of the JS core
+ -lang=Go,Java
+   Comma-separated list of languages to generate, currently supporting
+   Go,Java,Javascript
+ -status=true
+   Show package names as they are updated
+
+Vdl List
+
+List returns information about packages and their transitive dependencies, in
+transitive order.  This is the same order the generate and compile commands use
+for processing.  If "vdl list A" is run and A depends on B, which depends on C,
+the returned order will be C, B, A.  If multiple packages are specified the
+ordering is over all combined dependencies.
+
+Reminder: cyclic dependencies between packages are not allowed.  Cyclic
+dependencies between VDL files within the same package are also not allowed.
+This is more strict than regular Go; it makes it easier to generate code for
+other languages like C++.
+
+Usage:
+   vdl list <packages>
+
+<packages> are a list of packages to process, similar to the standard go tool.
+For more information, run "vdl help packages".
+
+Vdl Help
+
+Help with no args displays the usage of the parent command.
+
+Help with args displays the usage of the specified sub-command or help topic.
+
+"help ..." recursively displays help for all commands and topics.
+
+The output is formatted to a target width in runes.  The target width is
+determined by checking the environment variable CMDLINE_WIDTH, falling back on
+the terminal width from the OS, falling back on 80 chars.  By setting
+CMDLINE_WIDTH=x, if x > 0 the width is x, if x < 0 the width is unlimited, and
+if x == 0 or is unset one of the fallbacks is used.
+
+Usage:
+   vdl help [flags] [command/topic ...]
+
+[command/topic ...] optionally identifies a specific sub-command or help topic.
+
+The vdl help flags are:
+ -style=text
+   The formatting style for help output, either "text" or "godoc".
+
+Vdl Packages - help topic
+
+Most vdl commands apply to a list of packages:
+
+   vdl command <packages>
+
+<packages> are a list of packages to process, similar to the standard go tool.
+In its simplest form each package is an import path; e.g.
+   "v.io/x/ref/lib/vdl"
+
+A package that is an absolute path or that begins with a . or .. element is
+interpreted as a file system path, and denotes the package in that directory.
+
+A package is a pattern if it includes one or more "..." wildcards, each of which
+can match any string, including the empty string and strings containing slashes.
+Such a pattern expands to all packages found in VDLPATH with names matching the
+pattern.  As a special-case, x/... matches x as well as x's subdirectories.
+
+The special-case "all" is a synonym for "...", and denotes all packages found in
+VDLPATH.
+
+Import path elements and file names are not allowed to begin with "." or "_";
+such paths are ignored in wildcard matches, and return errors if specified
+explicitly.
+
+ Run "vdl help vdlpath" to see docs on VDLPATH.
+ Run "go help packages" to see the standard go package docs.
+
+Vdl Vdlpath - help topic
+
+The VDLPATH environment variable is used to resolve import statements. It must
+be set to compile and generate vdl packages.
+
+The format is a colon-separated list of directories, where each directory must
+have a "src/" directory that holds vdl source code.  The path below 'src'
+determines the import path.  If VDLPATH specifies multiple directories, imports
+are resolved by picking the first directory with a matching import name.
+
+An example:
+
+   VDPATH=/home/user/vdlA:/home/user/vdlB
+
+   /home/user/vdlA/
+      src/
+         foo/                 (import "foo" refers here)
+            foo1.vdl
+   /home/user/vdlB/
+      src/
+         foo/                 (this package is ignored)
+            foo2.vdl
+         bar/
+            baz/              (import "bar/baz" refers here)
+               baz.vdl
+
+Vdl Vdlroot - help topic
+
+The VDLROOT environment variable is similar to VDLPATH, but instead of pointing
+to multiple user source directories, it points at a single source directory
+containing the standard vdl packages.
+
+Setting VDLROOT is optional.
+
+If VDLROOT is empty, we try to construct it out of the VANADIUM_ROOT environment
+variable.  It is an error if both VDLROOT and VANADIUM_ROOT are empty.
+
+Vdl Vdl.Config - help topic
+
+Each vdl source package P may contain an optional file "vdl.config" within the P
+directory.  This file specifies additional configuration for the vdl tool.
+
+The format of this file is described by the vdltool.Config type in the "vdltool"
+standard package, located at VDLROOT/vdltool/config.vdl.
+
+If the file does not exist, we use the zero value of vdl.Config.
+*/
+package main
diff --git a/cmd/vdl/main.go b/cmd/vdl/main.go
new file mode 100644
index 0000000..bcd20ee
--- /dev/null
+++ b/cmd/vdl/main.go
@@ -0,0 +1,642 @@
+// The following enables go generate to generate the doc.go file.
+//go:generate go run $VANADIUM_ROOT/release/go/src/v.io/x/lib/cmdline/testdata/gendoc.go .
+
+package main
+
+import (
+	"bytes"
+	"fmt"
+	"io/ioutil"
+	"log"
+	"os"
+	"path/filepath"
+	"strings"
+
+	"v.io/v23/vdlroot/vdltool"
+	"v.io/x/lib/cmdline"
+	"v.io/x/lib/textutil"
+	"v.io/x/ref/lib/vdl/build"
+	"v.io/x/ref/lib/vdl/codegen/golang"
+	"v.io/x/ref/lib/vdl/codegen/java"
+	"v.io/x/ref/lib/vdl/codegen/javascript"
+	"v.io/x/ref/lib/vdl/compile"
+	"v.io/x/ref/lib/vdl/vdlutil"
+)
+
+func init() {
+	log.SetFlags(log.Lshortfile | log.Ltime | log.Lmicroseconds)
+}
+
+func main() {
+	os.Exit(cmdVDL.Main())
+}
+
+func checkErrors(errs *vdlutil.Errors) error {
+	if errs.IsEmpty() {
+		return nil
+	}
+	return fmt.Errorf(`
+%s   (run with "vdl -v" for verbose logging or "vdl help" for help)`, errs)
+}
+
+// runHelper returns a function that generates a sorted list of transitive
+// targets, and calls the supplied run function.
+func runHelper(run func(targets []*build.Package, env *compile.Env)) func(cmd *cmdline.Command, args []string) error {
+	return func(cmd *cmdline.Command, args []string) error {
+		if flagVerbose {
+			vdlutil.SetVerbose()
+		}
+		if len(args) == 0 {
+			// If the user doesn't specify any targets, the cwd is implied.
+			args = append(args, ".")
+		}
+		env := compile.NewEnv(flagMaxErrors)
+		env.DisallowPathQualifiers()
+		mode := build.UnknownPathIsError
+		if flagIgnoreUnknown {
+			mode = build.UnknownPathIsIgnored
+		}
+		var opts build.Opts
+		opts.Extensions = strings.Split(flagExts, ",")
+		opts.VDLConfigName = flagVDLConfig
+		targets := build.TransitivePackages(args, mode, opts, env.Errors)
+		if err := checkErrors(env.Errors); err != nil {
+			return err
+		}
+		run(targets, env)
+		return checkErrors(env.Errors)
+	}
+}
+
+var topicPackages = cmdline.Topic{
+	Name:  "packages",
+	Short: "Description of package lists",
+	Long: `
+Most vdl commands apply to a list of packages:
+
+   vdl command <packages>
+
+<packages> are a list of packages to process, similar to the standard go tool.
+In its simplest form each package is an import path; e.g.
+   "v.io/x/ref/lib/vdl"
+
+A package that is an absolute path or that begins with a . or .. element is
+interpreted as a file system path, and denotes the package in that directory.
+
+A package is a pattern if it includes one or more "..." wildcards, each of which
+can match any string, including the empty string and strings containing
+slashes.  Such a pattern expands to all packages found in VDLPATH with names
+matching the pattern.  As a special-case, x/... matches x as well as x's
+subdirectories.
+
+The special-case "all" is a synonym for "...", and denotes all packages found
+in VDLPATH.
+
+Import path elements and file names are not allowed to begin with "." or "_";
+such paths are ignored in wildcard matches, and return errors if specified
+explicitly.
+
+ Run "vdl help vdlpath" to see docs on VDLPATH.
+ Run "go help packages" to see the standard go package docs.
+`,
+}
+
+var topicVdlPath = cmdline.Topic{
+	Name:  "vdlpath",
+	Short: "Description of VDLPATH environment variable",
+	Long: `
+The VDLPATH environment variable is used to resolve import statements.
+It must be set to compile and generate vdl packages.
+
+The format is a colon-separated list of directories, where each directory must
+have a "src/" directory that holds vdl source code.  The path below 'src'
+determines the import path.  If VDLPATH specifies multiple directories, imports
+are resolved by picking the first directory with a matching import name.
+
+An example:
+
+   VDPATH=/home/user/vdlA:/home/user/vdlB
+
+   /home/user/vdlA/
+      src/
+         foo/                 (import "foo" refers here)
+            foo1.vdl
+   /home/user/vdlB/
+      src/
+         foo/                 (this package is ignored)
+            foo2.vdl
+         bar/
+            baz/              (import "bar/baz" refers here)
+               baz.vdl
+`,
+}
+
+var topicVdlRoot = cmdline.Topic{
+	Name:  "vdlroot",
+	Short: "Description of VDLROOT environment variable",
+	Long: `
+The VDLROOT environment variable is similar to VDLPATH, but instead of pointing
+to multiple user source directories, it points at a single source directory
+containing the standard vdl packages.
+
+Setting VDLROOT is optional.
+
+If VDLROOT is empty, we try to construct it out of the VANADIUM_ROOT environment
+variable.  It is an error if both VDLROOT and VANADIUM_ROOT are empty.
+`,
+}
+
+var topicVdlConfig = cmdline.Topic{
+	Name:  "vdl.config",
+	Short: "Description of vdl.config files",
+	Long: `
+Each vdl source package P may contain an optional file "vdl.config" within the P
+directory.  This file specifies additional configuration for the vdl tool.
+
+The format of this file is described by the vdltool.Config type in the "vdltool"
+standard package, located at VDLROOT/vdltool/config.vdl.
+
+If the file does not exist, we use the zero value of vdl.Config.
+`,
+}
+
+const pkgArgName = "<packages>"
+const pkgArgLong = `
+<packages> are a list of packages to process, similar to the standard go tool.
+For more information, run "vdl help packages".
+`
+
+var cmdCompile = &cmdline.Command{
+	Run:   runHelper(runCompile),
+	Name:  "compile",
+	Short: "Compile packages and dependencies, but don't generate code",
+	Long: `
+Compile compiles packages and their transitive dependencies, but does not
+generate code.  This is useful to sanity-check that your VDL files are valid.
+`,
+	ArgsName: pkgArgName,
+	ArgsLong: pkgArgLong,
+}
+
+var cmdGenerate = &cmdline.Command{
+	Run:   runHelper(runGenerate),
+	Name:  "generate",
+	Short: "Compile packages and dependencies, and generate code",
+	Long: `
+Generate compiles packages and their transitive dependencies, and generates code
+in the specified languages.
+`,
+	ArgsName: pkgArgName,
+	ArgsLong: pkgArgLong,
+}
+
+var cmdAudit = &cmdline.Command{
+	Run:   runHelper(runAudit),
+	Name:  "audit",
+	Short: "Check if any packages are stale and need generation",
+	Long: `
+Audit runs the same logic as generate, but doesn't write out generated files.
+Returns a 0 exit code if all packages are up-to-date, otherwise returns a
+non-0 exit code indicating some packages need generation.
+`,
+	ArgsName: pkgArgName,
+	ArgsLong: pkgArgLong,
+}
+
+var cmdList = &cmdline.Command{
+	Run:   runHelper(runList),
+	Name:  "list",
+	Short: "List package and dependency info in transitive order",
+	Long: `
+List returns information about packages and their transitive dependencies, in
+transitive order.  This is the same order the generate and compile commands use
+for processing.  If "vdl list A" is run and A depends on B, which depends on C,
+the returned order will be C, B, A.  If multiple packages are specified the
+ordering is over all combined dependencies.
+
+Reminder: cyclic dependencies between packages are not allowed.  Cyclic
+dependencies between VDL files within the same package are also not allowed.
+This is more strict than regular Go; it makes it easier to generate code for
+other languages like C++.
+`,
+	ArgsName: pkgArgName,
+	ArgsLong: pkgArgLong,
+}
+
+var genLangAll = genLangs(vdltool.GenLanguageAll)
+
+type genLangs []vdltool.GenLanguage
+
+func (gls genLangs) String() string {
+	var ret string
+	for i, gl := range gls {
+		if i > 0 {
+			ret += ","
+		}
+		ret += gl.String()
+	}
+	return ret
+}
+
+func (gls *genLangs) Set(value string) error {
+	// If the flag is repeated on the cmdline it is overridden.  Duplicates within
+	// the comma separated list are ignored, and retain their original ordering.
+	*gls = genLangs{}
+	seen := make(map[vdltool.GenLanguage]bool)
+	for _, str := range strings.Split(value, ",") {
+		gl, err := vdltool.GenLanguageFromString(str)
+		if err != nil {
+			return err
+		}
+		if !seen[gl] {
+			seen[gl] = true
+			*gls = append(*gls, gl)
+		}
+	}
+	return nil
+}
+
+// genOutDir has three modes:
+//   1) If dir is non-empty, we use it as the out dir.
+//   2) If rules is non-empty, we translate using the xlate rules.
+//   3) If everything is empty, we generate in-place.
+type genOutDir struct {
+	dir   string
+	rules xlateRules
+}
+
+// xlateSrcDst specifies a translation rule, where src must match the suffix of
+// the path just before the package path, and dst is the replacement for src.
+// If dst is the special string "SKIP" we'll skip generation of packages
+// matching the src.
+type xlateSrcDst struct {
+	src, dst string
+}
+
+// xlateRules specifies a collection of translation rules.
+type xlateRules []xlateSrcDst
+
+func (x *xlateRules) String() (ret string) {
+	for _, srcdst := range *x {
+		if len(ret) > 0 {
+			ret += ","
+		}
+		ret += srcdst.src + "->" + srcdst.dst
+	}
+	return
+}
+
+func (x *xlateRules) Set(value string) error {
+	for _, rule := range strings.Split(value, ",") {
+		srcdst := strings.Split(rule, "->")
+		if len(srcdst) != 2 {
+			return fmt.Errorf("invalid out dir xlate rule %q (not src->dst format)", rule)
+		}
+		*x = append(*x, xlateSrcDst{srcdst[0], srcdst[1]})
+	}
+	return nil
+}
+
+func (x *genOutDir) String() string {
+	if x.dir != "" {
+		return x.dir
+	}
+	return x.rules.String()
+}
+
+func (x *genOutDir) Set(value string) error {
+	if strings.Contains(value, "->") {
+		x.dir = ""
+		return x.rules.Set(value)
+	}
+	x.dir = value
+	return nil
+}
+
+var (
+	// Common flags for the tool itself, applicable to all commands.
+	flagVerbose       bool
+	flagMaxErrors     int
+	flagExts          string
+	flagVDLConfig     string
+	flagIgnoreUnknown bool
+
+	// Options for each command.
+	optCompileStatus bool
+	optGenStatus     bool
+	optGenGoOutDir   = genOutDir{}
+	optGenJavaOutDir = genOutDir{
+		rules: xlateRules{
+			{"go/src", "java/src/vdl/java"},
+		},
+	}
+	optGenJavascriptOutDir = genOutDir{
+		rules: xlateRules{
+			{"release/go/src", "release/javascript/core/src"},
+			{"roadmap/go/src", "release/javascript/core/src"},
+			{"third_party/go/src", "SKIP"},
+			{"tools/go/src", "SKIP"},
+			// TODO(toddw): Skip vdlroot javascript generation for now.
+			{"release/go/src/v.io/v23/vdlroot", "SKIP"},
+		},
+	}
+	optGenJavaOutPkg = xlateRules{
+		{"v.io", "io/v"},
+	}
+	optPathToJSCore string
+	// TODO(bjornick): Add javascript to the default gen langs.
+	optGenLangs = genLangs{vdltool.GenLanguageGo, vdltool.GenLanguageJava}
+)
+
+// Root returns the root command for the VDL tool.
+var cmdVDL = &cmdline.Command{
+	Name:  "vdl",
+	Short: "Manage veyron VDL source code",
+	Long: `
+The vdl tool manages veyron VDL source code.  It's similar to the go tool used
+for managing Go source code.
+`,
+	Children: []*cmdline.Command{cmdGenerate, cmdCompile, cmdAudit, cmdList},
+	Topics:   []cmdline.Topic{topicPackages, topicVdlPath, topicVdlRoot, topicVdlConfig},
+}
+
+func init() {
+	// Common flags for the tool itself, applicable to all commands.
+	cmdVDL.Flags.BoolVar(&flagVerbose, "v", false, "Turn on verbose logging.")
+	cmdVDL.Flags.IntVar(&flagMaxErrors, "max_errors", -1, "Stop processing after this many errors, or -1 for unlimited.")
+	cmdVDL.Flags.StringVar(&flagExts, "exts", ".vdl", "Comma-separated list of valid VDL file name extensions.")
+	cmdVDL.Flags.StringVar(&flagVDLConfig, "vdl.config", "vdl.config", "Basename of the optional per-package config file.")
+	cmdVDL.Flags.BoolVar(&flagIgnoreUnknown, "ignore_unknown", false, "Ignore unknown packages provided on the command line.")
+
+	// Options for compile.
+	cmdCompile.Flags.BoolVar(&optCompileStatus, "status", true, "Show package names while we compile")
+
+	// Options for generate.
+	cmdGenerate.Flags.Var(&optGenLangs, "lang", "Comma-separated list of languages to generate, currently supporting "+genLangAll.String())
+	cmdGenerate.Flags.BoolVar(&optGenStatus, "status", true, "Show package names as they are updated")
+	// TODO(toddw): Move out_dir configuration into vdl.config, and provide a
+	// generic override mechanism for vdl.config.
+	cmdGenerate.Flags.Var(&optGenGoOutDir, "go_out_dir", `
+Go output directory.  There are three modes:
+   ""                     : Generate output in-place in the source tree
+   "dir"                  : Generate output rooted at dir
+   "src->dst[,s2->d2...]" : Generate output using translation rules
+Assume your source tree is organized as follows:
+   VDLPATH=/home/vdl
+      /home/vdl/src/veyron/test_base/base1.vdl
+      /home/vdl/src/veyron/test_base/base2.vdl
+Here's example output under the different modes:
+   --go_out_dir=""
+      /home/vdl/src/veyron/test_base/base1.vdl.go
+      /home/vdl/src/veyron/test_base/base2.vdl.go
+   --go_out_dir="/tmp/foo"
+      /tmp/foo/veyron/test_base/base1.vdl.go
+      /tmp/foo/veyron/test_base/base2.vdl.go
+   --go_out_dir="vdl/src->foo/bar/src"
+      /home/foo/bar/src/veyron/test_base/base1.vdl.go
+      /home/foo/bar/src/veyron/test_base/base2.vdl.go
+When the src->dst form is used, src must match the suffix of the path just
+before the package path, and dst is the replacement for src.  Use commas to
+separate multiple rules; the first rule matching src is used.  The special dst
+SKIP indicates matching packages are skipped.`)
+	cmdGenerate.Flags.Var(&optGenJavaOutDir, "java_out_dir",
+		"Same semantics as --go_out_dir but applies to java code generation.")
+	cmdGenerate.Flags.Var(&optGenJavaOutPkg, "java_out_pkg", `
+Java output package translation rules.  Must be of the form:
+   "src->dst[,s2->d2...]"
+If a VDL package has a prefix src, the prefix will be replaced with dst.  Use
+commas to separate multiple rules; the first rule matching src is used, and if
+there are no matching rules, the package remains unchanged.  The special dst
+SKIP indicates matching packages are skipped.`)
+	cmdGenerate.Flags.Var(&optGenJavascriptOutDir, "js_out_dir",
+		"Same semantics as --go_out_dir but applies to js code generation.")
+	cmdGenerate.Flags.StringVar(&optPathToJSCore, "js_relative_path_to_core", "",
+		"If set, this is the relative path from js_out_dir to the root of the JS core")
+
+	// Options for audit are identical to generate.
+	cmdAudit.Flags = cmdGenerate.Flags
+}
+
+func runCompile(targets []*build.Package, env *compile.Env) {
+	for _, target := range targets {
+		pkg := build.BuildPackage(target, env)
+		if pkg != nil && optCompileStatus {
+			fmt.Println(pkg.Path)
+		}
+	}
+}
+
+func runGenerate(targets []*build.Package, env *compile.Env) {
+	gen(false, targets, env)
+}
+
+func runAudit(targets []*build.Package, env *compile.Env) {
+	if gen(true, targets, env) && env.Errors.IsEmpty() {
+		// Some packages are stale, and there were no errors; return an arbitrary
+		// non-0 exit code.  Errors are handled in runHelper, as usual.
+		os.Exit(10)
+	}
+}
+
+func shouldGenerate(config vdltool.Config, lang vdltool.GenLanguage) bool {
+	// If config.GenLanguages is empty, all languages are allowed to be generated.
+	_, ok := config.GenLanguages[lang]
+	return len(config.GenLanguages) == 0 || ok
+}
+
+// gen generates the given targets with env.  If audit is true, only checks
+// whether any packages are stale; otherwise files will actually be written out.
+// Returns true if any packages are stale.
+func gen(audit bool, targets []*build.Package, env *compile.Env) bool {
+	anychanged := false
+	for _, target := range targets {
+		pkg := build.BuildPackage(target, env)
+		if pkg == nil {
+			// Stop at the first package that fails to compile.
+			if env.Errors.IsEmpty() {
+				env.Errors.Errorf("%s: internal error (compiled into nil package)", target.Path)
+			}
+			return true
+		}
+		// TODO(toddw): Skip code generation if the semantic contents of the
+		// generated file haven't changed.
+		pkgchanged := false
+		for _, gl := range optGenLangs {
+			switch gl {
+			case vdltool.GenLanguageGo:
+				if !shouldGenerate(pkg.Config, vdltool.GenLanguageGo) {
+					continue
+				}
+				dir, err := xlateOutDir(target.Dir, target.GenPath, optGenGoOutDir, pkg.GenPath)
+				if handleErrorOrSkip("--go_out_dir", err, env) {
+					continue
+				}
+				for _, file := range pkg.Files {
+					data := golang.Generate(file, env)
+					if writeFile(audit, data, dir, file.BaseName+".go", env) {
+						pkgchanged = true
+					}
+				}
+			case vdltool.GenLanguageJava:
+				if !shouldGenerate(pkg.Config, vdltool.GenLanguageJava) {
+					continue
+				}
+				pkgPath, err := xlatePkgPath(pkg.GenPath, optGenJavaOutPkg)
+				if handleErrorOrSkip("--java_out_pkg", err, env) {
+					continue
+				}
+				dir, err := xlateOutDir(target.Dir, target.GenPath, optGenJavaOutDir, pkgPath)
+				if handleErrorOrSkip("--java_out_dir", err, env) {
+					continue
+				}
+				java.SetPkgPathXlator(func(pkgPath string) string {
+					result, _ := xlatePkgPath(pkgPath, optGenJavaOutPkg)
+					return result
+				})
+				for _, file := range java.Generate(pkg, env) {
+					fileDir := filepath.Join(dir, file.Dir)
+					if writeFile(audit, file.Data, fileDir, file.Name, env) {
+						pkgchanged = true
+					}
+				}
+			case vdltool.GenLanguageJavascript:
+				if !shouldGenerate(pkg.Config, vdltool.GenLanguageJavascript) {
+					continue
+				}
+				dir, err := xlateOutDir(target.Dir, target.GenPath, optGenJavascriptOutDir, pkg.GenPath)
+				if handleErrorOrSkip("--js_out_dir", err, env) {
+					continue
+				}
+				path := func(importPath string) string {
+					prefix := filepath.Clean(target.Dir[0 : len(target.Dir)-len(target.GenPath)])
+					pkgDir := filepath.Join(prefix, filepath.FromSlash(importPath))
+					fullDir, err := xlateOutDir(pkgDir, importPath, optGenJavascriptOutDir, importPath)
+					if err != nil {
+						panic(err)
+					}
+					cleanPath, err := filepath.Rel(dir, fullDir)
+					if err != nil {
+						panic(err)
+					}
+					return cleanPath
+				}
+				data := javascript.Generate(pkg, env, path, optPathToJSCore)
+				if writeFile(audit, data, dir, "index.js", env) {
+					pkgchanged = true
+				}
+			default:
+				env.Errors.Errorf("Generating code for language %v isn't supported", gl)
+			}
+		}
+		if pkgchanged {
+			anychanged = true
+			if optGenStatus {
+				fmt.Println(pkg.Path)
+			}
+		}
+	}
+	return anychanged
+}
+
+// writeFile writes data into the standard location for file, using the given
+// suffix.  Errors are reported via env.  Returns true iff the file doesn't
+// already exist with the given data.
+func writeFile(audit bool, data []byte, dirName, baseName string, env *compile.Env) bool {
+	dstName := filepath.Join(dirName, baseName)
+	// Don't change anything if old and new are the same.
+	if oldData, err := ioutil.ReadFile(dstName); err == nil && bytes.Equal(oldData, data) {
+		return false
+	}
+	if !audit {
+		// Create containing directory, if it doesn't already exist.
+		if err := os.MkdirAll(dirName, os.FileMode(0777)); err != nil {
+			env.Errors.Errorf("Couldn't create directory %s: %v", dirName, err)
+			return true
+		}
+		if err := ioutil.WriteFile(dstName, data, os.FileMode(0666)); err != nil {
+			env.Errors.Errorf("Couldn't write file %s: %v", dstName, err)
+			return true
+		}
+	}
+	return true
+}
+
+func handleErrorOrSkip(prefix string, err error, env *compile.Env) bool {
+	if err != nil {
+		if err != errSkip {
+			env.Errors.Errorf("%s error: %v", prefix, err)
+		}
+		return true
+	}
+	return false
+}
+
+var errSkip = fmt.Errorf("SKIP")
+
+func xlateOutDir(dir, path string, outdir genOutDir, outPkgPath string) (string, error) {
+	path = filepath.FromSlash(path)
+	outPkgPath = filepath.FromSlash(outPkgPath)
+	// Strip package path from the directory.
+	if !strings.HasSuffix(dir, path) {
+		return "", fmt.Errorf("package dir %q doesn't end with package path %q", dir, path)
+	}
+	dir = filepath.Clean(dir[:len(dir)-len(path)])
+
+	switch {
+	case outdir.dir != "":
+		return filepath.Join(outdir.dir, outPkgPath), nil
+	case len(outdir.rules) == 0:
+		return filepath.Join(dir, outPkgPath), nil
+	}
+	// Try translation rules in order.
+	for _, xlate := range outdir.rules {
+		d := dir
+		if !strings.HasSuffix(d, xlate.src) {
+			continue
+		}
+		if xlate.dst == "SKIP" {
+			return "", errSkip
+		}
+		d = filepath.Clean(d[:len(d)-len(xlate.src)])
+		return filepath.Join(d, xlate.dst, outPkgPath), nil
+	}
+	return "", fmt.Errorf("package prefix %q doesn't match translation rules %q", dir, outdir)
+}
+
+func xlatePkgPath(pkgPath string, rules xlateRules) (string, error) {
+	for _, xlate := range rules {
+		if !strings.HasPrefix(pkgPath, xlate.src) {
+			continue
+		}
+		if xlate.dst == "SKIP" {
+			return pkgPath, errSkip
+		}
+		return xlate.dst + pkgPath[len(xlate.src):], nil
+	}
+	return pkgPath, nil
+}
+
+func runList(targets []*build.Package, env *compile.Env) {
+	for tx, target := range targets {
+		num := fmt.Sprintf("%d", tx)
+		fmt.Printf("%s %s\n", num, strings.Repeat("=", termWidth()-len(num)-1))
+		fmt.Printf("Name:    %v\n", target.Name)
+		fmt.Printf("Config:  %+v\n", target.Config)
+		fmt.Printf("Path:    %v\n", target.Path)
+		fmt.Printf("GenPath: %v\n", target.GenPath)
+		fmt.Printf("Dir:     %v\n", target.Dir)
+		if len(target.BaseFileNames) > 0 {
+			fmt.Print("Files:\n")
+			for _, file := range target.BaseFileNames {
+				fmt.Printf("   %v\n", file)
+			}
+		}
+	}
+}
+
+func termWidth() int {
+	if _, width, err := textutil.TerminalSize(); err == nil && width > 0 {
+		return width
+	}
+	return 80 // have a reasonable default
+}
diff --git a/cmd/vdl/vdl_test.go b/cmd/vdl/vdl_test.go
new file mode 100644
index 0000000..93b2f9d
--- /dev/null
+++ b/cmd/vdl/vdl_test.go
@@ -0,0 +1,61 @@
+package main
+
+import (
+	"bytes"
+	"fmt"
+	"io/ioutil"
+	"os"
+	"path/filepath"
+	"strings"
+	"testing"
+)
+
+const (
+	testDir    = "../../lib/vdl/testdata/base"
+	outPkgPath = "v.io/x/ref/lib/vdl/testdata/base"
+)
+
+// Compares generated VDL files against the copy in the repo.
+func TestVDLGenerator(t *testing.T) {
+	// Setup the vdl command-line.
+	cmdVDL.Init(nil, os.Stdout, os.Stderr)
+	// Use vdl to generate Go code from input, into a temporary directory.
+	outDir, err := ioutil.TempDir("", "vdltest")
+	if err != nil {
+		t.Fatalf("TempDir() failed: %v", err)
+	}
+	defer os.RemoveAll(outDir)
+	// TODO(toddw): test the generated java and javascript files too.
+	outOpt := fmt.Sprintf("--go_out_dir=%s", outDir)
+	if err := cmdVDL.Execute([]string{"generate", "--lang=go", outOpt, testDir}); err != nil {
+		t.Fatalf("Execute() failed: %v", err)
+	}
+	// Check that each *.vdl.go file in the testDir matches the generated output.
+	entries, err := ioutil.ReadDir(testDir)
+	if err != nil {
+		t.Fatalf("ReadDir(%v) failed: %v", testDir, err)
+	}
+	numEqual := 0
+	for _, entry := range entries {
+		if !strings.HasSuffix(entry.Name(), ".vdl.go") {
+			continue
+		}
+		testFile := filepath.Join(testDir, entry.Name())
+		testBytes, err := ioutil.ReadFile(testFile)
+		if err != nil {
+			t.Fatalf("ReadFile(%v) failed: %v", testFile, err)
+		}
+		outFile := filepath.Join(outDir, outPkgPath, entry.Name())
+		outBytes, err := ioutil.ReadFile(outFile)
+		if err != nil {
+			t.Fatalf("ReadFile(%v) failed: %v", outFile, err)
+		}
+		if !bytes.Equal(outBytes, testBytes) {
+			t.Fatalf("GOT:\n%v\n\nWANT:\n%v\n", string(outBytes), string(testBytes))
+		}
+		numEqual++
+	}
+	if numEqual == 0 {
+		t.Fatalf("testDir %s has no golden files *.vdl.go", testDir)
+	}
+}
diff --git a/cmd/vom/doc.go b/cmd/vom/doc.go
new file mode 100644
index 0000000..ed87934
--- /dev/null
+++ b/cmd/vom/doc.go
@@ -0,0 +1,87 @@
+// This file was auto-generated via go generate.
+// DO NOT UPDATE MANUALLY
+
+/*
+The vom tool helps debug the Veyron Object Marshaling (vom) protocol.
+
+Usage:
+   vom <command>
+
+The vom commands are:
+   decode      Decode data encoded in the vom format
+   dump        Dump data encoded in the vom format into formatted output
+   help        Display help for commands or topics
+Run "vom help [command]" for command usage.
+
+Vom Decode
+
+Decode decodes data encoded in the vom format.  If no arguments are provided,
+decode reads the data from stdin, otherwise the argument is the data.
+
+By default the data is assumed to be represented in hex, with all whitespace
+anywhere in the data ignored.  Use the -data flag to specify other data
+representations.
+
+Usage:
+   vom decode [flags] [data]
+
+[data] is the data to decode; if not specified, reads from stdin
+
+The vom decode flags are:
+ -data=Hex
+   Data representation, one of [Hex Binary]
+
+Vom Dump
+
+Dump dumps data encoded in the vom format, generating formatted output
+describing each portion of the encoding.  If no arguments are provided, dump
+reads the data from stdin, otherwise the argument is the data.
+
+By default the data is assumed to be represented in hex, with all whitespace
+anywhere in the data ignored.  Use the -data flag to specify other data
+representations.
+
+Calling "vom dump" with no flags and no arguments combines the default stdin
+mode with the default hex mode.  This default mode is special; certain non-hex
+characters may be input to represent commands:
+  . (period)    Calls Dumper.Status to get the current decoding status.
+  ; (semicolon) Calls Dumper.Flush to flush output and start a new message.
+
+This lets you cut-and-paste hex strings into your terminal, and use the commands
+to trigger status or flush calls; i.e. a rudimentary debugging UI.
+
+See v.io/v23/vom.Dumper for details on the dump output.
+
+Usage:
+   vom dump [flags] [data]
+
+[data] is the data to dump; if not specified, reads from stdin
+
+The vom dump flags are:
+ -data=Hex
+   Data representation, one of [Hex Binary]
+
+Vom Help
+
+Help with no args displays the usage of the parent command.
+
+Help with args displays the usage of the specified sub-command or help topic.
+
+"help ..." recursively displays help for all commands and topics.
+
+The output is formatted to a target width in runes.  The target width is
+determined by checking the environment variable CMDLINE_WIDTH, falling back on
+the terminal width from the OS, falling back on 80 chars.  By setting
+CMDLINE_WIDTH=x, if x > 0 the width is x, if x < 0 the width is unlimited, and
+if x == 0 or is unset one of the fallbacks is used.
+
+Usage:
+   vom help [flags] [command/topic ...]
+
+[command/topic ...] optionally identifies a specific sub-command or help topic.
+
+The vom help flags are:
+ -style=text
+   The formatting style for help output, either "text" or "godoc".
+*/
+package main
diff --git a/cmd/vom/types.vdl b/cmd/vom/types.vdl
new file mode 100644
index 0000000..6387f15
--- /dev/null
+++ b/cmd/vom/types.vdl
@@ -0,0 +1,6 @@
+package main
+
+type dataRep enum {
+	Hex
+	Binary
+}
diff --git a/cmd/vom/types.vdl.go b/cmd/vom/types.vdl.go
new file mode 100644
index 0000000..4a9810c
--- /dev/null
+++ b/cmd/vom/types.vdl.go
@@ -0,0 +1,61 @@
+// This file was auto-generated by the veyron vdl tool.
+// Source: types.vdl
+
+package main
+
+import (
+	// VDL system imports
+	"fmt"
+	"v.io/v23/vdl"
+)
+
+type dataRep int
+
+const (
+	dataRepHex dataRep = iota
+	dataRepBinary
+)
+
+// dataRepAll holds all labels for dataRep.
+var dataRepAll = []dataRep{dataRepHex, dataRepBinary}
+
+// dataRepFromString creates a dataRep from a string label.
+func dataRepFromString(label string) (x dataRep, err error) {
+	err = x.Set(label)
+	return
+}
+
+// Set assigns label to x.
+func (x *dataRep) Set(label string) error {
+	switch label {
+	case "Hex", "hex":
+		*x = dataRepHex
+		return nil
+	case "Binary", "binary":
+		*x = dataRepBinary
+		return nil
+	}
+	*x = -1
+	return fmt.Errorf("unknown label %q in main.dataRep", label)
+}
+
+// String returns the string label of x.
+func (x dataRep) String() string {
+	switch x {
+	case dataRepHex:
+		return "Hex"
+	case dataRepBinary:
+		return "Binary"
+	}
+	return ""
+}
+
+func (dataRep) __VDLReflect(struct {
+	Name string "v.io/x/ref/cmd/vom.dataRep"
+	Enum struct{ Hex, Binary string }
+}) {
+}
+
+func init() {
+	vdl.Register((*dataRep)(nil))
+}
diff --git a/cmd/vom/vom.go b/cmd/vom/vom.go
new file mode 100644
index 0000000..4b55635
--- /dev/null
+++ b/cmd/vom/vom.go
@@ -0,0 +1,265 @@
+// The following enables go generate to generate the doc.go file.
+//go:generate go run $VANADIUM_ROOT/release/go/src/v.io/x/lib/cmdline/testdata/gendoc.go .
+
+package main
+
+import (
+	"bytes"
+	"encoding/hex"
+	"fmt"
+	"io"
+	"io/ioutil"
+	"os"
+	"strings"
+	"unicode"
+
+	"v.io/v23/vdl"
+	"v.io/v23/vom"
+	"v.io/x/lib/cmdline"
+)
+
+func main() {
+	os.Exit(cmdVom.Main())
+}
+
+var cmdVom = &cmdline.Command{
+	Name:  "vom",
+	Short: "Veyron Object Marshaling debugging tool",
+	Long: `
+The vom tool helps debug the Veyron Object Marshaling (vom) protocol.
+`,
+	Children: []*cmdline.Command{cmdDecode, cmdDump},
+}
+
+var cmdDecode = &cmdline.Command{
+	Run:   runDecode,
+	Name:  "decode",
+	Short: "Decode data encoded in the vom format",
+	Long: `
+Decode decodes data encoded in the vom format.  If no arguments are provided,
+decode reads the data from stdin, otherwise the argument is the data.
+
+By default the data is assumed to be represented in hex, with all whitespace
+anywhere in the data ignored.  Use the -data flag to specify other data
+representations.
+
+`,
+	ArgsName: "[data]",
+	ArgsLong: "[data] is the data to decode; if not specified, reads from stdin",
+}
+
+var cmdDump = &cmdline.Command{
+	Run:   runDump,
+	Name:  "dump",
+	Short: "Dump data encoded in the vom format into formatted output",
+	Long: `
+Dump dumps data encoded in the vom format, generating formatted output
+describing each portion of the encoding.  If no arguments are provided, dump
+reads the data from stdin, otherwise the argument is the data.
+
+By default the data is assumed to be represented in hex, with all whitespace
+anywhere in the data ignored.  Use the -data flag to specify other data
+representations.
+
+Calling "vom dump" with no flags and no arguments combines the default stdin
+mode with the default hex mode.  This default mode is special; certain non-hex
+characters may be input to represent commands:
+  . (period)    Calls Dumper.Status to get the current decoding status.
+  ; (semicolon) Calls Dumper.Flush to flush output and start a new message.
+
+This lets you cut-and-paste hex strings into your terminal, and use the commands
+to trigger status or flush calls; i.e. a rudimentary debugging UI.
+
+See v.io/v23/vom.Dumper for details on the dump output.
+`,
+	ArgsName: "[data]",
+	ArgsLong: "[data] is the data to dump; if not specified, reads from stdin",
+}
+
+var (
+	flagDataRep = dataRepHex
+)
+
+func init() {
+	cmdDecode.Flags.Var(&flagDataRep, "data",
+		"Data representation, one of "+fmt.Sprint(dataRepAll))
+	cmdDump.Flags.Var(&flagDataRep, "data",
+		"Data representation, one of "+fmt.Sprint(dataRepAll))
+}
+
+func runDecode(cmd *cmdline.Command, args []string) error {
+	// Convert all inputs into a reader over binary bytes.
+	var data string
+	switch {
+	case len(args) > 1:
+		return cmd.UsageErrorf("too many args")
+	case len(args) == 1:
+		data = args[0]
+	default:
+		bytes, err := ioutil.ReadAll(os.Stdin)
+		if err != nil {
+			return err
+		}
+		data = string(bytes)
+	}
+	binbytes, err := dataToBinaryBytes(data)
+	if err != nil {
+		return err
+	}
+	reader := bytes.NewBuffer(binbytes)
+	// Decode the binary bytes.
+	// TODO(toddw): Add a flag to set a specific type to decode into.
+	decoder, err := vom.NewDecoder(reader)
+	if err != nil {
+		return err
+	}
+	var result *vdl.Value
+	if err := decoder.Decode(&result); err != nil {
+		return err
+	}
+	fmt.Fprintln(cmd.Stdout(), result)
+	if reader.Len() != 0 {
+		return fmt.Errorf("%d leftover bytes: % x", reader.Len(), reader.String())
+	}
+	return nil
+}
+
+func runDump(cmd *cmdline.Command, args []string) error {
+	// Handle non-streaming cases.
+	switch {
+	case len(args) > 1:
+		return cmd.UsageErrorf("too many args")
+	case len(args) == 1:
+		binbytes, err := dataToBinaryBytes(args[0])
+		if err != nil {
+			return err
+		}
+		fmt.Fprintln(cmd.Stdout(), vom.Dump(binbytes))
+		return nil
+	}
+	// Handle streaming from stdin.
+	// TODO(toddw): Add a flag to configure stdout/stderr dumping.
+	dumper := vom.NewDumper(dumpWriter{cmd.Stdout(), cmd.Stdout()})
+	defer dumper.Close()
+	// Handle simple non-hex cases.
+	switch flagDataRep {
+	case dataRepBinary:
+		_, err := io.Copy(dumper, os.Stdin)
+		return err
+	}
+	return runDumpHexStream(dumper)
+}
+
+// runDumpHexStream handles the hex stdin-streaming special-case, with commands
+// for status and flush.  This is tricky because we need to strip whitespace,
+// handle commands where they appear in the stream, and deal with the fact that
+// it takes two hex characters to encode a single byte.
+//
+// The strategy is to run a ReadLoop that reads into a reasonably-sized buffer.
+// Inside the ReadLoop we take the buffer, strip whitespace, and keep looping to
+// process all data up to each command, and then process the command.  If a
+// command appears in the middle of two hex characters representing a byte, we
+// send the command first, before sending the byte.
+//
+// Any leftover non-command single byte is stored in buf and bufStart is set, so
+// that the next iteration of ReadLoop can read after those bytes.
+func runDumpHexStream(dumper *vom.Dumper) error {
+	buf := make([]byte, 1024)
+	bufStart := 0
+ReadLoop:
+	for {
+		n, err := os.Stdin.Read(buf[bufStart:])
+		switch {
+		case n == 0 && err == io.EOF:
+			return nil
+		case n == 0 && err != nil:
+			return err
+		}
+		// We may have hex interspersed with spaces and commands.  The strategy is
+		// to strip all whitespace, and process each even-sized chunk of hex bytes
+		// up to a command or the end of the buffer.
+		//
+		// Data that appears before a command is written before the command, and
+		// data after the command is written after.  But if a command appears in the
+		// middle of two hex characters representing a byte, we send the command
+		// first, before sending the byte.
+		hexbytes := bytes.Map(dropWhitespace, buf[:bufStart+n])
+		for len(hexbytes) > 0 {
+			end := len(hexbytes)
+			cmdIndex := bytes.IndexAny(hexbytes, ".;")
+			if cmdIndex != -1 {
+				end = cmdIndex
+			} else if end == 1 {
+				// We have a single non-command byte left in hexbytes; copy it into buf
+				// and set bufStart.
+				copy(buf, hexbytes[0:1])
+				bufStart = 1
+				continue ReadLoop
+			}
+			if end%2 == 1 {
+				end -= 1 // Ensure the end is on an even boundary.
+			}
+			// Write this even-sized chunk of hex bytes to the dumper.
+			binbytes, err := hex.DecodeString(string(hexbytes[:end]))
+			if err != nil {
+				return err
+			}
+			if _, err := dumper.Write(binbytes); err != nil {
+				return err
+			}
+			// Handle commands.
+			if cmdIndex != -1 {
+				switch cmd := hexbytes[cmdIndex]; cmd {
+				case '.':
+					dumper.Status()
+				case ';':
+					dumper.Flush()
+				default:
+					return fmt.Errorf("unhandled command %q", cmd)
+				}
+				// Move data after the command forward.
+				copy(hexbytes[cmdIndex:], hexbytes[cmdIndex+1:])
+				hexbytes = hexbytes[:len(hexbytes)-1]
+			}
+			// Move data after the end forward.
+			copy(hexbytes, hexbytes[end:])
+			hexbytes = hexbytes[:len(hexbytes)-end]
+		}
+		bufStart = 0
+	}
+}
+
+func dataToBinaryBytes(data string) ([]byte, error) {
+	// Transform all data representations to binary.
+	switch flagDataRep {
+	case dataRepHex:
+		// Remove all whitespace out of the hex string.
+		binbytes, err := hex.DecodeString(strings.Map(dropWhitespace, data))
+		if err != nil {
+			return nil, err
+		}
+		return binbytes, nil
+	}
+	return []byte(data), nil
+}
+
+func dropWhitespace(r rune) rune {
+	if unicode.IsSpace(r) {
+		return -1
+	}
+	return r
+}
+
+type dumpWriter struct {
+	atom, status io.Writer
+}
+
+var _ vom.DumpWriter = dumpWriter{}
+
+func (w dumpWriter) WriteAtom(atom vom.DumpAtom) {
+	w.atom.Write([]byte(atom.String() + "\n"))
+}
+
+func (w dumpWriter) WriteStatus(status vom.DumpStatus) {
+	w.status.Write([]byte("\n" + status.String() + "\n"))
+}
diff --git a/cmd/vomtestgen/doc.go b/cmd/vomtestgen/doc.go
new file mode 100644
index 0000000..42d1129
--- /dev/null
+++ b/cmd/vomtestgen/doc.go
@@ -0,0 +1,30 @@
+// This file was auto-generated via go generate.
+// DO NOT UPDATE MANUALLY
+
+/*
+The vomtestgen tool generates vom test data, using the vomdata file as input,
+and creating a vdl file as output.
+
+Usage:
+   vomtestgen [flags] [vomdata]
+
+[vomdata] is the path to the vomdata input file, specified in the vdl config
+file format.  It must be of the form "NAME.vdl.config", and the output vdl file
+will be generated at "NAME.vdl".
+
+The config file should export a const []any that contains all of the values that
+will be tested.  Here's an example:
+   config = []any{
+     bool(true), uint64(123), string("abc"),
+   }
+
+If not specified, we'll try to find the file at its canonical location:
+   v.io/v23/vom/testdata/vomdata.vdl.config
+
+The vomtestgen flags are:
+ -exts=.vdl
+   Comma-separated list of valid VDL file name extensions.
+ -max_errors=-1
+   Stop processing after this many errors, or -1 for unlimited.
+*/
+package main
diff --git a/cmd/vomtestgen/generate.go b/cmd/vomtestgen/generate.go
new file mode 100644
index 0000000..f685a09
--- /dev/null
+++ b/cmd/vomtestgen/generate.go
@@ -0,0 +1,328 @@
+package main
+
+import (
+	"bytes"
+	"fmt"
+	"io"
+	"io/ioutil"
+	"os"
+	"path/filepath"
+	"strings"
+
+	"v.io/v23/vdl"
+	"v.io/v23/vom"
+	"v.io/x/lib/cmdline"
+	"v.io/x/ref/lib/vdl/build"
+	"v.io/x/ref/lib/vdl/codegen"
+	"v.io/x/ref/lib/vdl/codegen/vdlgen"
+	"v.io/x/ref/lib/vdl/compile"
+)
+
+const (
+	testpkg          = "v.io/v23/vom/testdata"
+	vomdataCanonical = testpkg + "/" + vomdataConfig
+	vomdataConfig    = "vomdata.vdl.config"
+)
+
+var cmdGenerate = &cmdline.Command{
+	Run:   runGenerate,
+	Name:  "vomtestgen",
+	Short: "Generate test data for the vom encoder / decoder",
+	Long: `
+The vomtestgen tool generates vom test data, using the vomdata file as input,
+and creating a vdl file as output.
+`,
+	ArgsName: "[vomdata]",
+	ArgsLong: `
+[vomdata] is the path to the vomdata input file, specified in the vdl config
+file format.  It must be of the form "NAME.vdl.config", and the output vdl
+file will be generated at "NAME.vdl".
+
+The config file should export a const []any that contains all of the values
+that will be tested.  Here's an example:
+   config = []any{
+     bool(true), uint64(123), string("abc"),
+   }
+
+If not specified, we'll try to find the file at its canonical location:
+   ` + vomdataCanonical,
+}
+
+var (
+	optGenMaxErrors int
+	optGenExts      string
+)
+
+func init() {
+	cmdGenerate.Flags.IntVar(&optGenMaxErrors, "max_errors", -1, "Stop processing after this many errors, or -1 for unlimited.")
+	cmdGenerate.Flags.StringVar(&optGenExts, "exts", ".vdl", "Comma-separated list of valid VDL file name extensions.")
+}
+
+func runGenerate(cmd *cmdline.Command, args []string) error {
+	debug := new(bytes.Buffer)
+	defer dumpDebug(cmd.Stderr(), debug)
+	env := compile.NewEnv(optGenMaxErrors)
+	// Get the input datafile path.
+	var path string
+	switch len(args) {
+	case 0:
+		srcDirs := build.SrcDirs(env.Errors)
+		if err := env.Errors.ToError(); err != nil {
+			return cmd.UsageErrorf("%v", err)
+		}
+		path = guessDataFilePath(debug, srcDirs)
+		if path == "" {
+			return cmd.UsageErrorf("couldn't find vomdata file in src dirs: %v", srcDirs)
+		}
+	case 1:
+		path = args[0]
+	default:
+		return cmd.UsageErrorf("too many args (expecting exactly 1 vomdata file)")
+	}
+	inName := filepath.Clean(path)
+	if !strings.HasSuffix(inName, ".vdl.config") {
+		return cmd.UsageErrorf(`vomdata file doesn't end in ".vdl.config": %s`, inName)
+	}
+	outName := inName[:len(inName)-len(".config")]
+	// Remove the generated file, so that it doesn't interfere with compiling the
+	// config.  Ignore errors since it might not exist yet.
+	if err := os.Remove(outName); err == nil {
+		fmt.Fprintf(debug, "Removed output file %v\n", outName)
+	}
+	config, err := compileConfig(debug, inName, env)
+	if err != nil {
+		return err
+	}
+	data, err := generate(config)
+	if err != nil {
+		return err
+	}
+	if err := writeFile(data, outName); err != nil {
+		return err
+	}
+	debug.Reset() // Don't dump debugging information on success
+	fmt.Fprintf(cmd.Stdout(), "Wrote output file %v\n", outName)
+	return nil
+}
+
+func dumpDebug(stderr io.Writer, debug *bytes.Buffer) {
+	if d := debug.Bytes(); len(d) > 0 {
+		io.Copy(stderr, debug)
+	}
+}
+
+func guessDataFilePath(debug io.Writer, srcDirs []string) string {
+	// Try to guess the data file path by looking for the canonical vomdata input
+	// file in each of our source directories.
+	for _, dir := range srcDirs {
+		guess := filepath.Join(dir, vomdataCanonical)
+		if stat, err := os.Stat(guess); err == nil && !stat.IsDir() {
+			fmt.Fprintf(debug, "Found vomdata file %s\n", guess)
+			return guess
+		}
+	}
+	return ""
+}
+
+func compileConfig(debug io.Writer, inName string, env *compile.Env) (*vdl.Value, error) {
+	basename := filepath.Base(inName)
+	data, err := os.Open(inName)
+	if err != nil {
+		return nil, fmt.Errorf("couldn't open vomdata file %s: %v", inName, err)
+	}
+	defer func() { data.Close() }() // make defer use the latest value of data
+	if stat, err := data.Stat(); err != nil || stat.IsDir() {
+		return nil, fmt.Errorf("vomdata file %s is a directory", inName)
+	}
+	var opts build.Opts
+	opts.Extensions = strings.Split(optGenExts, ",")
+	// Compile package dependencies in transitive order.
+	deps := build.TransitivePackagesForConfig(basename, data, opts, env.Errors)
+	for _, dep := range deps {
+		if pkg := build.BuildPackage(dep, env); pkg != nil {
+			fmt.Fprintf(debug, "Built package %s\n", pkg.Path)
+		}
+	}
+	// Try to seek back to the beginning of the data file, or if that fails try to
+	// open the data file again.
+	if off, err := data.Seek(0, 0); off != 0 || err != nil {
+		if err := data.Close(); err != nil {
+			return nil, fmt.Errorf("couldn't close vomdata file %s: %v", inName, err)
+		}
+		data, err = os.Open(inName)
+		if err != nil {
+			return nil, fmt.Errorf("couldn't re-open vomdata file %s: %v", inName, err)
+		}
+	}
+	// Compile config into our test values.
+	config := build.BuildConfig(basename, data, nil, nil, env)
+	if err := env.Errors.ToError(); err != nil {
+		return nil, err
+	}
+	fmt.Fprintf(debug, "Compiled vomdata file %s\n", inName)
+	return config, err
+}
+
+func generate(config *vdl.Value) ([]byte, error) {
+	// This config needs to have a specific struct format. See @testdata/vomtype.vdl.
+	// TODO(alexfandrianto): Instead of this, we should have separate generator
+	// functions that switch off of the vomdata config filename. That way, we can
+	// have 1 config each for encode/decode, compatibility, and convertibility.
+	buf := new(bytes.Buffer)
+	fmt.Fprintf(buf, `// This file was auto-generated via "vomtest generate".
+// DO NOT UPDATE MANUALLY; read the comments in `+vomdataConfig+`.
+
+package testdata
+`)
+	imports := codegen.ImportsForValue(config, testpkg)
+	if len(imports) > 0 {
+		fmt.Fprintf(buf, "\n%s\n", vdlgen.Imports(imports))
+	}
+	fmt.Fprintf(buf, `
+// TestCase represents an individual testcase for vom encoding and decoding.
+type TestCase struct {
+	Name       string // Name of the testcase
+	Value      any    // Value to test
+	Hex        string // Hex pattern representing vom encoding of Value
+	TypeString string // The string representation of the Type
+}
+
+// Tests contains the testcases to use to test vom encoding and decoding.
+const Tests = []TestCase {`)
+	// The vom encode-decode test cases need to be of type []any.
+	encodeDecodeTests := config.StructField(0)
+	if got, want := encodeDecodeTests.Type(), vdl.ListType(vdl.AnyType); got != want {
+		return nil, fmt.Errorf("got encodeDecodeTests type %v, want %v", got, want)
+	}
+
+	for ix := 0; ix < encodeDecodeTests.Len(); ix++ {
+		value := encodeDecodeTests.Index(ix)
+		if !value.IsNil() {
+			// The encodeDecodeTests has type []any, and there's no need for our values to
+			// include the "any" type explicitly, so we descend into the elem value.
+			value = value.Elem()
+		}
+		valstr := vdlgen.TypedConst(value, testpkg, imports)
+		vomhex, vomdump, err := toVomHex(value)
+		if err != nil {
+			return nil, err
+		}
+		fmt.Fprintf(buf, `
+%[3]s
+	{
+		%#[1]q,
+		%[1]s,
+		%[2]q,
+		%[4]q,
+	},`, valstr, vomhex, vomdump, value.Type().String())
+	}
+	fmt.Fprintf(buf, `
+}
+`)
+
+	// The vom compatibility tests need to be of type map[string][]typeobject
+	// Each of the []typeobject are a slice of inter-compatible typeobjects.
+	// However, the typeobjects are not compatible with any other []typeobject.
+	// Note: any and optional should be tested separately.
+	compatTests := config.StructField(1)
+	if got, want := compatTests.Type(), vdl.MapType(vdl.StringType, vdl.ListType(vdl.TypeObjectType)); got != want {
+		return nil, fmt.Errorf("got compatTests type %v, want %v", got, want)
+	}
+	fmt.Fprintf(buf, `
+// CompatTests contains the testcases to use to test vom type compatibility.
+// CompatTests maps TestName (string) to CompatibleTypeSet ([]typeobject)
+// Each CompatibleTypeSet contains types compatible with each other. However,
+// types from different CompatibleTypeSets are incompatible.
+const CompatTests = map[string][]typeobject{`)
+
+	for _, testName := range vdl.SortValuesAsString(compatTests.Keys()) {
+		compatibleTypeSet := compatTests.MapIndex(testName)
+		valstr := vdlgen.TypedConst(compatibleTypeSet, testpkg, imports)
+		fmt.Fprintf(buf, `
+	%[1]q: %[2]s,`, testName.RawString(), valstr)
+	}
+	fmt.Fprintf(buf, `
+}
+`)
+
+	// The vom conversion tests need to be a map[string][]ConvertGroup
+	// See vom/testdata/vomtype.vdl
+	convertTests := config.StructField(2)
+	fmt.Fprintf(buf, `
+// ConvertTests contains the testcases to check vom value convertibility.
+// ConvertTests maps TestName (string) to ConvertGroups ([]ConvertGroup)
+// Each ConvertGroup is a struct with 'Name', 'PrimaryType', and 'Values'.
+// The values within a ConvertGroup can convert between themselves w/o error.
+// However, values in higher-indexed ConvertGroups will error when converting up
+// to the primary type of the lower-indexed ConvertGroups.
+const ConvertTests = map[string][]ConvertGroup{`)
+	for _, testName := range vdl.SortValuesAsString(convertTests.Keys()) {
+		fmt.Fprintf(buf, `
+	%[1]q: {`, testName.RawString())
+
+		convertTest := convertTests.MapIndex(testName)
+		for ix := 0; ix < convertTest.Len(); ix++ {
+			convertGroup := convertTest.Index(ix)
+
+			fmt.Fprintf(buf, `
+		{
+			%[1]q,
+			%[2]s,
+			{ `, convertGroup.StructField(0).RawString(), vdlgen.TypedConst(convertGroup.StructField(1), testpkg, imports))
+
+			values := convertGroup.StructField(2)
+			for iy := 0; iy < values.Len(); iy++ {
+				value := values.Index(iy)
+				if !value.IsNil() {
+					// The value is any, and there's no need for our values to include
+					// the "any" type explicitly, so we descend into the elem value.
+					value = value.Elem()
+				}
+				valstr := vdlgen.TypedConst(value, testpkg, imports)
+				fmt.Fprintf(buf, `%[1]s, `, valstr)
+			}
+
+			fmt.Fprintf(buf, `},
+		},`)
+		}
+
+		fmt.Fprintf(buf, `
+	},`)
+	}
+	fmt.Fprintf(buf, `
+}
+`)
+
+	return buf.Bytes(), nil
+}
+
+func toVomHex(value *vdl.Value) (string, string, error) {
+	buf := new(bytes.Buffer)
+	enc, err := vom.NewEncoder(buf)
+	if err != nil {
+		return "", "", fmt.Errorf("vom.NewEncoder failed: %v", err)
+	}
+	if err := enc.Encode(value); err != nil {
+		return "", "", fmt.Errorf("vom.Encode(%v) failed: %v", value, err)
+	}
+	vombytes := buf.String()
+	const pre = "\t// "
+	vomdump := pre + strings.Replace(vom.Dump(buf.Bytes()), "\n", "\n"+pre, -1)
+	if strings.HasSuffix(vomdump, "\n"+pre) {
+		vomdump = vomdump[:len(vomdump)-len("\n"+pre)]
+	}
+	// TODO(toddw): Add hex pattern bracketing for map and set.
+	return fmt.Sprintf("%x", vombytes), vomdump, nil
+}
+
+func writeFile(data []byte, outName string) error {
+	// Create containing directory and write the file.
+	dirName := filepath.Dir(outName)
+	if err := os.MkdirAll(dirName, os.FileMode(0777)); err != nil {
+		return fmt.Errorf("couldn't create directory %s: %v", dirName, err)
+	}
+	if err := ioutil.WriteFile(outName, data, os.FileMode(0666)); err != nil {
+		return fmt.Errorf("couldn't write file %s: %v", outName, err)
+	}
+	return nil
+}
diff --git a/cmd/vomtestgen/main.go b/cmd/vomtestgen/main.go
new file mode 100644
index 0000000..355f434
--- /dev/null
+++ b/cmd/vomtestgen/main.go
@@ -0,0 +1,10 @@
+// The following enables go generate to generate the doc.go file.
+//go:generate go run $VANADIUM_ROOT/release/go/src/v.io/x/lib/cmdline/testdata/gendoc.go . -help
+
+package main
+
+import "os"
+
+func main() {
+	os.Exit(cmdGenerate.Main())
+}
diff --git a/cmd/vrpc/doc.go b/cmd/vrpc/doc.go
new file mode 100644
index 0000000..df0d470
--- /dev/null
+++ b/cmd/vrpc/doc.go
@@ -0,0 +1,139 @@
+// This file was auto-generated via go generate.
+// DO NOT UPDATE MANUALLY
+
+/*
+The vrpc tool provides command-line access to Vanadium servers via Remote
+Procedure Call.
+
+Usage:
+   vrpc <command>
+
+The vrpc commands are:
+   signature   Describe the interfaces of a Vanadium server
+   call        Call a method of a Vanadium server
+   identify    Reveal blessings presented by a Vanadium server
+   help        Display help for commands or topics
+Run "vrpc help [command]" for command usage.
+
+The global flags are:
+ -alsologtostderr=true
+   log to standard error as well as files
+ -log_backtrace_at=:0
+   when logging hits line file:N, emit a stack trace
+ -log_dir=
+   if non-empty, write log files to this directory
+ -logtostderr=false
+   log to standard error instead of files
+ -max_stack_buf_size=4292608
+   max size in bytes of the buffer to use for logging stack traces
+ -stderrthreshold=2
+   logs at or above this threshold go to stderr
+ -v=0
+   log level for V logs
+ -vanadium.i18n_catalogue=
+   18n catalogue files to load, comma separated
+ -veyron.credentials=
+   directory to use for storing security credentials
+ -veyron.namespace.root=[/ns.dev.v.io:8101]
+   local namespace root; can be repeated to provided multiple roots
+ -veyron.proxy=
+   object name of proxy service to use to export services across network
+   boundaries
+ -veyron.tcp.address=
+   address to listen on
+ -veyron.tcp.protocol=tcp
+   protocol to listen with
+ -veyron.vtrace.cache_size=1024
+   The number of vtrace traces to store in memory.
+ -veyron.vtrace.collect_regexp=
+   Spans and annotations that match this regular expression will trigger trace
+   collection.
+ -veyron.vtrace.dump_on_shutdown=true
+   If true, dump all stored traces on runtime shutdown.
+ -veyron.vtrace.sample_rate=0
+   Rate (from 0.0 to 1.0) to sample vtrace traces.
+ -vmodule=
+   comma-separated list of pattern=N settings for file-filtered logging
+
+Vrpc Signature
+
+Signature connects to the Vanadium server identified by <server>.
+
+If no [method] is provided, returns all interfaces implemented by the server.
+
+If a [method] is provided, returns the signature of just that method.
+
+Usage:
+   vrpc signature <server> [method]
+
+<server> identifies a Vanadium server.  It can either be the object address of
+the server, or an object name that will be resolved to an end-point.
+
+[method] is the optional server method name.
+
+Vrpc Call
+
+Call connects to the Vanadium server identified by <server> and calls the
+<method> with the given positional [args...], returning results on stdout.
+
+TODO(toddw): stdin is read for streaming arguments sent to the server.  An EOF
+on stdin (e.g. via ^D) causes the send stream to be closed.
+
+Regardless of whether the call is streaming, the main goroutine blocks for
+streaming and positional results received from the server.
+
+All input arguments (both positional and streaming) are specified as VDL
+expressions, with commas separating multiple expressions.  Positional arguments
+may also be specified as separate command-line arguments.  Streaming arguments
+may also be specified as separate newline-terminated expressions.
+
+The method signature is always retrieved from the server as a first step.  This
+makes it easier to input complex typed arguments, since the top-level type for
+each argument is implicit and doesn't need to be specified.
+
+Usage:
+   vrpc call <server> <method> [args...]
+
+<server> identifies a Vanadium server.  It can either be the object address of
+the server, or an object name that will be resolved to an end-point.
+
+<method> is the server method to call.
+
+[args...] are the positional input arguments, specified as VDL expressions.
+
+Vrpc Identify
+
+Identify connects to the Vanadium server identified by <server> and dumps out
+the blessings presented by that server (and the subset of those that are
+considered valid by the principal running this tool) to standard output.
+
+Usage:
+   vrpc identify <server>
+
+<server> identifies a Vanadium server.  It can either be the object address of
+the server, or an object name that will be resolved to an end-point.
+
+Vrpc Help
+
+Help with no args displays the usage of the parent command.
+
+Help with args displays the usage of the specified sub-command or help topic.
+
+"help ..." recursively displays help for all commands and topics.
+
+The output is formatted to a target width in runes.  The target width is
+determined by checking the environment variable CMDLINE_WIDTH, falling back on
+the terminal width from the OS, falling back on 80 chars.  By setting
+CMDLINE_WIDTH=x, if x > 0 the width is x, if x < 0 the width is unlimited, and
+if x == 0 or is unset one of the fallbacks is used.
+
+Usage:
+   vrpc help [flags] [command/topic ...]
+
+[command/topic ...] optionally identifies a specific sub-command or help topic.
+
+The vrpc help flags are:
+ -style=text
+   The formatting style for help output, either "text" or "godoc".
+*/
+package main
diff --git a/cmd/vrpc/test_base/test_base.vdl b/cmd/vrpc/test_base/test_base.vdl
new file mode 100644
index 0000000..70e3b85
--- /dev/null
+++ b/cmd/vrpc/test_base/test_base.vdl
@@ -0,0 +1,36 @@
+package test_base
+
+type Struct struct {
+  X,Y int32
+}
+
+type Array2Int [2]int32
+
+// TypeTester methods are listed in alphabetical order, to make it easier to
+// test Signature output, which sorts methods alphabetically.
+type TypeTester interface {
+	// Methods to test support for primitive types.
+	EchoBool(I1 bool) (O1 bool | error)
+	EchoFloat32(I1 float32) (O1 float32 | error)
+	EchoFloat64(I1 float64) (O1 float64 | error)
+	EchoInt32(I1 int32) (O1 int32 | error)
+	EchoInt64(I1 int64) (O1 int64 | error)
+	EchoString(I1 string) (O1 string | error)
+	EchoByte(I1 byte) (O1 byte | error)
+	EchoUint32(I1 uint32) (O1 uint32 | error)
+	EchoUint64(I1 uint64) (O1 uint64 | error)
+
+	// Methods to test support for composite types.
+	XEchoArray(I1 Array2Int) (O1 Array2Int | error)
+	XEchoMap(I1 map[int32]string) (O1 map[int32]string | error)
+	XEchoSet(I1 set[int32]) (O1 set[int32] | error)
+	XEchoSlice(I1 []int32) (O1 []int32 | error)
+	XEchoStruct(I1 Struct) (O1 Struct | error)
+
+	// Methods to test support for different number of arguments.
+	YMultiArg(I1, I2 int32) (O1, O2 int32 | error)
+	YNoArgs() error
+
+	// Methods to test support for streaming.
+	ZStream(NumStreamItems int32, StreamItem bool) stream<_, bool> error
+}
diff --git a/cmd/vrpc/test_base/test_base.vdl.go b/cmd/vrpc/test_base/test_base.vdl.go
new file mode 100644
index 0000000..c55bffd
--- /dev/null
+++ b/cmd/vrpc/test_base/test_base.vdl.go
@@ -0,0 +1,682 @@
+// This file was auto-generated by the veyron vdl tool.
+// Source: test_base.vdl
+
+package test_base
+
+import (
+	// VDL system imports
+	"io"
+	"v.io/v23"
+	"v.io/v23/context"
+	"v.io/v23/ipc"
+	"v.io/v23/vdl"
+)
+
+type Struct struct {
+	X int32
+	Y int32
+}
+
+func (Struct) __VDLReflect(struct {
+	Name string "v.io/x/ref/cmd/vrpc/test_base.Struct"
+}) {
+}
+
+type Array2Int [2]int32
+
+func (Array2Int) __VDLReflect(struct {
+	Name string "v.io/x/ref/cmd/vrpc/test_base.Array2Int"
+}) {
+}
+
+func init() {
+	vdl.Register((*Struct)(nil))
+	vdl.Register((*Array2Int)(nil))
+}
+
+// TypeTesterClientMethods is the client interface
+// containing TypeTester methods.
+//
+// TypeTester methods are listed in alphabetical order, to make it easier to
+// test Signature output, which sorts methods alphabetically.
+type TypeTesterClientMethods interface {
+	// Methods to test support for primitive types.
+	EchoBool(ctx *context.T, I1 bool, opts ...ipc.CallOpt) (O1 bool, err error)
+	EchoFloat32(ctx *context.T, I1 float32, opts ...ipc.CallOpt) (O1 float32, err error)
+	EchoFloat64(ctx *context.T, I1 float64, opts ...ipc.CallOpt) (O1 float64, err error)
+	EchoInt32(ctx *context.T, I1 int32, opts ...ipc.CallOpt) (O1 int32, err error)
+	EchoInt64(ctx *context.T, I1 int64, opts ...ipc.CallOpt) (O1 int64, err error)
+	EchoString(ctx *context.T, I1 string, opts ...ipc.CallOpt) (O1 string, err error)
+	EchoByte(ctx *context.T, I1 byte, opts ...ipc.CallOpt) (O1 byte, err error)
+	EchoUint32(ctx *context.T, I1 uint32, opts ...ipc.CallOpt) (O1 uint32, err error)
+	EchoUint64(ctx *context.T, I1 uint64, opts ...ipc.CallOpt) (O1 uint64, err error)
+	// Methods to test support for composite types.
+	XEchoArray(ctx *context.T, I1 Array2Int, opts ...ipc.CallOpt) (O1 Array2Int, err error)
+	XEchoMap(ctx *context.T, I1 map[int32]string, opts ...ipc.CallOpt) (O1 map[int32]string, err error)
+	XEchoSet(ctx *context.T, I1 map[int32]struct{}, opts ...ipc.CallOpt) (O1 map[int32]struct{}, err error)
+	XEchoSlice(ctx *context.T, I1 []int32, opts ...ipc.CallOpt) (O1 []int32, err error)
+	XEchoStruct(ctx *context.T, I1 Struct, opts ...ipc.CallOpt) (O1 Struct, err error)
+	// Methods to test support for different number of arguments.
+	YMultiArg(ctx *context.T, I1 int32, I2 int32, opts ...ipc.CallOpt) (O1 int32, O2 int32, err error)
+	YNoArgs(*context.T, ...ipc.CallOpt) error
+	// Methods to test support for streaming.
+	ZStream(ctx *context.T, NumStreamItems int32, StreamItem bool, opts ...ipc.CallOpt) (TypeTesterZStreamClientCall, error)
+}
+
+// TypeTesterClientStub adds universal methods to TypeTesterClientMethods.
+type TypeTesterClientStub interface {
+	TypeTesterClientMethods
+	ipc.UniversalServiceMethods
+}
+
+// TypeTesterClient returns a client stub for TypeTester.
+func TypeTesterClient(name string, opts ...ipc.BindOpt) TypeTesterClientStub {
+	var client ipc.Client
+	for _, opt := range opts {
+		if clientOpt, ok := opt.(ipc.Client); ok {
+			client = clientOpt
+		}
+	}
+	return implTypeTesterClientStub{name, client}
+}
+
+type implTypeTesterClientStub struct {
+	name   string
+	client ipc.Client
+}
+
+func (c implTypeTesterClientStub) c(ctx *context.T) ipc.Client {
+	if c.client != nil {
+		return c.client
+	}
+	return v23.GetClient(ctx)
+}
+
+func (c implTypeTesterClientStub) EchoBool(ctx *context.T, i0 bool, opts ...ipc.CallOpt) (o0 bool, err error) {
+	var call ipc.ClientCall
+	if call, err = c.c(ctx).StartCall(ctx, c.name, "EchoBool", []interface{}{i0}, opts...); err != nil {
+		return
+	}
+	err = call.Finish(&o0)
+	return
+}
+
+func (c implTypeTesterClientStub) EchoFloat32(ctx *context.T, i0 float32, opts ...ipc.CallOpt) (o0 float32, err error) {
+	var call ipc.ClientCall
+	if call, err = c.c(ctx).StartCall(ctx, c.name, "EchoFloat32", []interface{}{i0}, opts...); err != nil {
+		return
+	}
+	err = call.Finish(&o0)
+	return
+}
+
+func (c implTypeTesterClientStub) EchoFloat64(ctx *context.T, i0 float64, opts ...ipc.CallOpt) (o0 float64, err error) {
+	var call ipc.ClientCall
+	if call, err = c.c(ctx).StartCall(ctx, c.name, "EchoFloat64", []interface{}{i0}, opts...); err != nil {
+		return
+	}
+	err = call.Finish(&o0)
+	return
+}
+
+func (c implTypeTesterClientStub) EchoInt32(ctx *context.T, i0 int32, opts ...ipc.CallOpt) (o0 int32, err error) {
+	var call ipc.ClientCall
+	if call, err = c.c(ctx).StartCall(ctx, c.name, "EchoInt32", []interface{}{i0}, opts...); err != nil {
+		return
+	}
+	err = call.Finish(&o0)
+	return
+}
+
+func (c implTypeTesterClientStub) EchoInt64(ctx *context.T, i0 int64, opts ...ipc.CallOpt) (o0 int64, err error) {
+	var call ipc.ClientCall
+	if call, err = c.c(ctx).StartCall(ctx, c.name, "EchoInt64", []interface{}{i0}, opts...); err != nil {
+		return
+	}
+	err = call.Finish(&o0)
+	return
+}
+
+func (c implTypeTesterClientStub) EchoString(ctx *context.T, i0 string, opts ...ipc.CallOpt) (o0 string, err error) {
+	var call ipc.ClientCall
+	if call, err = c.c(ctx).StartCall(ctx, c.name, "EchoString", []interface{}{i0}, opts...); err != nil {
+		return
+	}
+	err = call.Finish(&o0)
+	return
+}
+
+func (c implTypeTesterClientStub) EchoByte(ctx *context.T, i0 byte, opts ...ipc.CallOpt) (o0 byte, err error) {
+	var call ipc.ClientCall
+	if call, err = c.c(ctx).StartCall(ctx, c.name, "EchoByte", []interface{}{i0}, opts...); err != nil {
+		return
+	}
+	err = call.Finish(&o0)
+	return
+}
+
+func (c implTypeTesterClientStub) EchoUint32(ctx *context.T, i0 uint32, opts ...ipc.CallOpt) (o0 uint32, err error) {
+	var call ipc.ClientCall
+	if call, err = c.c(ctx).StartCall(ctx, c.name, "EchoUint32", []interface{}{i0}, opts...); err != nil {
+		return
+	}
+	err = call.Finish(&o0)
+	return
+}
+
+func (c implTypeTesterClientStub) EchoUint64(ctx *context.T, i0 uint64, opts ...ipc.CallOpt) (o0 uint64, err error) {
+	var call ipc.ClientCall
+	if call, err = c.c(ctx).StartCall(ctx, c.name, "EchoUint64", []interface{}{i0}, opts...); err != nil {
+		return
+	}
+	err = call.Finish(&o0)
+	return
+}
+
+func (c implTypeTesterClientStub) XEchoArray(ctx *context.T, i0 Array2Int, opts ...ipc.CallOpt) (o0 Array2Int, err error) {
+	var call ipc.ClientCall
+	if call, err = c.c(ctx).StartCall(ctx, c.name, "XEchoArray", []interface{}{i0}, opts...); err != nil {
+		return
+	}
+	err = call.Finish(&o0)
+	return
+}
+
+func (c implTypeTesterClientStub) XEchoMap(ctx *context.T, i0 map[int32]string, opts ...ipc.CallOpt) (o0 map[int32]string, err error) {
+	var call ipc.ClientCall
+	if call, err = c.c(ctx).StartCall(ctx, c.name, "XEchoMap", []interface{}{i0}, opts...); err != nil {
+		return
+	}
+	err = call.Finish(&o0)
+	return
+}
+
+func (c implTypeTesterClientStub) XEchoSet(ctx *context.T, i0 map[int32]struct{}, opts ...ipc.CallOpt) (o0 map[int32]struct{}, err error) {
+	var call ipc.ClientCall
+	if call, err = c.c(ctx).StartCall(ctx, c.name, "XEchoSet", []interface{}{i0}, opts...); err != nil {
+		return
+	}
+	err = call.Finish(&o0)
+	return
+}
+
+func (c implTypeTesterClientStub) XEchoSlice(ctx *context.T, i0 []int32, opts ...ipc.CallOpt) (o0 []int32, err error) {
+	var call ipc.ClientCall
+	if call, err = c.c(ctx).StartCall(ctx, c.name, "XEchoSlice", []interface{}{i0}, opts...); err != nil {
+		return
+	}
+	err = call.Finish(&o0)
+	return
+}
+
+func (c implTypeTesterClientStub) XEchoStruct(ctx *context.T, i0 Struct, opts ...ipc.CallOpt) (o0 Struct, err error) {
+	var call ipc.ClientCall
+	if call, err = c.c(ctx).StartCall(ctx, c.name, "XEchoStruct", []interface{}{i0}, opts...); err != nil {
+		return
+	}
+	err = call.Finish(&o0)
+	return
+}
+
+func (c implTypeTesterClientStub) YMultiArg(ctx *context.T, i0 int32, i1 int32, opts ...ipc.CallOpt) (o0 int32, o1 int32, err error) {
+	var call ipc.ClientCall
+	if call, err = c.c(ctx).StartCall(ctx, c.name, "YMultiArg", []interface{}{i0, i1}, opts...); err != nil {
+		return
+	}
+	err = call.Finish(&o0, &o1)
+	return
+}
+
+func (c implTypeTesterClientStub) YNoArgs(ctx *context.T, opts ...ipc.CallOpt) (err error) {
+	var call ipc.ClientCall
+	if call, err = c.c(ctx).StartCall(ctx, c.name, "YNoArgs", nil, opts...); err != nil {
+		return
+	}
+	err = call.Finish()
+	return
+}
+
+func (c implTypeTesterClientStub) ZStream(ctx *context.T, i0 int32, i1 bool, opts ...ipc.CallOpt) (ocall TypeTesterZStreamClientCall, err error) {
+	var call ipc.ClientCall
+	if call, err = c.c(ctx).StartCall(ctx, c.name, "ZStream", []interface{}{i0, i1}, opts...); err != nil {
+		return
+	}
+	ocall = &implTypeTesterZStreamClientCall{ClientCall: call}
+	return
+}
+
+// TypeTesterZStreamClientStream is the client stream for TypeTester.ZStream.
+type TypeTesterZStreamClientStream interface {
+	// RecvStream returns the receiver side of the TypeTester.ZStream client stream.
+	RecvStream() interface {
+		// Advance stages an item so that it may be retrieved via Value.  Returns
+		// true iff there is an item to retrieve.  Advance must be called before
+		// Value is called.  May block if an item is not available.
+		Advance() bool
+		// Value returns the item that was staged by Advance.  May panic if Advance
+		// returned false or was not called.  Never blocks.
+		Value() bool
+		// Err returns any error encountered by Advance.  Never blocks.
+		Err() error
+	}
+}
+
+// TypeTesterZStreamClientCall represents the call returned from TypeTester.ZStream.
+type TypeTesterZStreamClientCall interface {
+	TypeTesterZStreamClientStream
+	// Finish blocks until the server is done, and returns the positional return
+	// values for call.
+	//
+	// Finish returns immediately if the call has been canceled; depending on the
+	// timing the output could either be an error signaling cancelation, or the
+	// valid positional return values from the server.
+	//
+	// Calling Finish is mandatory for releasing stream resources, unless the call
+	// has been canceled or any of the other methods return an error.  Finish should
+	// be called at most once.
+	Finish() error
+}
+
+type implTypeTesterZStreamClientCall struct {
+	ipc.ClientCall
+	valRecv bool
+	errRecv error
+}
+
+func (c *implTypeTesterZStreamClientCall) RecvStream() interface {
+	Advance() bool
+	Value() bool
+	Err() error
+} {
+	return implTypeTesterZStreamCallRecv{c}
+}
+
+type implTypeTesterZStreamCallRecv struct {
+	c *implTypeTesterZStreamClientCall
+}
+
+func (c implTypeTesterZStreamCallRecv) Advance() bool {
+	c.c.errRecv = c.c.Recv(&c.c.valRecv)
+	return c.c.errRecv == nil
+}
+func (c implTypeTesterZStreamCallRecv) Value() bool {
+	return c.c.valRecv
+}
+func (c implTypeTesterZStreamCallRecv) Err() error {
+	if c.c.errRecv == io.EOF {
+		return nil
+	}
+	return c.c.errRecv
+}
+func (c *implTypeTesterZStreamClientCall) Finish() (err error) {
+	err = c.ClientCall.Finish()
+	return
+}
+
+// TypeTesterServerMethods is the interface a server writer
+// implements for TypeTester.
+//
+// TypeTester methods are listed in alphabetical order, to make it easier to
+// test Signature output, which sorts methods alphabetically.
+type TypeTesterServerMethods interface {
+	// Methods to test support for primitive types.
+	EchoBool(ctx ipc.ServerCall, I1 bool) (O1 bool, err error)
+	EchoFloat32(ctx ipc.ServerCall, I1 float32) (O1 float32, err error)
+	EchoFloat64(ctx ipc.ServerCall, I1 float64) (O1 float64, err error)
+	EchoInt32(ctx ipc.ServerCall, I1 int32) (O1 int32, err error)
+	EchoInt64(ctx ipc.ServerCall, I1 int64) (O1 int64, err error)
+	EchoString(ctx ipc.ServerCall, I1 string) (O1 string, err error)
+	EchoByte(ctx ipc.ServerCall, I1 byte) (O1 byte, err error)
+	EchoUint32(ctx ipc.ServerCall, I1 uint32) (O1 uint32, err error)
+	EchoUint64(ctx ipc.ServerCall, I1 uint64) (O1 uint64, err error)
+	// Methods to test support for composite types.
+	XEchoArray(ctx ipc.ServerCall, I1 Array2Int) (O1 Array2Int, err error)
+	XEchoMap(ctx ipc.ServerCall, I1 map[int32]string) (O1 map[int32]string, err error)
+	XEchoSet(ctx ipc.ServerCall, I1 map[int32]struct{}) (O1 map[int32]struct{}, err error)
+	XEchoSlice(ctx ipc.ServerCall, I1 []int32) (O1 []int32, err error)
+	XEchoStruct(ctx ipc.ServerCall, I1 Struct) (O1 Struct, err error)
+	// Methods to test support for different number of arguments.
+	YMultiArg(ctx ipc.ServerCall, I1 int32, I2 int32) (O1 int32, O2 int32, err error)
+	YNoArgs(ipc.ServerCall) error
+	// Methods to test support for streaming.
+	ZStream(ctx TypeTesterZStreamContext, NumStreamItems int32, StreamItem bool) error
+}
+
+// TypeTesterServerStubMethods is the server interface containing
+// TypeTester methods, as expected by ipc.Server.
+// The only difference between this interface and TypeTesterServerMethods
+// is the streaming methods.
+type TypeTesterServerStubMethods interface {
+	// Methods to test support for primitive types.
+	EchoBool(ctx ipc.ServerCall, I1 bool) (O1 bool, err error)
+	EchoFloat32(ctx ipc.ServerCall, I1 float32) (O1 float32, err error)
+	EchoFloat64(ctx ipc.ServerCall, I1 float64) (O1 float64, err error)
+	EchoInt32(ctx ipc.ServerCall, I1 int32) (O1 int32, err error)
+	EchoInt64(ctx ipc.ServerCall, I1 int64) (O1 int64, err error)
+	EchoString(ctx ipc.ServerCall, I1 string) (O1 string, err error)
+	EchoByte(ctx ipc.ServerCall, I1 byte) (O1 byte, err error)
+	EchoUint32(ctx ipc.ServerCall, I1 uint32) (O1 uint32, err error)
+	EchoUint64(ctx ipc.ServerCall, I1 uint64) (O1 uint64, err error)
+	// Methods to test support for composite types.
+	XEchoArray(ctx ipc.ServerCall, I1 Array2Int) (O1 Array2Int, err error)
+	XEchoMap(ctx ipc.ServerCall, I1 map[int32]string) (O1 map[int32]string, err error)
+	XEchoSet(ctx ipc.ServerCall, I1 map[int32]struct{}) (O1 map[int32]struct{}, err error)
+	XEchoSlice(ctx ipc.ServerCall, I1 []int32) (O1 []int32, err error)
+	XEchoStruct(ctx ipc.ServerCall, I1 Struct) (O1 Struct, err error)
+	// Methods to test support for different number of arguments.
+	YMultiArg(ctx ipc.ServerCall, I1 int32, I2 int32) (O1 int32, O2 int32, err error)
+	YNoArgs(ipc.ServerCall) error
+	// Methods to test support for streaming.
+	ZStream(ctx *TypeTesterZStreamContextStub, NumStreamItems int32, StreamItem bool) error
+}
+
+// TypeTesterServerStub adds universal methods to TypeTesterServerStubMethods.
+type TypeTesterServerStub interface {
+	TypeTesterServerStubMethods
+	// Describe the TypeTester interfaces.
+	Describe__() []ipc.InterfaceDesc
+}
+
+// TypeTesterServer returns a server stub for TypeTester.
+// It converts an implementation of TypeTesterServerMethods into
+// an object that may be used by ipc.Server.
+func TypeTesterServer(impl TypeTesterServerMethods) TypeTesterServerStub {
+	stub := implTypeTesterServerStub{
+		impl: impl,
+	}
+	// Initialize GlobState; always check the stub itself first, to handle the
+	// case where the user has the Glob method defined in their VDL source.
+	if gs := ipc.NewGlobState(stub); gs != nil {
+		stub.gs = gs
+	} else if gs := ipc.NewGlobState(impl); gs != nil {
+		stub.gs = gs
+	}
+	return stub
+}
+
+type implTypeTesterServerStub struct {
+	impl TypeTesterServerMethods
+	gs   *ipc.GlobState
+}
+
+func (s implTypeTesterServerStub) EchoBool(ctx ipc.ServerCall, i0 bool) (bool, error) {
+	return s.impl.EchoBool(ctx, i0)
+}
+
+func (s implTypeTesterServerStub) EchoFloat32(ctx ipc.ServerCall, i0 float32) (float32, error) {
+	return s.impl.EchoFloat32(ctx, i0)
+}
+
+func (s implTypeTesterServerStub) EchoFloat64(ctx ipc.ServerCall, i0 float64) (float64, error) {
+	return s.impl.EchoFloat64(ctx, i0)
+}
+
+func (s implTypeTesterServerStub) EchoInt32(ctx ipc.ServerCall, i0 int32) (int32, error) {
+	return s.impl.EchoInt32(ctx, i0)
+}
+
+func (s implTypeTesterServerStub) EchoInt64(ctx ipc.ServerCall, i0 int64) (int64, error) {
+	return s.impl.EchoInt64(ctx, i0)
+}
+
+func (s implTypeTesterServerStub) EchoString(ctx ipc.ServerCall, i0 string) (string, error) {
+	return s.impl.EchoString(ctx, i0)
+}
+
+func (s implTypeTesterServerStub) EchoByte(ctx ipc.ServerCall, i0 byte) (byte, error) {
+	return s.impl.EchoByte(ctx, i0)
+}
+
+func (s implTypeTesterServerStub) EchoUint32(ctx ipc.ServerCall, i0 uint32) (uint32, error) {
+	return s.impl.EchoUint32(ctx, i0)
+}
+
+func (s implTypeTesterServerStub) EchoUint64(ctx ipc.ServerCall, i0 uint64) (uint64, error) {
+	return s.impl.EchoUint64(ctx, i0)
+}
+
+func (s implTypeTesterServerStub) XEchoArray(ctx ipc.ServerCall, i0 Array2Int) (Array2Int, error) {
+	return s.impl.XEchoArray(ctx, i0)
+}
+
+func (s implTypeTesterServerStub) XEchoMap(ctx ipc.ServerCall, i0 map[int32]string) (map[int32]string, error) {
+	return s.impl.XEchoMap(ctx, i0)
+}
+
+func (s implTypeTesterServerStub) XEchoSet(ctx ipc.ServerCall, i0 map[int32]struct{}) (map[int32]struct{}, error) {
+	return s.impl.XEchoSet(ctx, i0)
+}
+
+func (s implTypeTesterServerStub) XEchoSlice(ctx ipc.ServerCall, i0 []int32) ([]int32, error) {
+	return s.impl.XEchoSlice(ctx, i0)
+}
+
+func (s implTypeTesterServerStub) XEchoStruct(ctx ipc.ServerCall, i0 Struct) (Struct, error) {
+	return s.impl.XEchoStruct(ctx, i0)
+}
+
+func (s implTypeTesterServerStub) YMultiArg(ctx ipc.ServerCall, i0 int32, i1 int32) (int32, int32, error) {
+	return s.impl.YMultiArg(ctx, i0, i1)
+}
+
+func (s implTypeTesterServerStub) YNoArgs(ctx ipc.ServerCall) error {
+	return s.impl.YNoArgs(ctx)
+}
+
+func (s implTypeTesterServerStub) ZStream(ctx *TypeTesterZStreamContextStub, i0 int32, i1 bool) error {
+	return s.impl.ZStream(ctx, i0, i1)
+}
+
+func (s implTypeTesterServerStub) Globber() *ipc.GlobState {
+	return s.gs
+}
+
+func (s implTypeTesterServerStub) Describe__() []ipc.InterfaceDesc {
+	return []ipc.InterfaceDesc{TypeTesterDesc}
+}
+
+// TypeTesterDesc describes the TypeTester interface.
+var TypeTesterDesc ipc.InterfaceDesc = descTypeTester
+
+// descTypeTester hides the desc to keep godoc clean.
+var descTypeTester = ipc.InterfaceDesc{
+	Name:    "TypeTester",
+	PkgPath: "v.io/x/ref/cmd/vrpc/test_base",
+	Doc:     "// TypeTester methods are listed in alphabetical order, to make it easier to\n// test Signature output, which sorts methods alphabetically.",
+	Methods: []ipc.MethodDesc{
+		{
+			Name: "EchoBool",
+			Doc:  "// Methods to test support for primitive types.",
+			InArgs: []ipc.ArgDesc{
+				{"I1", ``}, // bool
+			},
+			OutArgs: []ipc.ArgDesc{
+				{"O1", ``}, // bool
+			},
+		},
+		{
+			Name: "EchoFloat32",
+			InArgs: []ipc.ArgDesc{
+				{"I1", ``}, // float32
+			},
+			OutArgs: []ipc.ArgDesc{
+				{"O1", ``}, // float32
+			},
+		},
+		{
+			Name: "EchoFloat64",
+			InArgs: []ipc.ArgDesc{
+				{"I1", ``}, // float64
+			},
+			OutArgs: []ipc.ArgDesc{
+				{"O1", ``}, // float64
+			},
+		},
+		{
+			Name: "EchoInt32",
+			InArgs: []ipc.ArgDesc{
+				{"I1", ``}, // int32
+			},
+			OutArgs: []ipc.ArgDesc{
+				{"O1", ``}, // int32
+			},
+		},
+		{
+			Name: "EchoInt64",
+			InArgs: []ipc.ArgDesc{
+				{"I1", ``}, // int64
+			},
+			OutArgs: []ipc.ArgDesc{
+				{"O1", ``}, // int64
+			},
+		},
+		{
+			Name: "EchoString",
+			InArgs: []ipc.ArgDesc{
+				{"I1", ``}, // string
+			},
+			OutArgs: []ipc.ArgDesc{
+				{"O1", ``}, // string
+			},
+		},
+		{
+			Name: "EchoByte",
+			InArgs: []ipc.ArgDesc{
+				{"I1", ``}, // byte
+			},
+			OutArgs: []ipc.ArgDesc{
+				{"O1", ``}, // byte
+			},
+		},
+		{
+			Name: "EchoUint32",
+			InArgs: []ipc.ArgDesc{
+				{"I1", ``}, // uint32
+			},
+			OutArgs: []ipc.ArgDesc{
+				{"O1", ``}, // uint32
+			},
+		},
+		{
+			Name: "EchoUint64",
+			InArgs: []ipc.ArgDesc{
+				{"I1", ``}, // uint64
+			},
+			OutArgs: []ipc.ArgDesc{
+				{"O1", ``}, // uint64
+			},
+		},
+		{
+			Name: "XEchoArray",
+			Doc:  "// Methods to test support for composite types.",
+			InArgs: []ipc.ArgDesc{
+				{"I1", ``}, // Array2Int
+			},
+			OutArgs: []ipc.ArgDesc{
+				{"O1", ``}, // Array2Int
+			},
+		},
+		{
+			Name: "XEchoMap",
+			InArgs: []ipc.ArgDesc{
+				{"I1", ``}, // map[int32]string
+			},
+			OutArgs: []ipc.ArgDesc{
+				{"O1", ``}, // map[int32]string
+			},
+		},
+		{
+			Name: "XEchoSet",
+			InArgs: []ipc.ArgDesc{
+				{"I1", ``}, // map[int32]struct{}
+			},
+			OutArgs: []ipc.ArgDesc{
+				{"O1", ``}, // map[int32]struct{}
+			},
+		},
+		{
+			Name: "XEchoSlice",
+			InArgs: []ipc.ArgDesc{
+				{"I1", ``}, // []int32
+			},
+			OutArgs: []ipc.ArgDesc{
+				{"O1", ``}, // []int32
+			},
+		},
+		{
+			Name: "XEchoStruct",
+			InArgs: []ipc.ArgDesc{
+				{"I1", ``}, // Struct
+			},
+			OutArgs: []ipc.ArgDesc{
+				{"O1", ``}, // Struct
+			},
+		},
+		{
+			Name: "YMultiArg",
+			Doc:  "// Methods to test support for different number of arguments.",
+			InArgs: []ipc.ArgDesc{
+				{"I1", ``}, // int32
+				{"I2", ``}, // int32
+			},
+			OutArgs: []ipc.ArgDesc{
+				{"O1", ``}, // int32
+				{"O2", ``}, // int32
+			},
+		},
+		{
+			Name: "YNoArgs",
+		},
+		{
+			Name: "ZStream",
+			Doc:  "// Methods to test support for streaming.",
+			InArgs: []ipc.ArgDesc{
+				{"NumStreamItems", ``}, // int32
+				{"StreamItem", ``},     // bool
+			},
+		},
+	},
+}
+
+// TypeTesterZStreamServerStream is the server stream for TypeTester.ZStream.
+type TypeTesterZStreamServerStream interface {
+	// SendStream returns the send side of the TypeTester.ZStream server stream.
+	SendStream() interface {
+		// Send places the item onto the output stream.  Returns errors encountered
+		// while sending.  Blocks if there is no buffer space; will unblock when
+		// buffer space is available.
+		Send(item bool) error
+	}
+}
+
+// TypeTesterZStreamContext represents the context passed to TypeTester.ZStream.
+type TypeTesterZStreamContext interface {
+	ipc.ServerCall
+	TypeTesterZStreamServerStream
+}
+
+// TypeTesterZStreamContextStub is a wrapper that converts ipc.StreamServerCall into
+// a typesafe stub that implements TypeTesterZStreamContext.
+type TypeTesterZStreamContextStub struct {
+	ipc.StreamServerCall
+}
+
+// Init initializes TypeTesterZStreamContextStub from ipc.StreamServerCall.
+func (s *TypeTesterZStreamContextStub) Init(call ipc.StreamServerCall) {
+	s.StreamServerCall = call
+}
+
+// SendStream returns the send side of the TypeTester.ZStream server stream.
+func (s *TypeTesterZStreamContextStub) SendStream() interface {
+	Send(item bool) error
+} {
+	return implTypeTesterZStreamContextSend{s}
+}
+
+type implTypeTesterZStreamContextSend struct {
+	s *TypeTesterZStreamContextStub
+}
+
+func (s implTypeTesterZStreamContextSend) Send(item bool) error {
+	return s.s.Send(item)
+}
diff --git a/cmd/vrpc/vrpc.go b/cmd/vrpc/vrpc.go
new file mode 100644
index 0000000..1241b0b
--- /dev/null
+++ b/cmd/vrpc/vrpc.go
@@ -0,0 +1,265 @@
+// The following enables go generate to generate the doc.go file.
+//go:generate go run $VANADIUM_ROOT/release/go/src/v.io/x/lib/cmdline/testdata/gendoc.go .
+
+package main
+
+import (
+	"fmt"
+	"io"
+	"os"
+	"strings"
+	"time"
+
+	"v.io/v23"
+	"v.io/v23/context"
+	"v.io/v23/ipc/reserved"
+	"v.io/v23/options"
+	"v.io/v23/vdl"
+	"v.io/v23/vdlroot/signature"
+	"v.io/x/lib/cmdline"
+	"v.io/x/ref/lib/vdl/build"
+	"v.io/x/ref/lib/vdl/codegen/vdlgen"
+	"v.io/x/ref/lib/vdl/compile"
+
+	_ "v.io/x/ref/profiles"
+)
+
+var gctx *context.T
+
+func main() {
+	var shutdown v23.Shutdown
+	gctx, shutdown = v23.Init()
+	exitCode := cmdVRPC.Main()
+	shutdown()
+	os.Exit(exitCode)
+}
+
+var cmdVRPC = &cmdline.Command{
+	Name:  "vrpc",
+	Short: "Vanadium Remote Procedure Call tool",
+	Long: `
+The vrpc tool provides command-line access to Vanadium servers via Remote
+Procedure Call.
+`,
+	// TODO(toddw): Add cmdServe, which will take an interface as input, and set
+	// up a server capable of handling the given methods.  When a request is
+	// received, it'll allow the user to respond via stdin.
+	Children: []*cmdline.Command{cmdSignature, cmdCall, cmdIdentify},
+}
+
+const serverDesc = `
+<server> identifies a Vanadium server.  It can either be the object address of
+the server, or an object name that will be resolved to an end-point.
+`
+
+var cmdSignature = &cmdline.Command{
+	Run:   runSignature,
+	Name:  "signature",
+	Short: "Describe the interfaces of a Vanadium server",
+	Long: `
+Signature connects to the Vanadium server identified by <server>.
+
+If no [method] is provided, returns all interfaces implemented by the server.
+
+If a [method] is provided, returns the signature of just that method.
+`,
+	ArgsName: "<server> [method]",
+	ArgsLong: serverDesc + `
+[method] is the optional server method name.
+`,
+}
+
+var cmdCall = &cmdline.Command{
+	Run:   runCall,
+	Name:  "call",
+	Short: "Call a method of a Vanadium server",
+	Long: `
+Call connects to the Vanadium server identified by <server> and calls the
+<method> with the given positional [args...], returning results on stdout.
+
+TODO(toddw): stdin is read for streaming arguments sent to the server.  An EOF
+on stdin (e.g. via ^D) causes the send stream to be closed.
+
+Regardless of whether the call is streaming, the main goroutine blocks for
+streaming and positional results received from the server.
+
+All input arguments (both positional and streaming) are specified as VDL
+expressions, with commas separating multiple expressions.  Positional arguments
+may also be specified as separate command-line arguments.  Streaming arguments
+may also be specified as separate newline-terminated expressions.
+
+The method signature is always retrieved from the server as a first step.  This
+makes it easier to input complex typed arguments, since the top-level type for
+each argument is implicit and doesn't need to be specified.
+`,
+	ArgsName: "<server> <method> [args...]",
+	ArgsLong: serverDesc + `
+<method> is the server method to call.
+
+[args...] are the positional input arguments, specified as VDL expressions.
+`,
+}
+
+var cmdIdentify = &cmdline.Command{
+	Run:   runIdentify,
+	Name:  "identify",
+	Short: "Reveal blessings presented by a Vanadium server",
+	Long: `
+Identify connects to the Vanadium server identified by <server> and dumps out
+the blessings presented by that server (and the subset of those that are
+considered valid by the principal running this tool) to standard output.
+`,
+	ArgsName: "<server>",
+	ArgsLong: serverDesc,
+}
+
+func runSignature(cmd *cmdline.Command, args []string) error {
+	// Error-check args.
+	var server, method string
+	switch len(args) {
+	case 1:
+		server = args[0]
+	case 2:
+		server, method = args[0], args[1]
+	default:
+		return cmd.UsageErrorf("wrong number of arguments")
+	}
+	// Get the interface or method signature, and pretty-print.  We print the
+	// named types after the signatures, to aid in readability.
+	ctx, cancel := context.WithTimeout(gctx, time.Minute)
+	defer cancel()
+	var types vdlgen.NamedTypes
+	if method != "" {
+		methodSig, err := reserved.MethodSignature(ctx, server, method, options.SkipResolveAuthorization{})
+		if err != nil {
+			return fmt.Errorf("MethodSignature failed: %v", err)
+		}
+		vdlgen.PrintMethod(cmd.Stdout(), methodSig, &types)
+		fmt.Fprintln(cmd.Stdout())
+		types.Print(cmd.Stdout())
+		return nil
+	}
+	ifacesSig, err := reserved.Signature(ctx, server, options.SkipResolveAuthorization{})
+	if err != nil {
+		return fmt.Errorf("Signature failed: %v", err)
+	}
+	for i, iface := range ifacesSig {
+		if i > 0 {
+			fmt.Fprintln(cmd.Stdout())
+		}
+		vdlgen.PrintInterface(cmd.Stdout(), iface, &types)
+		fmt.Fprintln(cmd.Stdout())
+	}
+	types.Print(cmd.Stdout())
+	return nil
+}
+
+func runCall(cmd *cmdline.Command, args []string) error {
+	// Error-check args, and set up argsdata with a comma-separated list of
+	// arguments, allowing each individual arg to already be comma-separated.
+	//
+	// TODO(toddw): Should we just space-separate the args instead?
+	if len(args) < 2 {
+		return cmd.UsageErrorf("must specify <server> and <method>")
+	}
+	server, method := args[0], args[1]
+	var argsdata string
+	for _, arg := range args[2:] {
+		arg := strings.TrimSpace(arg)
+		if argsdata == "" || strings.HasSuffix(argsdata, ",") || strings.HasPrefix(arg, ",") {
+			argsdata += arg
+		} else {
+			argsdata += "," + arg
+		}
+	}
+	// Get the method signature and parse args.
+	ctx, cancel := context.WithTimeout(gctx, time.Minute)
+	defer cancel()
+	methodSig, err := reserved.MethodSignature(ctx, server, method)
+	if err != nil {
+		return fmt.Errorf("MethodSignature failed: %v", err)
+	}
+	inargs, err := parseInArgs(argsdata, methodSig)
+	if err != nil {
+		// TODO: Print signature and example.
+		return err
+	}
+	// Start the method call.
+	call, err := v23.GetClient(ctx).StartCall(ctx, server, method, inargs)
+	if err != nil {
+		return fmt.Errorf("StartCall failed: %v", err)
+	}
+	// TODO(toddw): Fire off a goroutine to handle streaming inputs.
+	// Handle streaming results.
+StreamingResultsLoop:
+	for {
+		var item *vdl.Value
+		switch err := call.Recv(&item); {
+		case err == io.EOF:
+			break StreamingResultsLoop
+		case err != nil:
+			return fmt.Errorf("call.Recv failed: %v", err)
+		}
+		fmt.Fprintf(cmd.Stdout(), "<< %v\n", vdlgen.TypedConst(item, "", nil))
+	}
+	// Finish the method call.
+	outargs := make([]*vdl.Value, len(methodSig.OutArgs))
+	outptrs := make([]interface{}, len(outargs))
+	for i := range outargs {
+		outptrs[i] = &outargs[i]
+	}
+	if err := call.Finish(outptrs...); err != nil {
+		return fmt.Errorf("call.Finish failed: %v", err)
+	}
+	// Pretty-print results.
+	for i, arg := range outargs {
+		if i > 0 {
+			fmt.Fprint(cmd.Stdout(), " ")
+		}
+		fmt.Fprint(cmd.Stdout(), vdlgen.TypedConst(arg, "", nil))
+	}
+	fmt.Fprintln(cmd.Stdout())
+	return nil
+}
+
+func parseInArgs(argsdata string, methodSig signature.Method) ([]interface{}, error) {
+	if len(methodSig.InArgs) == 0 {
+		return nil, nil
+	}
+	var intypes []*vdl.Type
+	for _, inarg := range methodSig.InArgs {
+		intypes = append(intypes, inarg.Type)
+	}
+	env := compile.NewEnv(-1)
+	inargs := build.BuildExprs(argsdata, intypes, env)
+	if err := env.Errors.ToError(); err != nil {
+		return nil, fmt.Errorf("can't parse in-args:\n%v", err)
+	}
+	if got, want := len(inargs), len(methodSig.InArgs); got != want {
+		return nil, fmt.Errorf("got %d args, want %d", got, want)
+	}
+	// Translate []*vdl.Value to []interface, with each item still *vdl.Value.
+	var ret []interface{}
+	for _, arg := range inargs {
+		ret = append(ret, arg)
+	}
+	return ret, nil
+}
+
+func runIdentify(cmd *cmdline.Command, args []string) error {
+	if len(args) != 1 {
+		return cmd.UsageErrorf("wrong number of arguments")
+	}
+	server := args[0]
+	ctx, cancel := context.WithTimeout(gctx, time.Minute)
+	defer cancel()
+	// The method name does not matter - only interested in authentication,
+	// not in actually making an RPC.
+	call, err := v23.GetClient(ctx).StartCall(ctx, server, "", nil)
+	if err != nil {
+		return fmt.Errorf(`client.StartCall(%q, "", nil) failed with %v`, server, err)
+	}
+	valid, presented := call.RemoteBlessings()
+	fmt.Fprintf(cmd.Stdout(), "PRESENTED:  %v\nVALID:      %v\n", presented, valid)
+	return nil
+}
diff --git a/cmd/vrpc/vrpc_test.go b/cmd/vrpc/vrpc_test.go
new file mode 100644
index 0000000..2b442ae
--- /dev/null
+++ b/cmd/vrpc/vrpc_test.go
@@ -0,0 +1,300 @@
+package main
+
+import (
+	"bytes"
+	"strings"
+	"testing"
+
+	"v.io/v23"
+	"v.io/v23/ipc"
+	"v.io/x/lib/vlog"
+
+	"v.io/x/ref/cmd/vrpc/test_base"
+	"v.io/x/ref/lib/testutil"
+	_ "v.io/x/ref/profiles"
+)
+
+type server struct{}
+
+// TypeTester interface implementation
+
+func (*server) EchoBool(call ipc.ServerCall, i1 bool) (bool, error) {
+	vlog.VI(2).Info("EchoBool(%v) was called.", i1)
+	return i1, nil
+}
+
+func (*server) EchoFloat32(call ipc.ServerCall, i1 float32) (float32, error) {
+	vlog.VI(2).Info("EchoFloat32(%v) was called.", i1)
+	return i1, nil
+}
+
+func (*server) EchoFloat64(call ipc.ServerCall, i1 float64) (float64, error) {
+	vlog.VI(2).Info("EchoFloat64(%v) was called.", i1)
+	return i1, nil
+}
+
+func (*server) EchoInt32(call ipc.ServerCall, i1 int32) (int32, error) {
+	vlog.VI(2).Info("EchoInt32(%v) was called.", i1)
+	return i1, nil
+}
+
+func (*server) EchoInt64(call ipc.ServerCall, i1 int64) (int64, error) {
+	vlog.VI(2).Info("EchoInt64(%v) was called.", i1)
+	return i1, nil
+}
+
+func (*server) EchoString(call ipc.ServerCall, i1 string) (string, error) {
+	vlog.VI(2).Info("EchoString(%v) was called.", i1)
+	return i1, nil
+}
+
+func (*server) EchoByte(call ipc.ServerCall, i1 byte) (byte, error) {
+	vlog.VI(2).Info("EchoByte(%v) was called.", i1)
+	return i1, nil
+}
+
+func (*server) EchoUint32(call ipc.ServerCall, i1 uint32) (uint32, error) {
+	vlog.VI(2).Info("EchoUint32(%v) was called.", i1)
+	return i1, nil
+}
+
+func (*server) EchoUint64(call ipc.ServerCall, i1 uint64) (uint64, error) {
+	vlog.VI(2).Info("EchoUint64(%v) was called.", i1)
+	return i1, nil
+}
+
+func (*server) XEchoArray(call ipc.ServerCall, i1 test_base.Array2Int) (test_base.Array2Int, error) {
+	vlog.VI(2).Info("XEchoArray(%v) was called.", i1)
+	return i1, nil
+}
+
+func (*server) XEchoMap(call ipc.ServerCall, i1 map[int32]string) (map[int32]string, error) {
+	vlog.VI(2).Info("XEchoMap(%v) was called.", i1)
+	return i1, nil
+}
+
+func (*server) XEchoSet(call ipc.ServerCall, i1 map[int32]struct{}) (map[int32]struct{}, error) {
+	vlog.VI(2).Info("XEchoSet(%v) was called.", i1)
+	return i1, nil
+}
+
+func (*server) XEchoSlice(call ipc.ServerCall, i1 []int32) ([]int32, error) {
+	vlog.VI(2).Info("XEchoSlice(%v) was called.", i1)
+	return i1, nil
+}
+
+func (*server) XEchoStruct(call ipc.ServerCall, i1 test_base.Struct) (test_base.Struct, error) {
+	vlog.VI(2).Info("XEchoStruct(%v) was called.", i1)
+	return i1, nil
+}
+
+func (*server) YMultiArg(call ipc.ServerCall, i1, i2 int32) (int32, int32, error) {
+	vlog.VI(2).Info("YMultiArg(%v,%v) was called.", i1, i2)
+	return i1, i2, nil
+}
+
+func (*server) YNoArgs(call ipc.ServerCall) error {
+	vlog.VI(2).Info("YNoArgs() was called.")
+	return nil
+}
+
+func (*server) ZStream(ctx test_base.TypeTesterZStreamContext, nStream int32, item bool) error {
+	vlog.VI(2).Info("ZStream(%v,%v) was called.", nStream, item)
+	sender := ctx.SendStream()
+	for i := int32(0); i < nStream; i++ {
+		sender.Send(item)
+	}
+	return nil
+}
+
+func initTest(t *testing.T) (name string, shutdown v23.Shutdown) {
+	// The gctx initialized here is the global context defined in vrpc.go.
+	gctx, shutdown = testutil.InitForTest()
+
+	ipcServer, err := v23.NewServer(gctx)
+	if err != nil {
+		t.Fatalf("NewServer failed: %v", err)
+		return
+	}
+	endpoints, err := ipcServer.Listen(v23.GetListenSpec(gctx))
+	if err != nil {
+		t.Fatalf("Listen failed: %v", err)
+		return
+	}
+	name = endpoints[0].Name()
+	obj := test_base.TypeTesterServer(&server{})
+	if err := ipcServer.Serve("", obj, nil); err != nil {
+		t.Fatalf("Serve failed: %v", err)
+		return name, shutdown
+	}
+	return name, shutdown
+}
+
+func TestSignature(t *testing.T) {
+	name, shutdown := initTest(t)
+	defer shutdown()
+	var stdout, stderr bytes.Buffer
+	cmdVRPC.Init(nil, &stdout, &stderr)
+
+	if err := cmdVRPC.Execute([]string{"signature", name}); err != nil {
+		t.Errorf("%v", err)
+		return
+	}
+	wantSig := `// TypeTester methods are listed in alphabetical order, to make it easier to
+// test Signature output, which sorts methods alphabetically.
+type "v.io/x/ref/cmd/vrpc/test_base".TypeTester interface {
+	// Methods to test support for primitive types.
+	EchoBool(I1 bool) (O1 bool | error)
+	EchoByte(I1 byte) (O1 byte | error)
+	EchoFloat32(I1 float32) (O1 float32 | error)
+	EchoFloat64(I1 float64) (O1 float64 | error)
+	EchoInt32(I1 int32) (O1 int32 | error)
+	EchoInt64(I1 int64) (O1 int64 | error)
+	EchoString(I1 string) (O1 string | error)
+	EchoUint32(I1 uint32) (O1 uint32 | error)
+	EchoUint64(I1 uint64) (O1 uint64 | error)
+	// Methods to test support for composite types.
+	XEchoArray(I1 "v.io/x/ref/cmd/vrpc/test_base".Array2Int) (O1 "v.io/x/ref/cmd/vrpc/test_base".Array2Int | error)
+	XEchoMap(I1 map[int32]string) (O1 map[int32]string | error)
+	XEchoSet(I1 set[int32]) (O1 set[int32] | error)
+	XEchoSlice(I1 []int32) (O1 []int32 | error)
+	XEchoStruct(I1 "v.io/x/ref/cmd/vrpc/test_base".Struct) (O1 "v.io/x/ref/cmd/vrpc/test_base".Struct | error)
+	// Methods to test support for different number of arguments.
+	YMultiArg(I1 int32, I2 int32) (O1 int32, O2 int32 | error)
+	YNoArgs() error
+	// Methods to test support for streaming.
+	ZStream(NumStreamItems int32, StreamItem bool) stream<_, bool> error
+}
+
+// Reserved methods implemented by the IPC framework.  Each method name is prefixed with a double underscore "__".
+type __Reserved interface {
+	// Glob returns all entries matching the pattern.
+	__Glob(pattern string) stream<any, any> error
+	// MethodSignature returns the signature for the given method.
+	__MethodSignature(method string) ("signature".Method | error)
+	// Signature returns all interface signatures implemented by the object.
+	__Signature() ([]"signature".Interface | error)
+}
+
+type "signature".Arg struct {
+	Name string
+	Doc string
+	Type typeobject
+}
+
+type "signature".Embed struct {
+	Name string
+	PkgPath string
+	Doc string
+}
+
+type "signature".Interface struct {
+	Name string
+	PkgPath string
+	Doc string
+	Embeds []"signature".Embed
+	Methods []"signature".Method
+}
+
+type "signature".Method struct {
+	Name string
+	Doc string
+	InArgs []"signature".Arg
+	OutArgs []"signature".Arg
+	InStream ?"signature".Arg
+	OutStream ?"signature".Arg
+	Tags []any
+}
+
+type "v.io/x/ref/cmd/vrpc/test_base".Array2Int [2]int32
+
+type "v.io/x/ref/cmd/vrpc/test_base".Struct struct {
+	X int32
+	Y int32
+}
+`
+	if got, want := stdout.String(), wantSig; got != want {
+		t.Errorf("got stdout %q, want %q", got, want)
+	}
+	if got, want := stderr.String(), ""; got != want {
+		t.Errorf("got stderr %q, want %q", got, want)
+	}
+}
+
+func TestMethodSignature(t *testing.T) {
+	name, shutdown := initTest(t)
+	defer shutdown()
+
+	tests := []struct {
+		Method, Want string
+	}{
+		// Spot-check some individual methods.
+		{"EchoByte", `EchoByte(I1 byte) (O1 byte | error)`},
+		{"EchoFloat32", `EchoFloat32(I1 float32) (O1 float32 | error)`},
+		{"XEchoStruct", `
+XEchoStruct(I1 "v.io/x/ref/cmd/vrpc/test_base".Struct) (O1 "v.io/x/ref/cmd/vrpc/test_base".Struct | error)
+
+type "v.io/x/ref/cmd/vrpc/test_base".Struct struct {
+	X int32
+	Y int32
+}
+`},
+	}
+	for _, test := range tests {
+		var stdout, stderr bytes.Buffer
+		cmdVRPC.Init(nil, &stdout, &stderr)
+		if err := cmdVRPC.Execute([]string{"signature", name, test.Method}); err != nil {
+			t.Errorf("%q failed: %v", test.Method, err)
+			continue
+		}
+		if got, want := strings.TrimSpace(stdout.String()), strings.TrimSpace(test.Want); got != want {
+			t.Errorf("got stdout %q, want %q", got, want)
+		}
+		if got, want := stderr.String(), ""; got != want {
+			t.Errorf("got stderr %q, want %q", got, want)
+		}
+	}
+}
+
+func TestCall(t *testing.T) {
+	name, shutdown := initTest(t)
+	defer shutdown()
+
+	tests := []struct {
+		Method, InArgs, Want string
+	}{
+		{"EchoBool", `true`, `true`},
+		{"EchoBool", `false`, `false`},
+		{"EchoFloat32", `1.2`, `float32(1.2)`},
+		{"EchoFloat64", `-3.4`, `float64(-3.4)`},
+		{"EchoInt32", `11`, `int32(11)`},
+		{"EchoInt64", `-22`, `int64(-22)`},
+		{"EchoString", `"abc"`, `"abc"`},
+		{"EchoByte", `33`, `byte(33)`},
+		{"EchoUint32", `44`, `uint32(44)`},
+		{"EchoUint64", `55`, `uint64(55)`},
+		{"XEchoArray", `{1,2}`, `"v.io/x/ref/cmd/vrpc/test_base".Array2Int{1, 2}`},
+		{"XEchoMap", `{1:"a"}`, `map[int32]string{1: "a"}`},
+		{"XEchoSet", `{1}`, `set[int32]{1}`},
+		{"XEchoSlice", `{1,2}`, `[]int32{1, 2}`},
+		{"XEchoStruct", `{1,2}`, `"v.io/x/ref/cmd/vrpc/test_base".Struct{X: 1, Y: 2}`},
+		{"YMultiArg", `1,2`, `int32(1) int32(2)`},
+		{"YNoArgs", ``, ``},
+		{"ZStream", `2,true`, `<< true
+<< true`},
+	}
+	for _, test := range tests {
+		var stdout, stderr bytes.Buffer
+		cmdVRPC.Init(nil, &stdout, &stderr)
+		if err := cmdVRPC.Execute([]string{"call", name, test.Method, test.InArgs}); err != nil {
+			t.Errorf("%q(%s) failed: %v", test.Method, test.InArgs, err)
+			continue
+		}
+		if got, want := strings.TrimSpace(stdout.String()), strings.TrimSpace(test.Want); got != want {
+			t.Errorf("got stdout %q, want %q", got, want)
+		}
+		if got, want := stderr.String(), ""; got != want {
+			t.Errorf("got stderr %q, want %q", got, want)
+		}
+	}
+}
diff --git a/cmd/vrun/doc.go b/cmd/vrun/doc.go
new file mode 100644
index 0000000..edb137a
--- /dev/null
+++ b/cmd/vrun/doc.go
@@ -0,0 +1,41 @@
+/*
+The vrun tool executes a command with a derived principal.
+
+Usage:
+   vrun [flags] <command> [command args...]
+
+The vrun flags are:
+ -duration=1h0m0s
+   Duration for the blessing.
+ -name=
+   Name to use for the blessing. Uses the command name if unset.
+
+The global flags are:
+ -alsologtostderr=true
+   log to standard error as well as files
+ -log_backtrace_at=:0
+   when logging hits line file:N, emit a stack trace
+ -log_dir=
+   if non-empty, write log files to this directory
+ -logtostderr=false
+   log to standard error instead of files
+ -max_stack_buf_size=4292608
+   max size in bytes of the buffer to use for logging stack traces
+ -stderrthreshold=2
+   logs at or above this threshold go to stderr
+ -v=0
+   log level for V logs
+ -veyron.credentials=
+   directory to use for storing security credentials
+ -veyron.namespace.root=[/ns.dev.v.io:8101]
+   local namespace root; can be repeated to provided multiple roots
+ -veyron.vtrace.cache_size=1024
+   The number of vtrace traces to store in memory.
+ -veyron.vtrace.dump_on_shutdown=false
+   If true, dump all stored traces on runtime shutdown.
+ -veyron.vtrace.sample_rate=0
+   Rate (from 0.0 to 1.0) to sample vtrace traces.
+ -vmodule=
+   comma-separated list of pattern=N settings for file-filtered logging
+*/
+package main
diff --git a/cmd/vrun/internal/v23_test.go b/cmd/vrun/internal/v23_test.go
new file mode 100644
index 0000000..b3abb99
--- /dev/null
+++ b/cmd/vrun/internal/v23_test.go
@@ -0,0 +1,25 @@
+// Copyright 2015 The Vanadium 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 file was auto-generated via go generate.
+// DO NOT UPDATE MANUALLY
+package main_test
+
+import "testing"
+import "os"
+
+import "v.io/x/ref/lib/testutil"
+import "v.io/x/ref/lib/testutil/v23tests"
+
+func TestMain(m *testing.M) {
+	testutil.Init()
+	cleanup := v23tests.UseSharedBinDir()
+	r := m.Run()
+	cleanup()
+	os.Exit(r)
+}
+
+func TestV23Agentd(t *testing.T) {
+	v23tests.RunTest(t, V23TestAgentd)
+}
diff --git a/cmd/vrun/internal/vrun_test_helper.go b/cmd/vrun/internal/vrun_test_helper.go
new file mode 100644
index 0000000..97e26ab
--- /dev/null
+++ b/cmd/vrun/internal/vrun_test_helper.go
@@ -0,0 +1,69 @@
+package main
+
+import (
+	"fmt"
+	"os"
+	"os/exec"
+	"path/filepath"
+	"strings"
+)
+
+// Helper script for testing vrun.
+func main() {
+	if len(os.Args) != 4 {
+		fmt.Fprintf(os.Stderr, "usage: %s <vrun_path> <pingpong_path> <principal_path>\n", os.Args[0])
+		os.Exit(1)
+	}
+
+	vrunPath := os.Args[1]
+	pingpongPath := os.Args[2]
+	principalPath := os.Args[3]
+
+	if output, err := exec.Command(principalPath, "dump").Output(); err != nil {
+		fmt.Fprintf(os.Stderr, "could not run %s dump\n", principalPath)
+		os.Exit(1)
+	} else {
+		if want := "Default blessings: agent_principal"; !strings.Contains(string(output), want) {
+			fmt.Fprintf(os.Stderr, "expected output to contain %s, but did not. Output was:\n%s\n")
+			os.Exit(1)
+		}
+	}
+
+	if output, err := exec.Command(vrunPath, principalPath, "dump").Output(); err != nil {
+		fmt.Fprintf(os.Stderr, "could not run %s %s dump\n", vrunPath, principalPath)
+		os.Exit(1)
+	} else {
+		if want := "Default blessings: agent_principal/principal"; !strings.Contains(string(output), want) {
+			fmt.Fprintf(os.Stderr, "expected output to contain %s, but did not. Output was:\n%s\n")
+			os.Exit(1)
+		}
+	}
+
+	if output, err := exec.Command(vrunPath, "--name=foo", principalPath, "dump").Output(); err != nil {
+		fmt.Fprintf(os.Stderr, "could not run %s %s dump\n", vrunPath, principalPath)
+		os.Exit(1)
+	} else {
+		if want := "Default blessings: agent_principal/foo"; !strings.Contains(string(output), want) {
+			fmt.Fprintf(os.Stderr, "expected output to contain %s, but did not. Output was:\n%s\n")
+			os.Exit(1)
+		}
+	}
+
+	server, err := os.StartProcess(vrunPath, []string{filepath.Base(vrunPath), pingpongPath, "--server"}, &os.ProcAttr{})
+	defer func() {
+		if server != nil {
+			server.Kill()
+		}
+	}()
+	if err != nil {
+		fmt.Fprintf(os.Stderr, "could not start server: %v\n", err)
+		os.Exit(1)
+	}
+
+	if output, err := exec.Command(vrunPath, pingpongPath).Output(); err != nil {
+		fmt.Fprintf(os.Stderr, "could not start client: %v\n", err)
+		os.Exit(1)
+	} else {
+		fmt.Fprintf(os.Stdout, "Received output: %s\n", output)
+	}
+}
diff --git a/cmd/vrun/internal/vrun_v23_test.go b/cmd/vrun/internal/vrun_v23_test.go
new file mode 100644
index 0000000..30efa19
--- /dev/null
+++ b/cmd/vrun/internal/vrun_v23_test.go
@@ -0,0 +1,28 @@
+package main_test
+
+//go:generate v23 test generate .
+
+import (
+	"os"
+
+	"v.io/x/ref/lib/testutil/v23tests"
+	_ "v.io/x/ref/profiles/static"
+)
+
+func V23TestAgentd(t *v23tests.T) {
+	vrunBin := t.BuildGoPkg("v.io/x/ref/cmd/vrun")
+	pingpongBin := t.BuildGoPkg("v.io/x/ref/security/agent/pingpong")
+	agentdBin := t.BuildGoPkg("v.io/x/ref/security/agent/agentd")
+	helperBin := t.BuildGoPkg("v.io/x/ref/cmd/vrun/internal")
+	principalBin := t.BuildGoPkg("v.io/x/ref/cmd/principal")
+
+	v23tests.RunRootMT(t, "--veyron.tcp.address=127.0.0.1:0")
+
+	creds := t.NewTempDir()
+	agentdBin.WithEnv("VEYRON_CREDENTIALS="+creds).Start("--no_passphrase",
+		"--additional_principals="+creds,
+		helperBin.Path(),
+		vrunBin.Path(),
+		pingpongBin.Path(),
+		principalBin.Path()).WaitOrDie(os.Stdout, os.Stderr)
+}
diff --git a/cmd/vrun/vrun.go b/cmd/vrun/vrun.go
new file mode 100644
index 0000000..afc3ca5
--- /dev/null
+++ b/cmd/vrun/vrun.go
@@ -0,0 +1,137 @@
+package main
+
+import (
+	"flag"
+	"os"
+	"path/filepath"
+	"syscall"
+	"time"
+
+	"v.io/x/lib/cmdline"
+	"v.io/x/ref/lib/flags/consts"
+	"v.io/x/ref/security/agent"
+	"v.io/x/ref/security/agent/keymgr"
+
+	"v.io/v23"
+	"v.io/v23/context"
+	"v.io/v23/security"
+	"v.io/x/lib/vlog"
+
+	_ "v.io/x/ref/profiles"
+)
+
+var durationFlag time.Duration
+var nameOverride string
+
+var cmdVrun = &cmdline.Command{
+	Run:      vrun,
+	Name:     "vrun",
+	Short:    "Executes a command with a derived principal.",
+	Long:     "The vrun tool executes a command with a derived principal.",
+	ArgsName: "<command> [command args...]",
+}
+
+func main() {
+	syscall.CloseOnExec(3)
+	syscall.CloseOnExec(4)
+
+	flag.DurationVar(&durationFlag, "duration", 1*time.Hour, "Duration for the blessing.")
+	flag.StringVar(&nameOverride, "name", "", "Name to use for the blessing. Uses the command name if unset.")
+	cmdVrun.Flags.DurationVar(&durationFlag, "duration", 1*time.Hour, "Duration for the blessing.")
+	cmdVrun.Flags.StringVar(&nameOverride, "name", "", "Name to use for the blessing. Uses the command name if unset.")
+
+	os.Exit(cmdVrun.Main())
+}
+
+func vrun(cmd *cmdline.Command, args []string) error {
+	ctx, shutdown := v23.Init()
+	defer shutdown()
+
+	if len(args) == 0 {
+		return cmd.UsageErrorf("vrun: no command specified")
+	}
+	principal, conn, err := createPrincipal(ctx)
+	if err != nil {
+		return err
+	}
+	err = bless(ctx, principal, filepath.Base(args[0]))
+	if err != nil {
+		return err
+	}
+	return doExec(args, conn)
+}
+
+func bless(ctx *context.T, p security.Principal, name string) error {
+	caveat, err := security.ExpiryCaveat(time.Now().Add(durationFlag))
+	if err != nil {
+		vlog.Errorf("Couldn't create caveat")
+		return err
+	}
+	if 0 != len(nameOverride) {
+		name = nameOverride
+	}
+
+	rp := v23.GetPrincipal(ctx)
+	blessing, err := rp.Bless(p.PublicKey(), rp.BlessingStore().Default(), name, caveat)
+	if err != nil {
+		vlog.Errorf("Couldn't bless")
+		return err
+	}
+
+	if err = p.BlessingStore().SetDefault(blessing); err != nil {
+		vlog.Errorf("Couldn't set default blessing")
+		return err
+	}
+	if _, err = p.BlessingStore().Set(blessing, security.AllPrincipals); err != nil {
+		vlog.Errorf("Couldn't set default client blessing")
+		return err
+	}
+	if err = p.AddToRoots(blessing); err != nil {
+		vlog.Errorf("Couldn't set trusted roots")
+		return err
+	}
+	return nil
+}
+
+func doExec(cmd []string, conn *os.File) error {
+	os.Setenv(consts.VeyronCredentials, "")
+	os.Setenv(agent.FdVarName, "3")
+	if conn.Fd() != 3 {
+		if err := syscall.Dup2(int(conn.Fd()), 3); err != nil {
+			vlog.Errorf("Couldn't dup fd")
+			return err
+		}
+		conn.Close()
+	}
+	err := syscall.Exec(cmd[0], cmd, os.Environ())
+	vlog.Errorf("Couldn't exec %s.", cmd[0])
+	return err
+}
+
+func createPrincipal(ctx *context.T) (security.Principal, *os.File, error) {
+	kagent, err := keymgr.NewAgent()
+	if err != nil {
+		vlog.Errorf("Could not initialize agent")
+		return nil, nil, err
+	}
+
+	_, conn, err := kagent.NewPrincipal(ctx, true)
+	if err != nil {
+		vlog.Errorf("Couldn't create principal")
+		return nil, nil, err
+	}
+
+	// Connect to the Principal
+	fd, err := syscall.Dup(int(conn.Fd()))
+	if err != nil {
+		vlog.Errorf("Couldn't copy fd")
+		return nil, nil, err
+	}
+	syscall.CloseOnExec(fd)
+	principal, err := agent.NewAgentPrincipal(ctx, fd, v23.GetClient(ctx))
+	if err != nil {
+		vlog.Errorf("Couldn't connect to principal")
+		return nil, nil, err
+	}
+	return principal, conn, nil
+}