Merge "vdl: temporary fix of issue 29: Generate a use of wiretype whenever there is an interface to temporarily fix errors on packages with just bootstrap types"
diff --git a/runtimes/google/ipc/stream/crypto/tls.go b/runtimes/google/ipc/stream/crypto/tls.go
index 314f7ed..8dfb09d 100644
--- a/runtimes/google/ipc/stream/crypto/tls.go
+++ b/runtimes/google/ipc/stream/crypto/tls.go
@@ -195,9 +195,11 @@
 		Certificates:       []tls.Certificate{c},
 		InsecureSkipVerify: true,
 		ClientAuth:         tls.NoClientCert,
-		// TLS_ECDHE_RSA_WITH_RC4_128_SHA is 4-5X faster compared to
-		// the other cipher suites and is what google.com seems to use.
-		CipherSuites: []uint16{tls.TLS_ECDHE_RSA_WITH_RC4_128_SHA},
+		// RC4_128_SHA is 4-5X faster compared to the other cipher suites
+		// and is what google.com seems to use.
+		// Allowing ECDHE_RSA for the key exchange since some older binaries
+		// have an RSA certificate hardcoded in them.
+		CipherSuites: []uint16{tls.TLS_ECDHE_ECDSA_WITH_RC4_128_SHA},
 	}
 }
 
@@ -206,45 +208,28 @@
 //
 // PEM-encoded certificates and keys used in the tests.
 // One way to generate them is:
-//   go run $GOROOT/src/pkg/crypto/tls/generate_cert.go  --host=localhost
+//   go run $GOROOT/src/pkg/crypto/tls/generate_cert.go  --host=localhost --duration=87600h --ecdsa-curve=P256
+// (This generates a self-signed certificate valid for 10 years)
+// (The --ecdsa-curve flag has not yet been submitted back to the Go repository)
 // which will create cert.pem and key.pem files.
 const (
 	serverCert = `
 -----BEGIN CERTIFICATE-----
-MIIC1jCCAj+gAwIBAgIJAOsQamnsz2kWMA0GCSqGSIb3DQEBBQUAMIGDMQswCQYD
-VQQGEwJERTEMMAoGA1UECAwDTlJXMQ4wDAYDVQQHDAVFYXJ0aDEXMBUGA1UECgwO
-UmFuZG9tIENvbXBhbnkxCzAJBgNVBAsMAklUMRcwFQYDVQQDDA53d3cucmFuZG9t
-LmNvbTEXMBUGCSqGSIb3DQEJARYIZ2F1dGhhbXQwHhcNMTMwNDIzMjEzMTA4WhcN
-MjMwNDIxMjEzMTA4WjCBgzELMAkGA1UEBhMCREUxDDAKBgNVBAgMA05SVzEOMAwG
-A1UEBwwFRWFydGgxFzAVBgNVBAoMDlJhbmRvbSBDb21wYW55MQswCQYDVQQLDAJJ
-VDEXMBUGA1UEAwwOd3d3LnJhbmRvbS5jb20xFzAVBgkqhkiG9w0BCQEWCGdhdXRo
-YW10MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDBaDlmU0csZctqYP8AWJ60
-IYGPmT/gGWGo6p0B6jPy02LuY91jQAn0XkiAdjgdtkkkWQyRtQgaaGsGC6qT5qVX
-Ogx/5l/wb5hOa75gGiOdaGxStkzCjS8hAn4Lr0AbI/JmssUQ0xwNJr6t+aHBJ5Go
-gjG0TsedkLL3qw6ktQd47wIDAQABo1AwTjAdBgNVHQ4EFgQUh166SbXiiSTt+Tud
-rLWaA0sS3bQwHwYDVR0jBBgwFoAUh166SbXiiSTt+TudrLWaA0sS3bQwDAYDVR0T
-BAUwAwEB/zANBgkqhkiG9w0BAQUFAAOBgQB8550EwYMrcFXEwQHktpFrcaOEUWN+
-50NeS0lJ0IHwb31dcMywCX0xsteKyUwkXUCSjE8Ubnktjelo3KPMaur78Jy12pK1
-g3Ay6y3nBDKwpBDPcoy7Pt/pz0yL8Qy54fVnU2iQBiHMjTR/kmDsK+BwRksJfk9V
-MFLsr6ZAZxOPbg==
+MIIBbTCCAROgAwIBAgIQMD+Kzawjvhij1B/BmvHxLDAKBggqhkjOPQQDAjASMRAw
+DgYDVQQKEwdBY21lIENvMB4XDTE0MDcxODIzMTYxMloXDTI0MDcxNTIzMTYxMlow
+EjEQMA4GA1UEChMHQWNtZSBDbzBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABLiz
+Ajsly1DS8NJF2KE195V83TgidfgGEB7nudscdKWH3+5uQHgCc+2BV/7AGGj3yePR
+ZZLzYD95goJ/a7eet/2jSzBJMA4GA1UdDwEB/wQEAwIAoDATBgNVHSUEDDAKBggr
+BgEFBQcDATAMBgNVHRMBAf8EAjAAMBQGA1UdEQQNMAuCCWxvY2FsaG9zdDAKBggq
+hkjOPQQDAgNIADBFAiAb4tBxggEpnKdxv66TBVFxAUn3EBWX25XlL1G2GF8RkAIh
+AOAwys3mvzM4Td/2kV9QNyQPZ9kLLQr9A9ryB0H3N9Yz
 -----END CERTIFICATE-----
 `
 	serverKey = `
------BEGIN PRIVATE KEY-----
-MIICdgIBADANBgkqhkiG9w0BAQEFAASCAmAwggJcAgEAAoGBAMFoOWZTRyxly2pg
-/wBYnrQhgY+ZP+AZYajqnQHqM/LTYu5j3WNACfReSIB2OB22SSRZDJG1CBpoawYL
-qpPmpVc6DH/mX/BvmE5rvmAaI51obFK2TMKNLyECfguvQBsj8mayxRDTHA0mvq35
-ocEnkaiCMbROx52QsverDqS1B3jvAgMBAAECgYEAl2Xk+Orb3i9ZSs7fDwBQS6Wm
-7CgEzoJP5pCxk1woij9bRE28cgMhR7++dYEVcHzPSLrEkhLqYvG2RadAQkLczcy+
-NgXFm1I0HcZXbVT2rafaKS27GpT7NicIrIw48goncMwAI0+UB3Ply9RDwfs+VhDo
-G2a8JTVx2FNpJoIIJOECQQDl80AJPi17TbJehEByQOF0Q7KgfN4aD9hx+E6SLdPq
-ddn0xqnmsbBD1EPv25qeAtQ6sHRxjlP03gvhQ4CQQ0+nAkEA11ExtkqGXayf2hAe
-dMwi2JrAuIGtOCQHQOCAADYgIH+3/SIf05kk/PUiXFTlGkm69qpBmLPaiDSfHV6g
-taT1eQJAe9KClveOUilCdTbN5TgerxaNJ3JVvr7tlGFbHcfjpwsS9IXNk1X3Tm8M
-rioYliF72qaN7V/wwZiX2RMaNZSpXQJAXmuBlEG8CGoBsztsT6WRBlFef8qF7l+G
-OsH3/5+8mOPJCB0lvcGjgbXxenHUAaIhdbeVimQcSaxhthxf9ye+aQJAMstlAS7X
-4rJXYVJUL5JQISgz/D5BzM5pbgJivVRcHO2Qk3HZO2F95Sg3lpD1tdOWBtOhOyRS
-AS91NC8w9ruJeg==
------END PRIVATE KEY-----
+-----BEGIN ECDSA PRIVATE KEY-----
+MHcCAQEEIPLfwg+SVC2/xUcKq0bI9y2+SDEEdCeGuxuBz22BhAw1oAoGCCqGSM49
+AwEHoUQDQgAEuLMCOyXLUNLw0kXYoTX3lXzdOCJ1+AYQHue52xx0pYff7m5AeAJz
+7YFX/sAYaPfJ49FlkvNgP3mCgn9rt563/Q==
+-----END ECDSA PRIVATE KEY-----
 `
 )
diff --git a/services/mgmt/node/config/config.go b/services/mgmt/node/config/config.go
new file mode 100644
index 0000000..dc1fb78
--- /dev/null
+++ b/services/mgmt/node/config/config.go
@@ -0,0 +1,139 @@
+// Package config handles configuration state passed across instances of the
+// node manager.
+//
+// The State object captures setting that the node manager needs to be aware of
+// when it starts.  This is passed to the first invocation of the node manager,
+// and then passed down from old node manager to new node manager upon update.
+// The node manager has an implementation-dependent mechanism for parsing and
+// passing state, which is encapsulated by the state sub-package (currently, the
+// mechanism uses environment variables).  When instantiating a new instance of
+// the node manager service, the developer needs to pass in a copy of State.
+// They can obtain this by calling Load, which captures any config state passed
+// by a previous version of node manager during update.  Any new version of the
+// node manager must be able to decode a previous version's config state, even
+// if the new version changes the mechanism for passing this state (that is,
+// node manager implementations must be backward-compatible as far as accepting
+// and passing config state goes).  TODO(caprita): add config state versioning?
+package config
+
+import (
+	"encoding/json"
+	"fmt"
+	"os"
+	"path/filepath"
+	"strings"
+
+	"veyron2/services/mgmt/application"
+)
+
+// State specifies how the node manager is configured.  This should encapsulate
+// what the node manager needs to know and/or be able to mutate about its
+// environment.
+type State struct {
+	// Name is the node manager's object name.  Must be non-empty.
+	Name string
+	// Envelope is the node manager's application envelope.  If nil, any
+	// envelope fetched from the application repository will trigger an
+	// update.
+	Envelope *application.Envelope
+	// Previous holds the local path to the previous version of the node
+	// manager.  If empty, revert is disabled.
+	Previous string
+	// Root is the directory on the local filesystem that contains
+	// the applications' workspaces.  Must be non-empty.
+	Root string
+	// Origin is the application repository object name for the node
+	// manager application.  If empty, update is disabled.
+	Origin string
+	// CurrentLink is the local filesystem soft link that should point to
+	// the version of the node manager binary/script through which node
+	// manager is started.  Node manager is expected to mutate this during
+	// a self-update.  Must be non-empty.
+	CurrentLink string
+}
+
+// Validate checks the config state.
+func (c *State) Validate() error {
+	if c.Name == "" {
+		return fmt.Errorf("Name cannot be empty")
+	}
+	if c.Root == "" {
+		return fmt.Errorf("Root cannot be empty")
+	}
+	if c.CurrentLink == "" {
+		return fmt.Errorf("CurrentLink cannot be empty")
+	}
+	return nil
+}
+
+// Load reconstructs the config state passed to the node manager (presumably by
+// the parent node manager during an update).  Currently, this is done via
+// environment variables.
+func Load() (*State, error) {
+	var env *application.Envelope
+	if jsonEnvelope := os.Getenv(EnvelopeEnv); jsonEnvelope != "" {
+		env = new(application.Envelope)
+		if err := json.Unmarshal([]byte(jsonEnvelope), env); err != nil {
+			return nil, fmt.Errorf("failed to decode envelope from %v: %v", jsonEnvelope, err)
+		}
+	}
+	return &State{
+		Envelope:    env,
+		Previous:    os.Getenv(PreviousEnv),
+		Root:        os.Getenv(RootEnv),
+		Origin:      os.Getenv(OriginEnv),
+		CurrentLink: os.Getenv(CurrentLinkEnv),
+	}, nil
+}
+
+// Save serializes the config state meant to be passed to a child node manager
+// during an update, returning a slice of "key=value" strings, which are
+// expected to be stuffed into environment variable settings by the caller.
+func (c *State) Save(envelope *application.Envelope) ([]string, error) {
+	jsonEnvelope, err := json.Marshal(envelope)
+	if err != nil {
+		return nil, fmt.Errorf("failed to encode envelope %v: %v", envelope, err)
+	}
+	currScript, err := filepath.EvalSymlinks(c.CurrentLink)
+	if err != nil {
+		return nil, fmt.Errorf("EvalSymlink failed: %v", err)
+	}
+	settings := map[string]string{
+		EnvelopeEnv:    string(jsonEnvelope),
+		PreviousEnv:    currScript,
+		RootEnv:        c.Root,
+		OriginEnv:      c.Origin,
+		CurrentLinkEnv: c.CurrentLink,
+	}
+	// We need to manually pass the namespace roots to the child, since we
+	// currently don't have a way for the child to obtain this information
+	// from a config service at start-up.
+	for _, ev := range os.Environ() {
+		p := strings.SplitN(ev, "=", 2)
+		if len(p) != 2 {
+			continue
+		}
+		k, v := p[0], p[1]
+		if strings.HasPrefix(k, "NAMESPACE_ROOT") {
+			settings[k] = v
+		}
+	}
+	var ret []string
+	for k, v := range settings {
+		ret = append(ret, k+"="+v)
+	}
+	return ret, nil
+}
+
+// QuoteEnv wraps environment variable values in double quotes, making them
+// suitable for inclusion in a bash script.
+func QuoteEnv(env []string) (ret []string) {
+	for _, e := range env {
+		if eqIdx := strings.Index(e, "="); eqIdx > 0 {
+			ret = append(ret, fmt.Sprintf("%s=%q", e[:eqIdx], e[eqIdx+1:]))
+		} else {
+			ret = append(ret, e)
+		}
+	}
+	return
+}
diff --git a/services/mgmt/node/config/config_test.go b/services/mgmt/node/config/config_test.go
new file mode 100644
index 0000000..44264b3
--- /dev/null
+++ b/services/mgmt/node/config/config_test.go
@@ -0,0 +1,112 @@
+package config_test
+
+import (
+	"os"
+	"path/filepath"
+	"reflect"
+	"strings"
+	"testing"
+
+	"veyron/services/mgmt/node/config"
+
+	"veyron2/services/mgmt/application"
+)
+
+// TestState checks that encoding/decoding State to child/from parent works
+// as expected.
+func TestState(t *testing.T) {
+	currScript := filepath.Join(os.TempDir(), "fido/was/here")
+	if err := os.MkdirAll(currScript, 0700); err != nil {
+		t.Fatalf("MkdirAll failed: %v", err)
+	}
+	defer os.RemoveAll(currScript)
+	currLink := filepath.Join(os.TempDir(), "familydog")
+	if err := os.Symlink(currScript, currLink); err != nil {
+		t.Fatalf("Symlink failed: %v", err)
+	}
+	defer os.Remove(currLink)
+	state := &config.State{
+		Name:        "fido",
+		Previous:    "doesn't matter",
+		Root:        "fidos/doghouse",
+		Origin:      "pet/store",
+		CurrentLink: currLink,
+	}
+	if err := state.Validate(); err != nil {
+		t.Errorf("Config state %v failed to validate: %v", state, err)
+	}
+	encoded, err := state.Save(&application.Envelope{
+		Title: "dog",
+		Args:  []string{"roll-over", "play-dead"},
+	})
+	if err != nil {
+		t.Errorf("Config state %v Save failed: %v", state, err)
+	}
+	for _, e := range encoded {
+		pair := strings.SplitN(e, "=", 2)
+		os.Setenv(pair[0], pair[1])
+	}
+	decodedState, err := config.Load()
+	if err != nil {
+		t.Errorf("Config state Load failed: %v", err)
+	}
+	expectedState := state
+	expectedState.Envelope = &application.Envelope{
+		Title: "dog",
+		Args:  []string{"roll-over", "play-dead"},
+	}
+	expectedState.Name = ""
+	expectedState.Previous = currScript
+	if !reflect.DeepEqual(decodedState, expectedState) {
+		t.Errorf("Decode state: want %v, got %v", expectedState, decodedState)
+	}
+}
+
+// TestValidate checks the Validate method of State.
+func TestValidate(t *testing.T) {
+	state := &config.State{
+		Name:        "schweinsteiger",
+		Previous:    "a",
+		Root:        "b",
+		Origin:      "c",
+		CurrentLink: "d",
+	}
+	if err := state.Validate(); err != nil {
+		t.Errorf("Config state %v failed to validate: %v", state, err)
+	}
+	state.Root = ""
+	if err := state.Validate(); err == nil {
+		t.Errorf("Config state %v should have failed to validate.", state)
+	}
+	state.Root, state.CurrentLink = "a", ""
+	if err := state.Validate(); err == nil {
+		t.Errorf("Confi stateg %v should have failed to validate.", state)
+	}
+	state.CurrentLink, state.Name = "d", ""
+	if err := state.Validate(); err == nil {
+		t.Errorf("Config state %v should have failed to validate.", state)
+	}
+}
+
+// TestQuoteEnv checks the QuoteEnv method.
+func TestQuoteEnv(t *testing.T) {
+	cases := []struct {
+		before, after string
+	}{
+		{`a=b`, `a="b"`},
+		{`a=`, `a=""`},
+		{`a`, `a`},
+		{`a=x y`, `a="x y"`},
+		{`a="x y"`, `a="\"x y\""`},
+		{`a='x y'`, `a="'x y'"`},
+	}
+	var input []string
+	var want []string
+	for _, c := range cases {
+		input = append(input, c.before)
+		want = append(want, c.after)
+	}
+	if got := config.QuoteEnv(input); !reflect.DeepEqual(want, got) {
+		t.Errorf("QuoteEnv(%v) wanted %v, got %v instead", input, want, got)
+	}
+}
diff --git a/services/mgmt/node/config/const.go b/services/mgmt/node/config/const.go
new file mode 100644
index 0000000..c57cd98
--- /dev/null
+++ b/services/mgmt/node/config/const.go
@@ -0,0 +1,21 @@
+package config
+
+const (
+	// EnvelopeEnv is the name of the environment variable that holds the
+	// serialized node manager application envelope.
+	EnvelopeEnv = "VEYRON_NM_ENVELOPE"
+	// PreviousEnv is the name of the environment variable that holds the
+	// path to the previous version of the node manager.
+	PreviousEnv = "VEYRON_NM_PREVIOUS"
+	// OriginEnv is the name of the environment variable that holds the
+	// object name of the application repository that can be used to
+	// retrieve the node manager application envelope.
+	OriginEnv = "VEYRON_NM_ORIGIN"
+	// RootEnv is the name of the environment variable that holds the
+	// path to the directory in which node manager workspaces are
+	// created.
+	RootEnv = "VEYRON_NM_ROOT"
+	// CurrentLinkEnv is the name of the environment variable that holds
+	// the path to the soft link that points to the current node manager.
+	CurrentLinkEnv = "VEYRON_NM_CURRENT"
+)
diff --git a/services/mgmt/node/doc.go b/services/mgmt/node/doc.go
new file mode 100644
index 0000000..2f98bea
--- /dev/null
+++ b/services/mgmt/node/doc.go
@@ -0,0 +1,34 @@
+// Package node contains the implementation for the veyron2/mgmt/node APIs.
+//
+// The node manager is a server that is expected to run on every Veyron-enabled
+// node, and it handles both node management and management of the applications
+// running on the node.
+//
+// The node manager is responsible for installing, updating, and launching
+// applications.  It therefore sets up a footprint on the local filesystem, both
+// to maintain its internal state, and to provide applications with their own
+// private workspaces.
+//
+// The node manager is responsible for updating itself.  The mechanism to do so
+// is implementation-dependent, though each node manager expects to be supplied
+// with the file path of a symbolic link file, which the node manager will then
+// update to point to the updated version of itself before terminating itself.
+// The node manager should therefore be launched via this symbolic link to
+// enable auto-updates.  To enable updates, in addition to the symbolic link
+// path, the node manager needs to be told what its application metadata is
+// (such as command-line arguments and environment variables, i.e. the
+// application envelope defined in the veyron2/services/mgmt/application
+// package), as well as the object name for where it can fetch an updated
+// envelope, and the local filesystem path for its previous version (for
+// rollbacks).
+//
+// Finally, the node manager needs to know its own object name, so it can pass
+// that along to the applications that it starts.
+//
+// The impl subpackage contains the implementation of the node manager service.
+//
+// The config subpackage encapsulates the configuration settings that form the
+// node manager service's 'contract' with its environment.
+//
+// The noded subpackage contains the main driver.
+package node
diff --git a/services/mgmt/node/impl/callback.go b/services/mgmt/node/impl/callback.go
new file mode 100644
index 0000000..787425d
--- /dev/null
+++ b/services/mgmt/node/impl/callback.go
@@ -0,0 +1,34 @@
+package impl
+
+import (
+	"veyron/services/mgmt/lib/exec"
+	inode "veyron/services/mgmt/node"
+
+	"veyron2/mgmt"
+	"veyron2/rt"
+	"veyron2/vlog"
+)
+
+// InvokeCallback provides the parent node manager with the given name (which is
+// expected to be this node manager's object name).
+func InvokeCallback(name string) {
+	handle, err := exec.GetChildHandle()
+	switch err {
+	case nil:
+		// Node manager was started by self-update, notify the parent.
+		callbackName, err := handle.Config.Get(mgmt.ParentNodeManagerConfigKey)
+		if err != nil {
+			vlog.Fatalf("Failed to get callback name from config: %v", err)
+		}
+		nmClient, err := inode.BindNode(callbackName)
+		if err != nil {
+			vlog.Fatalf("BindNode(%v) failed: %v", callbackName, err)
+		}
+		if err := nmClient.Set(rt.R().NewContext(), mgmt.ChildNodeManagerConfigKey, name); err != nil {
+			vlog.Fatalf("Set(%v, %v) failed: %v", mgmt.ChildNodeManagerConfigKey, name, err)
+		}
+	case exec.ErrNoVersion:
+	default:
+		vlog.Fatalf("GetChildHandle() failed: %v", err)
+	}
+}
diff --git a/services/mgmt/node/impl/const.go b/services/mgmt/node/impl/const.go
deleted file mode 100644
index c3c3d7b..0000000
--- a/services/mgmt/node/impl/const.go
+++ /dev/null
@@ -1,19 +0,0 @@
-package impl
-
-const (
-	// BinaryEnv is the name of the environment variable that holds the
-	// object name used for obtaining the node manager binary.
-	BinaryEnv = "VEYRON_NM_BINARY"
-	// PreviousEnv is the name of the environment variable that holds
-	// the path to the workspace that contains the previous version of
-	// the node manager.
-	PreviousEnv = "VEYRON_NM_PREVIOUS"
-	// OriginEnv is the name of the environment variable that holds the
-	// object name of the application repository that can be used to
-	// retrieve the node manager application envelope.
-	OriginEnv = "VEYRON_NM_ORIGIN"
-	// RootEnv is the name of the environment variable that holds the
-	// path to the directory in which node manager workspaces are
-	// created.
-	RootEnv = "VEYRON_NM_ROOT"
-)
diff --git a/services/mgmt/node/impl/dispatcher.go b/services/mgmt/node/impl/dispatcher.go
index 19147f6..33abf91 100644
--- a/services/mgmt/node/impl/dispatcher.go
+++ b/services/mgmt/node/impl/dispatcher.go
@@ -1,39 +1,47 @@
 package impl
 
 import (
+	"fmt"
 	"sync"
 
 	"veyron/services/mgmt/node"
+	"veyron/services/mgmt/node/config"
 
 	"veyron2/ipc"
 	"veyron2/security"
-	"veyron2/services/mgmt/application"
 )
 
 // dispatcher holds the state of the node manager dispatcher.
 type dispatcher struct {
-	auth  security.Authorizer
-	state *state
+	auth     security.Authorizer
+	internal *internalState
+	config   *config.State
 }
 
 // NewDispatcher is the node manager dispatcher factory.
-func NewDispatcher(auth security.Authorizer, envelope *application.Envelope, name, previous string) *dispatcher {
+func NewDispatcher(auth security.Authorizer, config *config.State) (*dispatcher, error) {
+	if err := config.Validate(); err != nil {
+		return nil, fmt.Errorf("Invalid config %v: %v", config, err)
+	}
+
 	return &dispatcher{
 		auth: auth,
-		state: &state{
+		internal: &internalState{
 			channels:      make(map[string]chan string),
 			channelsMutex: new(sync.Mutex),
-			envelope:      envelope,
-			name:          name,
-			previous:      previous,
 			updating:      false,
 			updatingMutex: new(sync.Mutex),
 		},
-	}
+		config: config,
+	}, nil
 }
 
 // DISPATCHER INTERFACE IMPLEMENTATION
 
 func (d *dispatcher) Lookup(suffix string) (ipc.Invoker, security.Authorizer, error) {
-	return ipc.ReflectInvoker(node.NewServerNode(NewInvoker(d.state, suffix))), d.auth, nil
+	return ipc.ReflectInvoker(node.NewServerNode(&invoker{
+		internal: d.internal,
+		config:   d.config,
+		suffix:   suffix,
+	})), d.auth, nil
 }
diff --git a/services/mgmt/node/impl/impl_test.go b/services/mgmt/node/impl/impl_test.go
index adb226c..2146596 100644
--- a/services/mgmt/node/impl/impl_test.go
+++ b/services/mgmt/node/impl/impl_test.go
@@ -1,451 +1,341 @@
 package impl_test
 
 import (
-	"crypto/md5"
-	"encoding/hex"
-	"errors"
 	"fmt"
-	"io"
 	"io/ioutil"
 	"os"
+	goexec "os/exec"
 	"path/filepath"
 	"strings"
-	"syscall"
 	"testing"
-	"time"
 
 	"veyron/lib/signals"
-	_ "veyron/lib/testutil"
 	"veyron/lib/testutil/blackbox"
-	"veyron/lib/testutil/security"
 	"veyron/services/mgmt/lib/exec"
-	"veyron/services/mgmt/node"
+	"veyron/services/mgmt/node/config"
 	"veyron/services/mgmt/node/impl"
-	mtlib "veyron/services/mounttable/lib"
 
-	"veyron2"
-	"veyron2/ipc"
-	"veyron2/mgmt"
 	"veyron2/naming"
 	"veyron2/rt"
 	"veyron2/services/mgmt/application"
-	"veyron2/services/mgmt/binary"
-	"veyron2/services/mgmt/repository"
 	"veyron2/verror"
 	"veyron2/vlog"
 )
 
-const (
-	testEnv = "VEYRON_NM_TEST"
-)
-
-var (
-	errOperationFailed = errors.New("operation failed")
-)
-
-func init() {
-	blackbox.CommandTable["nodeManager"] = nodeManager
-}
-
-// arInvoker holds the state of an application repository invocation
-// mock.  On its first invocation of Match, this mock will return a bogus
-// application title.  On the second invocation, it will return the correct
-// node manager app title.  We make use of this behavior to test that Update
-// fails when the app title mismatches.
-type arInvoker struct {
-	firstInvocation bool
-}
-
-// APPLICATION REPOSITORY INTERFACE IMPLEMENTATION
-
-func (i *arInvoker) Match(ipc.ServerContext, []string) (application.Envelope, error) {
-	vlog.VI(0).Infof("Match(), first invocation: %t", i.firstInvocation)
-	envelope := generateEnvelope()
-	if i.firstInvocation {
-		i.firstInvocation = false
-		envelope.Title = "gibberish"
-	} else {
-		envelope.Title = application.NodeManagerTitle
-	}
-	envelope.Env = exec.Setenv(envelope.Env, testEnv, "child")
-	envelope.Binary = "cr"
-	return *envelope, nil
-}
-
-// crInvoker holds the state of a binary repository invocation mock.
-type crInvoker struct{}
-
-// BINARY REPOSITORY INTERFACE IMPLEMENTATION
-
-func (*crInvoker) Create(ipc.ServerContext, int32) error {
-	vlog.VI(0).Infof("Create()")
-	return nil
-}
-
-func (i *crInvoker) Delete(ipc.ServerContext) error {
-	vlog.VI(0).Infof("Delete()")
-	return nil
-}
-
-func (i *crInvoker) Download(_ ipc.ServerContext, _ int32, stream repository.BinaryServiceDownloadStream) error {
-	vlog.VI(0).Infof("Download()")
-	file, err := os.Open(os.Args[0])
-	if err != nil {
-		vlog.Errorf("Open() failed: %v", err)
-		return errOperationFailed
-	}
-	defer file.Close()
-	bufferLength := 4096
-	buffer := make([]byte, bufferLength)
-	for {
-		n, err := file.Read(buffer)
-		switch err {
-		case io.EOF:
-			return nil
-		case nil:
-			if err := stream.Send(buffer[:n]); err != nil {
-				vlog.Errorf("Send() failed: %v", err)
-				return errOperationFailed
-			}
-		default:
-			vlog.Errorf("Read() failed: %v", err)
-			return errOperationFailed
-		}
-	}
-}
-
-func (*crInvoker) DownloadURL(ipc.ServerContext) (string, int64, error) {
-	vlog.VI(0).Infof("DownloadURL()")
-	return "", 0, nil
-}
-
-func (*crInvoker) Stat(ipc.ServerContext) ([]binary.PartInfo, error) {
-	vlog.VI(0).Infof("Stat()")
-	h := md5.New()
-	bytes, err := ioutil.ReadFile(os.Args[0])
-	if err != nil {
-		return []binary.PartInfo{}, errOperationFailed
-	}
-	h.Write(bytes)
-	part := binary.PartInfo{Checksum: hex.EncodeToString(h.Sum(nil)), Size: int64(len(bytes))}
-	return []binary.PartInfo{part}, nil
-}
-
-func (i *crInvoker) Upload(ipc.ServerContext, int32, repository.BinaryServiceUploadStream) error {
-	vlog.VI(0).Infof("Upload()")
-	return nil
-}
-
-func generateBinary(workspace string) string {
-	path := filepath.Join(workspace, "noded")
-	if err := os.Link(os.Args[0], path); err != nil {
-		vlog.Fatalf("Link(%v, %v) failed: %v", os.Args[0], path, err)
-	}
-	return path
-}
-
-func generateEnvelope() *application.Envelope {
-	envelope := &application.Envelope{}
-	envelope.Args = os.Args[1:]
-	for _, env := range os.Environ() {
-		i := strings.Index(env, "=")
-		envelope.Env = append(envelope.Env, fmt.Sprintf("%s=%q", env[:i], env[i+1:]))
-	}
-	return envelope
-}
-
-func generateLink(root, workspace string) {
-	link := filepath.Join(root, impl.CurrentWorkspace)
-	newLink := link + ".new"
-	fi, err := os.Lstat(newLink)
-	if err == nil {
-		if err := os.Remove(fi.Name()); err != nil {
-			vlog.Fatalf("Remove(%v) failed: %v", fi.Name(), err)
-		}
-	}
-	if err := os.Symlink(workspace, newLink); err != nil {
-		vlog.Fatalf("Symlink(%v, %v) failed: %v", workspace, newLink, err)
-	}
-	if err := os.Rename(newLink, link); err != nil {
-		vlog.Fatalf("Rename(%v, %v) failed: %v", newLink, link, err)
-	}
-}
-
-func generateScript(workspace, binary string) string {
-	envelope := generateEnvelope()
-	envelope.Env = exec.Setenv(envelope.Env, testEnv, "parent")
-	output := "#!/bin/bash\n"
-	output += strings.Join(envelope.Env, " ") + " "
-	output += binary + " " + strings.Join(envelope.Args, " ")
-	path := filepath.Join(workspace, "noded.sh")
-	if err := ioutil.WriteFile(path, []byte(output), 0755); err != nil {
-		vlog.Fatalf("WriteFile(%v) failed: %v", path, err)
-	}
-	return path
-}
-
-func invokeCallback(name string) {
-	handle, err := exec.GetChildHandle()
-	switch err {
-	case nil:
-		// Node manager was started by self-update, notify the parent
-		// process that you are ready.
-		handle.SetReady()
-		callbackName, err := handle.Config.Get(mgmt.ParentNodeManagerConfigKey)
-		if err != nil {
-			vlog.Fatalf("Failed to get callback name from config: %v", err)
-		}
-		nmClient, err := node.BindNode(callbackName)
-		if err != nil {
-			vlog.Fatalf("BindNode(%v) failed: %v", callbackName, err)
-		}
-		if err := nmClient.Set(rt.R().NewContext(), mgmt.ChildNodeManagerConfigKey, name); err != nil {
-			vlog.Fatalf("Set(%v, %v) failed: %v", mgmt.ChildNodeManagerConfigKey, name, err)
-		}
-	case exec.ErrNoVersion:
-		vlog.Fatalf("invokeCallback should only be called from child node manager")
-	default:
-		vlog.Fatalf("NewChildHandle() failed: %v", err)
-	}
-}
-
-func invokeUpdate(t *testing.T, name string, expectFail bool) {
-	address := naming.JoinAddressName(name, "nm")
-	stub, err := node.BindNode(address)
-	if err != nil {
-		t.Fatalf("BindNode(%v) failed: %v", address, err)
-	}
-	err = stub.Update(rt.R().NewContext())
-	if expectFail {
-		if !verror.Is(err, verror.BadArg) {
-			t.Fatalf("Unexpected update error: %v", err)
-		}
-	} else if err != nil {
-		t.Fatalf("Update() failed: %v", err)
-	}
-}
-
-// nodeManager is an enclosure for setting up and starting the parent
-// and child node manager used by the TestUpdate() method.
-func nodeManager(argv []string) {
-	root := os.Getenv(impl.RootEnv)
-	switch os.Getenv(testEnv) {
-	case "setup":
-		workspace := filepath.Join(root, fmt.Sprintf("%v", time.Now().Format(time.RFC3339Nano)))
-		perm := os.FileMode(0755)
-		if err := os.MkdirAll(workspace, perm); err != nil {
-			vlog.Fatalf("MkdirAll(%v, %v) failed: %v", workspace, perm, err)
-		}
-		binary := generateBinary(workspace)
-		script := generateScript(workspace, binary)
-		generateLink(root, workspace)
-		argv, envv := []string{}, []string{}
-		if err := syscall.Exec(script, argv, envv); err != nil {
-			vlog.Fatalf("Exec(%v, %v, %v) failed: %v", script, argv, envv, err)
-		}
-	case "parent":
-		runtime := rt.Init()
-		defer runtime.Cleanup()
-		// Set up a mock binary repository, a mock application repository, and a node manager.
-		_, crCleanup := startBinaryRepository()
-		defer crCleanup()
-		_, arCleanup := startApplicationRepository()
-		defer arCleanup()
-		_, nmCleanup := startNodeManager()
-		defer nmCleanup()
-		// Wait until shutdown.
-		<-signals.ShutdownOnSignals()
-		blackbox.WaitForEOFOnStdin()
-	case "child":
-		runtime := rt.Init()
-		defer runtime.Cleanup()
-		// Set up a node manager.
-		name, nmCleanup := startNodeManager()
-		defer nmCleanup()
-		invokeCallback(name)
-		// Wait until shutdown.
-		<-signals.ShutdownOnSignals()
-		blackbox.WaitForEOFOnStdin()
-	}
-}
-
-func spawnNodeManager(t *testing.T, mtName string, idFile string) *blackbox.Child {
-	root := filepath.Join(os.TempDir(), "noded")
-	child := blackbox.HelperCommand(t, "nodeManager")
-	child.Cmd.Env = exec.Setenv(child.Cmd.Env, "NAMESPACE_ROOT", mtName)
-	child.Cmd.Env = exec.Setenv(child.Cmd.Env, "VEYRON_IDENTITY", idFile)
-	child.Cmd.Env = exec.Setenv(child.Cmd.Env, impl.OriginEnv, "ar")
-	child.Cmd.Env = exec.Setenv(child.Cmd.Env, impl.RootEnv, root)
-	child.Cmd.Env = exec.Setenv(child.Cmd.Env, testEnv, "setup")
-	if err := child.Cmd.Start(); err != nil {
-		t.Fatalf("Start() failed: %v", err)
-	}
-	return child
-}
-
-func startApplicationRepository() (string, func()) {
-	server, err := rt.R().NewServer()
-	if err != nil {
-		vlog.Fatalf("NewServer() failed: %v", err)
-	}
-	dispatcher := ipc.SoloDispatcher(repository.NewServerApplication(&arInvoker{firstInvocation: true}), nil)
-	protocol, hostname := "tcp", "localhost:0"
-	endpoint, err := server.Listen(protocol, hostname)
-	if err != nil {
-		vlog.Fatalf("Listen(%v, %v) failed: %v", protocol, hostname, err)
-	}
-	vlog.VI(1).Infof("Application repository running at endpoint: %s", endpoint)
-	name := "ar"
-	if err := server.Serve(name, dispatcher); err != nil {
-		vlog.Fatalf("Serve(%v) failed: %v", name, err)
-	}
-	return name, func() {
-		if err := server.Stop(); err != nil {
-			vlog.Fatalf("Stop() failed: %v", err)
-		}
-	}
-}
-
-func startBinaryRepository() (string, func()) {
-	server, err := rt.R().NewServer()
-	if err != nil {
-		vlog.Fatalf("NewServer() failed: %v", err)
-	}
-	dispatcher := ipc.SoloDispatcher(repository.NewServerBinary(&crInvoker{}), nil)
-	protocol, hostname := "tcp", "localhost:0"
-	endpoint, err := server.Listen(protocol, hostname)
-	if err != nil {
-		vlog.Fatalf("Listen(%v, %v) failed: %v", protocol, hostname, err)
-	}
-	vlog.VI(1).Infof("Binary repository running at endpoint: %s", endpoint)
-	name := "cr"
-	if err := server.Serve(name, dispatcher); err != nil {
-		vlog.Fatalf("Serve(%v) failed: %v", name, err)
-	}
-	return name, func() {
-		if err := server.Stop(); err != nil {
-			vlog.Fatalf("Stop() failed: %v", err)
-		}
-	}
-}
-
-func startMountTable(t *testing.T) (string, func()) {
-	server, err := rt.R().NewServer(veyron2.ServesMountTableOpt(true))
-	if err != nil {
-		t.Fatalf("NewServer() failed: %v", err)
-	}
-	dispatcher, err := mtlib.NewMountTable("")
-	if err != nil {
-		t.Fatalf("NewMountTable() failed: %v", err)
-	}
-	protocol, hostname := "tcp", "localhost:0"
-	endpoint, err := server.Listen(protocol, hostname)
-	if err != nil {
-		t.Fatalf("Listen(%v, %v) failed: %v", protocol, hostname, err)
-	}
-	if err := server.Serve("", dispatcher); err != nil {
-		t.Fatalf("Serve(%v) failed: %v", dispatcher, err)
-	}
-	name := naming.JoinAddressName(endpoint.String(), "")
-	vlog.VI(1).Infof("Mount table name: %v", name)
-	return name, func() {
-		if err := server.Stop(); err != nil {
-			t.Fatalf("Stop() failed: %v", err)
-		}
-	}
-}
-
-func startNodeManager() (string, func()) {
-	server, err := rt.R().NewServer()
-	if err != nil {
-		vlog.Fatalf("NewServer() failed: %v", err)
-	}
-	protocol, hostname := "tcp", "localhost:0"
-	endpoint, err := server.Listen(protocol, hostname)
-	if err != nil {
-		vlog.Fatalf("Listen(%v, %v) failed: %v", protocol, hostname, err)
-	}
-	envelope := &application.Envelope{}
-	name := naming.MakeTerminal(naming.JoinAddressName(endpoint.String(), ""))
-	vlog.VI(0).Infof("Node manager name: %v", name)
-	// TODO(jsimsa): Replace <PreviousEnv> with a command-line flag when
-	// command-line flags in tests are supported.
-	dispatcher := impl.NewDispatcher(nil, envelope, name, os.Getenv(impl.PreviousEnv))
-	publishAs := "nm"
-	if err := server.Serve(publishAs, dispatcher); err != nil {
-		vlog.Fatalf("Serve(%v) failed: %v", publishAs, err)
-	}
-	fmt.Printf("ready\n")
-	return name, func() {
-		if err := server.Stop(); err != nil {
-			vlog.Fatalf("Stop() failed: %v", err)
-		}
-	}
-}
-
+// TestHelperProcess is blackbox boilerplate.
 func TestHelperProcess(t *testing.T) {
 	blackbox.HelperProcess(t)
 }
 
-// TestUpdate checks that the node manager Update() method works as
-// expected. To that end, this test spawns a new process that invokes
-// the nodeManager() method. The behavior of this method depends on
-// the value of the VEYRON_NM_TEST environment variable:
-//
-// 1) Initially, the value of VEYRON_NM_TEST is set to be "setup",
-// which prompts the nodeManager() method to setup a new workspace
-// that mimics the structure used for storing the node manager
-// binary. The method then sets VEYRON_NM_TEST to "parent" and
-// restarts itself using syscall.Exec(), effectively becoming the
-// process described next.
-//
-// 2) The "parent" branch sets up a mock application and binary
-// repository and a node manager that is pointed to the mock
-// application repository for updates. When all three services start,
-// the TestUpdate() method is notified and it proceeds to invoke
-// Update() on the node manager. This in turn results in the node
-// manager downloading an application envelope from the mock
-// application repository and a binary from the mock binary
-// repository. These are identical to the application envelope of the
-// "parent" node manager, except for the VEYRON_NM_TEST variable,
-// which is set to "child". The Update() method then spawns the child
-// node manager, checks that it is a valid node manager, and
-// terminates.
-//
-// 3) The "child" branch sets up a node manager and then calls back to
-// the "parent" node manager. This prompts the parent node manager to
-// invoke the Revert() method on the child node manager, which
-// terminates the child.
-func TestUpdate(t *testing.T) {
-	// Set up a mount table.
-	runtime := rt.Init()
-	mtName, mtCleanup := startMountTable(t)
-	defer mtCleanup()
-	ns := runtime.Namespace()
-	// The local, client-side Namespace is now relative to the
-	// MountTable server started above.
-	ns.SetRoots(mtName)
-	// Spawn a node manager with an identity blessed by the MountTable's
-	// identity under the name "test", and obtain its address.
-	//
-	// TODO(ataly): Eventually we want to use the same identity the node
-	// manager would have if it was running in production.
-	idFile := security.SaveIdentityToFile(security.NewBlessedIdentity(runtime.Identity(), "test"))
-	defer os.Remove(idFile)
-	child := spawnNodeManager(t, mtName, idFile)
-	defer child.Cleanup()
-	child.Expect("ready")
-	ctx, name := runtime.NewContext(), naming.Join(mtName, "nm")
-	results, err := ns.Resolve(ctx, name)
+func init() {
+	// All the tests and the subprocesses they start require a runtime; so just
+	// create it here.
+	rt.Init()
+
+	blackbox.CommandTable["nodeManager"] = nodeManager
+	blackbox.CommandTable["execScript"] = execScript
+}
+
+// execScript launches the script passed as argument.
+func execScript(args []string) {
+	if want, got := 1, len(args); want != got {
+		vlog.Fatalf("execScript expected %d arguments, got %d instead", want, got)
+	}
+	script := args[0]
+	env := []string{}
+	if os.Getenv("PAUSE_BEFORE_STOP") == "1" {
+		env = append(env, "PAUSE_BEFORE_STOP=1")
+	}
+	cmd := goexec.Cmd{
+		Path:   script,
+		Env:    env,
+		Stderr: os.Stderr,
+		Stdout: os.Stdout,
+	}
+	go func() {
+		stdin, err := cmd.StdinPipe()
+		if err != nil {
+			vlog.Fatalf("Failed to get stdin pipe: %v", err)
+		}
+		blackbox.WaitForEOFOnStdin()
+		stdin.Close()
+	}()
+	if err := cmd.Run(); err != nil {
+		vlog.Fatalf("Run cmd %v failed: %v", cmd, err)
+	}
+}
+
+// nodeManager sets up a node manager server.  It accepts the name to publish
+// the server under as an argument.  Additional arguments can optionally specify
+// node manager config settings.
+func nodeManager(args []string) {
+	if len(args) == 0 {
+		vlog.Fatalf("nodeManager expected at least an argument")
+	}
+	publishName := args[0]
+
+	defer fmt.Printf("%v terminating\n", publishName)
+	defer rt.R().Cleanup()
+	server, endpoint := newServer()
+	defer server.Stop()
+	name := naming.MakeTerminal(naming.JoinAddressName(endpoint, ""))
+	vlog.VI(1).Infof("Node manager name: %v", name)
+
+	// Satisfy the contract described in doc.go by passing the config state
+	// through to the node manager dispatcher constructor.
+	configState, err := config.Load()
 	if err != nil {
-		t.Fatalf("Resolve(%v) failed: %v", name, err)
+		vlog.Fatalf("Failed to decode config state: %v", err)
 	}
-	if expected, got := 1, len(results); expected != got {
-		t.Fatalf("Unexpected number of results: expected %d, got %d", expected, got)
+	configState.Name = name
+
+	// This exemplifies how to override or set specific config fields, if,
+	// for example, the node manager is invoked 'by hand' instead of via a
+	// script prepared by a previous version of the node manager.
+	if len(args) > 1 {
+		if want, got := 3, len(args)-1; want != got {
+			vlog.Fatalf("expected %d additional arguments, got %d instead", want, got)
+		}
+		configState.Root, configState.Origin, configState.CurrentLink = args[1], args[2], args[3]
 	}
-	// First invocation will cause app repository mock to return a bogus
-	// app title and hence the update should fail.
-	invokeUpdate(t, name, true)
-	invokeUpdate(t, name, false)
-	child.Expect("ready")
+
+	dispatcher, err := impl.NewDispatcher(nil, configState)
+	if err != nil {
+		vlog.Fatalf("Failed to create node manager dispatcher: %v", err)
+	}
+	if err := server.Serve(publishName, dispatcher); err != nil {
+		vlog.Fatalf("Serve(%v) failed: %v", publishName, err)
+	}
+
+	impl.InvokeCallback(name)
+
+	fmt.Println("ready")
+	<-signals.ShutdownOnSignals()
+	if os.Getenv("PAUSE_BEFORE_STOP") == "1" {
+		blackbox.WaitForEOFOnStdin()
+	}
+}
+
+// generateScript is very similar in behavior to its namesake in invoker.go.
+// However, we chose to re-implement it here for two reasons: (1) avoid making
+// generateScript public; and (2) how the test choses to invoke the node manager
+// subprocess the first time should be independent of how node manager
+// implementation sets up its updated versions.
+func generateScript(t *testing.T, root string, cmd *goexec.Cmd) string {
+	output := "#!/bin/bash\n"
+	output += strings.Join(config.QuoteEnv(cmd.Env), " ") + " "
+	output += cmd.Args[0] + " " + strings.Join(cmd.Args[1:], " ")
+	if err := os.MkdirAll(filepath.Join(root, "factory"), 0755); err != nil {
+		t.Fatalf("MkdirAll failed: %v", err)
+	}
+	// Why pigeons? To show that the name we choose for the initial script
+	// doesn't matter and in particular is independent of how node manager
+	// names its updated version scripts (noded.sh).
+	path := filepath.Join(root, "factory", "pigeons.sh")
+	if err := ioutil.WriteFile(path, []byte(output), 0755); err != nil {
+		t.Fatalf("WriteFile(%v) failed: %v", path, err)
+	}
+	return path
+}
+
+// envelopeFromCmd returns an envelope that describes the given command object.
+func envelopeFromCmd(cmd *goexec.Cmd) *application.Envelope {
+	return &application.Envelope{
+		Title:  application.NodeManagerTitle,
+		Args:   cmd.Args[1:],
+		Env:    cmd.Env,
+		Binary: "br",
+	}
+}
+
+// TestUpdateAndRevert makes the node manager go through the motions of updating
+// itself to newer versions (twice), and reverting itself back (twice).  It also
+// checks that update and revert fail when they're supposed to.  The initial
+// node manager is started 'by hand' via a blackbox command.  Further versions
+// are started through the soft link that the node manager itself updates.
+func TestUpdateAndRevert(t *testing.T) {
+	// Set up mount table, application, and binary repositories.
+	defer setupLocalNamespace(t)()
+	envelope, cleanup := startApplicationRepository()
+	defer cleanup()
+	defer startBinaryRepository()()
+
+	// This is the local filesystem location that the node manager is told
+	// to use.
+	root := filepath.Join(os.TempDir(), "nodemanager")
+	defer os.RemoveAll(root)
+
+	// Current link does not have to live in the root dir.
+	currLink := filepath.Join(os.TempDir(), "testcurrent")
+	defer os.Remove(currLink)
+
+	// Set up the initial version of the node manager, the so-called
+	// "factory" version.
+	nm := blackbox.HelperCommand(t, "nodeManager", "factoryNM", root, "ar", currLink)
+	defer setupChildCommand(nm)()
+
+	// This is the script that we'll point the current link to initially.
+	scriptPathFactory := generateScript(t, root, nm.Cmd)
+
+	if err := os.Symlink(scriptPathFactory, currLink); err != nil {
+		t.Fatalf("Symlink(%q, %q) failed: %v", scriptPathFactory, currLink, err)
+	}
+	// We instruct the initial node manager that we run to pause before
+	// stopping its service, so that we get a chance to verify that
+	// attempting an update while another one is ongoing will fail.
+	nm.Cmd.Env = exec.Setenv(nm.Cmd.Env, "PAUSE_BEFORE_STOP", "1")
+
+	resolveExpectError(t, "factoryNM", verror.NotFound) // Ensure a clean slate.
+
+	// Start the node manager -- we use the blackbox-generated command to
+	// start it.  We could have also used the scriptPathFactory to start it, but
+	// this demonstrates that the initial node manager could be started by
+	// hand as long as the right initial configuration is passed into the
+	// node manager implementation.
+	if err := nm.Cmd.Start(); err != nil {
+		t.Fatalf("Start() failed: %v", err)
+	}
+	deferrer := nm.Cleanup
+	defer func() {
+		if deferrer != nil {
+			deferrer()
+		}
+	}()
+	nm.Expect("ready")
+	resolve(t, "factoryNM") // Verify the node manager has published itself.
+
+	// Simulate an invalid envelope in the application repository.
+	*envelope = *envelopeFromCmd(nm.Cmd)
+	envelope.Title = "bogus"
+	updateExpectError(t, "factoryNM", verror.BadArg)   // Incorrect title.
+	revertExpectError(t, "factoryNM", verror.NotFound) // No previous version available.
+
+	// Set up a second version of the node manager.  We use the blackbox
+	// command solely to collect the args and env we need to provide the
+	// application repository with an envelope that will actually run the
+	// node manager subcommand.  The blackbox command is never started by
+	// hand -- instead, the information in the envelope will be used by the
+	// node manager to stage the next version.
+	nmV2 := blackbox.HelperCommand(t, "nodeManager", "v2NM")
+	defer setupChildCommand(nmV2)()
+	*envelope = *envelopeFromCmd(nmV2.Cmd)
+	update(t, "factoryNM")
+
+	// Current link should have been updated to point to v2.
+	evalLink := func() string {
+		path, err := filepath.EvalSymlinks(currLink)
+		if err != nil {
+			t.Fatalf("EvalSymlinks(%v) failed: %v", currLink, err)
+		}
+		return path
+	}
+	scriptPathV2 := evalLink()
+	if scriptPathFactory == scriptPathV2 {
+		t.Errorf("current link didn't change")
+	}
+
+	// This is from the child node manager started by the node manager
+	// as an update test.
+	nm.Expect("ready")
+	nm.Expect("v2NM terminating")
+
+	updateExpectError(t, "factoryNM", verror.Exists) // Update already in progress.
+
+	nm.CloseStdin()
+	nm.Expect("factoryNM terminating")
+	deferrer = nil
+	nm.Cleanup()
+
+	// A successful update means the node manager has stopped itself.  We
+	// relaunch it from the current link.
+	runNM := blackbox.HelperCommand(t, "execScript", currLink)
+	resolveExpectError(t, "v2NM", verror.NotFound) // Ensure a clean slate.
+	if err := runNM.Cmd.Start(); err != nil {
+		t.Fatalf("Start() failed: %v", err)
+	}
+	deferrer = runNM.Cleanup
+	runNM.Expect("ready")
+	resolve(t, "v2NM") // Current link should have been launching v2.
+
+	// Try issuing an update without changing the envelope in the application
+	// repository: this should fail, and current link should be unchanged.
+	updateExpectError(t, "v2NM", verror.NotFound)
+	if evalLink() != scriptPathV2 {
+		t.Errorf("script changed")
+	}
+
+	// Create a third version of the node manager and issue an update.
+	nmV3 := blackbox.HelperCommand(t, "nodeManager", "v3NM")
+	defer setupChildCommand(nmV3)()
+	*envelope = *envelopeFromCmd(nmV3.Cmd)
+	update(t, "v2NM")
+
+	scriptPathV3 := evalLink()
+	if scriptPathV3 == scriptPathV2 {
+		t.Errorf("current link didn't change")
+	}
+
+	// This is from the child node manager started by the node manager
+	// as an update test.
+	runNM.Expect("ready")
+	// Both the parent and child node manager should terminate upon successful
+	// update.
+	runNM.ExpectSet([]string{"v3NM terminating", "v2NM terminating"})
+
+	deferrer = nil
+	runNM.Cleanup()
+
+	// Re-lanuch the node manager from current link.
+	runNM = blackbox.HelperCommand(t, "execScript", currLink)
+	// We instruct the node manager to pause before stopping its server, so
+	// that we can verify that a second revert fails while a revert is in
+	// progress.
+	runNM.Cmd.Env = exec.Setenv(nm.Cmd.Env, "PAUSE_BEFORE_STOP", "1")
+	resolveExpectError(t, "v3NM", verror.NotFound) // Ensure a clean slate.
+	if err := runNM.Cmd.Start(); err != nil {
+		t.Fatalf("Start() failed: %v", err)
+	}
+	deferrer = runNM.Cleanup
+	runNM.Expect("ready")
+	resolve(t, "v3NM") // Current link should have been launching v3.
+
+	// Revert the node manager to its previous version (v2).
+	revert(t, "v3NM")
+	revertExpectError(t, "v3NM", verror.Exists) // Revert already in progress.
+	nm.CloseStdin()
+	runNM.Expect("v3NM terminating")
+	if evalLink() != scriptPathV2 {
+		t.Errorf("current link didn't change")
+	}
+	deferrer = nil
+	runNM.Cleanup()
+
+	// Re-launch the node manager from current link.
+	runNM = blackbox.HelperCommand(t, "execScript", currLink)
+	resolveExpectError(t, "v2NM", verror.NotFound) // Ensure a clean slate.
+	if err := runNM.Cmd.Start(); err != nil {
+		t.Fatalf("Start() failed: %v", err)
+	}
+	deferrer = runNM.Cleanup
+	runNM.Expect("ready")
+	resolve(t, "v2NM") // Current link should have been launching v2.
+
+	// Revert the node manager to its previous version (factory).
+	revert(t, "v2NM")
+	runNM.Expect("v2NM terminating")
+	if evalLink() != scriptPathFactory {
+		t.Errorf("current link didn't change")
+	}
+	deferrer = nil
+	runNM.Cleanup()
+
+	// Re-launch the node manager from current link.
+	runNM = blackbox.HelperCommand(t, "execScript", currLink)
+	resolveExpectError(t, "factoryNM", verror.NotFound) // Ensure a clean slate.
+	if err := runNM.Cmd.Start(); err != nil {
+		t.Fatalf("Start() failed: %v", err)
+	}
+	deferrer = runNM.Cleanup
+	runNM.Expect("ready")
+	resolve(t, "factoryNM") // Current link should have been launching factory version.
 }
diff --git a/services/mgmt/node/impl/invoker.go b/services/mgmt/node/impl/invoker.go
index ed450c6..b7b4611 100644
--- a/services/mgmt/node/impl/invoker.go
+++ b/services/mgmt/node/impl/invoker.go
@@ -1,11 +1,9 @@
 package impl
 
-// The implementation of the node manager expects that the node
-// manager installations are all organized in the following directory
-// structure:
+// The implementation of the node manager expects that the node manager
+// installations are all organized in the following directory structure:
 //
 // VEYRON_NM_ROOT/
-//   current # symlink to one of the workspaces
 //   workspace-1/
 //     noded - the node manager binary
 //     noded.sh - a shell script to start the binary
@@ -14,15 +12,14 @@
 //     noded - the node manager binary
 //     noded.sh - a shell script to start the binary
 //
-// The node manager is always expected to be started through
-// VEYRON_NM_ROOT/current/noded.sh, which is monitored by an init
-// daemon. This provides for simple and robust updates.
+// The node manager is always expected to be started through the symbolic link
+// passed in as config.CurrentLink, which is monitored by an init daemon. This
+// provides for simple and robust updates.
 //
-// To update the node manager to a newer version, a new workspace is
-// created and the "current" symlink is updated
-// accordingly. Similarly, to revert the node manager to a previous
-// version, all that is required is to update the symlink to point to
-// the previous workspace.
+// To update the node manager to a newer version, a new workspace is created and
+// the symlink is updated to the new noded.sh script. Similarly, to revert the
+// node manager to a previous version, all that is required is to update the
+// symlink to point to the previous noded.sh script.
 
 import (
 	"bytes"
@@ -43,6 +40,7 @@
 	"veyron/lib/config"
 	blib "veyron/services/mgmt/lib/binary"
 	vexec "veyron/services/mgmt/lib/exec"
+	iconfig "veyron/services/mgmt/node/config"
 	"veyron/services/mgmt/profile"
 
 	"veyron2/ipc"
@@ -58,24 +56,15 @@
 	"veyron2/vlog"
 )
 
-const CurrentWorkspace = "current"
-
-// state wraps state shared between different node manager
+// internalState wraps state shared between different node manager
 // invocations.
-type state struct {
+type internalState struct {
 	// channels maps callback identifiers to channels that are used to
 	// communicate information from child processes.
 	channels map[string]chan string
 	// channelsMutex is a lock for coordinating concurrent access to
 	// <channels>.
 	channelsMutex *sync.Mutex
-	// envelope is the node manager application envelope.
-	envelope *application.Envelope
-	// name is the node manager name.
-	name string
-	// previous holds the local path to the previous version of the node
-	// manager.
-	previous string
 	// updating is a flag that records whether this instance of node
 	// manager is being updated.
 	updating bool
@@ -86,7 +75,11 @@
 
 // invoker holds the state of a node manager invocation.
 type invoker struct {
-	state *state
+	// internal holds the node manager's internal state that persists across
+	// RPC requests.
+	internal *internalState
+	// config holds the node manager's (immutable) configuration state.
+	config *iconfig.State
 	// suffix is the suffix of the current invocation that is assumed to
 	// be used as a relative object name to identify an application,
 	// installation, or instance.
@@ -100,16 +93,9 @@
 	errOperationFailed    = verror.Internalf("operation failed")
 	errUpdateInProgress   = verror.Existsf("update in progress")
 	errIncompatibleUpdate = verror.BadArgf("update failed: mismatching app title")
+	errUpdateNoOp         = verror.NotFoundf("no different version available")
 )
 
-// NewInvoker is the invoker factory.
-func NewInvoker(state *state, suffix string) *invoker {
-	return &invoker{
-		state:  state,
-		suffix: suffix,
-	}
-}
-
 // NODE INTERFACE IMPLEMENTATION
 
 // computeNodeProfile generates a description of the runtime
@@ -290,7 +276,7 @@
 }
 
 func (i *invoker) Describe(call ipc.ServerContext) (node.Description, error) {
-	vlog.VI(0).Infof("%v.Describe()", i.suffix)
+	vlog.VI(1).Infof("%v.Describe()", i.suffix)
 	empty := node.Description{}
 	nodeProfile, err := i.computeNodeProfile()
 	if err != nil {
@@ -305,7 +291,7 @@
 }
 
 func (i *invoker) IsRunnable(call ipc.ServerContext, description binary.Description) (bool, error) {
-	vlog.VI(0).Infof("%v.IsRunnable(%v)", i.suffix, description)
+	vlog.VI(1).Infof("%v.IsRunnable(%v)", i.suffix, description)
 	nodeProfile, err := i.computeNodeProfile()
 	if err != nil {
 		return false, err
@@ -323,7 +309,7 @@
 }
 
 func (i *invoker) Reset(call ipc.ServerContext, deadline uint64) error {
-	vlog.VI(0).Infof("%v.Reset(%v)", i.suffix, deadline)
+	vlog.VI(1).Infof("%v.Reset(%v)", i.suffix, deadline)
 	// TODO(jsimsa): Implement.
 	return nil
 }
@@ -377,16 +363,14 @@
 
 // TODO(jsimsa): Replace <PreviousEnv> with a command-line flag when
 // command-line flags in tests are supported.
-func generateScript(workspace string, envelope *application.Envelope) error {
+func generateScript(workspace string, configSettings []string, envelope *application.Envelope) error {
 	path, err := filepath.EvalSymlinks(os.Args[0])
 	if err != nil {
 		vlog.Errorf("EvalSymlinks(%v) failed: %v", os.Args[0], err)
 		return errOperationFailed
 	}
 	output := "#!/bin/bash\n"
-	output += BinaryEnv + "=" + envelope.Binary + " "
-	output += PreviousEnv + "=" + filepath.Dir(path) + " "
-	output += strings.Join(envelope.Env, " ") + " "
+	output += strings.Join(iconfig.QuoteEnv(append(envelope.Env, configSettings...)), " ") + " "
 	output += filepath.Join(workspace, "noded") + " "
 	output += strings.Join(envelope.Args, " ")
 	path = filepath.Join(workspace, "noded.sh")
@@ -397,25 +381,25 @@
 	return nil
 }
 
-// getCurrentFileInfo returns the os.FileInfo for both the symbolic
-// link $VEYRON_NM_ROOT/current and the workspace this link points to.
-func getCurrentFileInfo() (os.FileInfo, os.FileInfo, error) {
-	path := filepath.Join(os.Getenv(RootEnv), CurrentWorkspace)
+// getCurrentFileInfo returns the os.FileInfo for both the symbolic link
+// CurrentLink, and the node script in the workspace that this link points to.
+func (i *invoker) getCurrentFileInfo() (os.FileInfo, string, error) {
+	path := i.config.CurrentLink
 	link, err := os.Lstat(path)
 	if err != nil {
 		vlog.Errorf("Lstat(%v) failed: %v", path, err)
-		return nil, nil, err
+		return nil, "", err
 	}
-	workspace, err := os.Stat(path)
+	scriptPath, err := filepath.EvalSymlinks(path)
 	if err != nil {
-		vlog.Errorf("Stat(%v) failed: %v", path, err)
-		return nil, nil, err
+		vlog.Errorf("EvalSymlinks(%v) failed: %v", path, err)
+		return nil, "", err
 	}
-	return link, workspace, nil
+	return link, scriptPath, nil
 }
 
-func updateLink(workspace string) error {
-	link := filepath.Join(os.Getenv(RootEnv), CurrentWorkspace)
+func (i *invoker) updateLink(newScript string) error {
+	link := i.config.CurrentLink
 	newLink := link + ".new"
 	fi, err := os.Lstat(newLink)
 	if err == nil {
@@ -424,8 +408,8 @@
 			return errOperationFailed
 		}
 	}
-	if err := os.Symlink(workspace, newLink); err != nil {
-		vlog.Errorf("Symlink(%v, %v) failed: %v", workspace, newLink, err)
+	if err := os.Symlink(newScript, newLink); err != nil {
+		vlog.Errorf("Symlink(%v, %v) failed: %v", newScript, newLink, err)
 		return errOperationFailed
 	}
 	if err := os.Rename(newLink, link); err != nil {
@@ -436,13 +420,13 @@
 }
 
 func (i *invoker) registerCallback(id string, channel chan string) {
-	i.state.channelsMutex.Lock()
-	defer i.state.channelsMutex.Unlock()
-	i.state.channels[id] = channel
+	i.internal.channelsMutex.Lock()
+	defer i.internal.channelsMutex.Unlock()
+	i.internal.channels[id] = channel
 }
 
 func (i *invoker) revertNodeManager() error {
-	if err := updateLink(i.state.previous); err != nil {
+	if err := i.updateLink(i.config.Previous); err != nil {
 		return err
 	}
 	rt.R().Stop()
@@ -457,7 +441,7 @@
 	// Setup up the child process callback.
 	id := fmt.Sprintf("%d", rand.Int())
 	cfg := config.New()
-	cfg.Set(mgmt.ParentNodeManagerConfigKey, naming.JoinAddressName(i.state.name, id))
+	cfg.Set(mgmt.ParentNodeManagerConfigKey, naming.MakeTerminal(naming.Join(i.config.Name, id)))
 	handle := vexec.NewParentHandle(cmd, vexec.ConfigOpt{cfg})
 	callbackChan := make(chan string)
 	i.registerCallback(id, callbackChan)
@@ -478,18 +462,18 @@
 	}
 	// Wait for the child process to invoke the Callback().
 	select {
-	case address := <-callbackChan:
+	case childName := <-callbackChan:
 		// Check that invoking Update() succeeds.
-		address = naming.JoinAddressName(address, "nm")
-		nmClient, err := node.BindNode(address)
+		childName = naming.MakeTerminal(naming.Join(childName, "nm"))
+		nmClient, err := node.BindNode(childName)
 		if err != nil {
-			vlog.Errorf("BindNode(%v) failed: %v", address, err)
+			vlog.Errorf("BindNode(%v) failed: %v", childName, err)
 			if err := handle.Clean(); err != nil {
 				vlog.Errorf("Clean() failed: %v", err)
 			}
 			return errOperationFailed
 		}
-		linkOld, workspaceOld, err := getCurrentFileInfo()
+		linkOld, pathOld, err := i.getCurrentFileInfo()
 		if err != nil {
 			if err := handle.Clean(); err != nil {
 				vlog.Errorf("Clean() failed: %v", err)
@@ -498,7 +482,7 @@
 		}
 		// Since the resolution of mtime for files is seconds,
 		// the test sleeps for a second to make sure it can
-		// check whether the "current" symlink is updated.
+		// check whether the current symlink is updated.
 		time.Sleep(time.Second)
 		if err := nmClient.Revert(rt.R().NewContext()); err != nil {
 			if err := handle.Clean(); err != nil {
@@ -506,23 +490,23 @@
 			}
 			return errOperationFailed
 		}
-		linkNew, workspaceNew, err := getCurrentFileInfo()
+		linkNew, pathNew, err := i.getCurrentFileInfo()
 		if err != nil {
 			if err := handle.Clean(); err != nil {
 				vlog.Errorf("Clean() failed: %v", err)
 			}
 			return errOperationFailed
 		}
-		// Check that the new node manager updated the symbolic link
-		// $VEYRON_NM_ROOT/current.
+		// Check that the new node manager updated the current symbolic
+		// link.
 		if !linkOld.ModTime().Before(linkNew.ModTime()) {
 			vlog.Errorf("new node manager test failed")
 			return errOperationFailed
 		}
-		// Check that the symbolic link $VEYRON_NM_ROOT/current points to
-		// the same workspace.
-		if workspaceOld.Name() != workspaceNew.Name() {
-			updateLink(workspaceOld.Name())
+		// Ensure that the current symbolic link points to the same
+		// script.
+		if pathNew != pathOld {
+			i.updateLink(pathOld)
 			vlog.Errorf("new node manager test failed")
 			return errOperationFailed
 		}
@@ -537,144 +521,162 @@
 }
 
 func (i *invoker) unregisterCallback(id string) {
-	i.state.channelsMutex.Lock()
-	defer i.state.channelsMutex.Unlock()
-	delete(i.state.channels, id)
+	i.internal.channelsMutex.Lock()
+	defer i.internal.channelsMutex.Unlock()
+	delete(i.internal.channels, id)
 }
 
 func (i *invoker) updateNodeManager() error {
-	envelope, err := fetchEnvelope(os.Getenv(OriginEnv))
+	if len(i.config.Origin) == 0 {
+		return errUpdateNoOp
+	}
+	envelope, err := fetchEnvelope(i.config.Origin)
 	if err != nil {
 		return err
 	}
 	if envelope.Title != application.NodeManagerTitle {
 		return errIncompatibleUpdate
 	}
-	if !reflect.DeepEqual(envelope, i.state.envelope) {
-		// Create new workspace.
-		workspace := filepath.Join(os.Getenv(RootEnv), fmt.Sprintf("%v", time.Now().Format(time.RFC3339Nano)))
-		perm := os.FileMode(0755)
-		if err := os.MkdirAll(workspace, perm); err != nil {
-			vlog.Errorf("MkdirAll(%v, %v) failed: %v", workspace, perm, err)
-			return errOperationFailed
-		}
-		// Populate the new workspace with a node manager binary.
-		if err := generateBinary(workspace, envelope, envelope.Binary != i.state.envelope.Binary); err != nil {
-			if err := os.RemoveAll(workspace); err != nil {
-				vlog.Errorf("RemoveAll(%v) failed: %v", workspace, err)
-			}
-			return err
-		}
-		// Populate the new workspace with a node manager script.
-		if err := generateScript(workspace, envelope); err != nil {
-			if err := os.RemoveAll(workspace); err != nil {
-				vlog.Errorf("RemoveAll(%v) failed: %v", workspace, err)
-			}
-			return err
-		}
-		if err := i.testNodeManager(workspace, envelope); err != nil {
-			if err := os.RemoveAll(workspace); err != nil {
-				vlog.Errorf("RemoveAll(%v) failed: %v", workspace, err)
-			}
-			return err
-		}
-		// If the binary has changed, update the node manager symlink.
-		if err := updateLink(workspace); err != nil {
-			if err := os.RemoveAll(workspace); err != nil {
-				vlog.Errorf("RemoveAll(%v) failed: %v", workspace, err)
-			}
-			return err
-		}
-		rt.R().Stop()
+	if i.config.Envelope != nil && reflect.DeepEqual(envelope, i.config.Envelope) {
+		return errUpdateNoOp
 	}
+	// Create new workspace.
+	workspace := filepath.Join(i.config.Root, fmt.Sprintf("%v", time.Now().Format(time.RFC3339Nano)))
+	perm := os.FileMode(0755)
+	if err := os.MkdirAll(workspace, perm); err != nil {
+		vlog.Errorf("MkdirAll(%v, %v) failed: %v", workspace, perm, err)
+		return errOperationFailed
+	}
+	// Populate the new workspace with a node manager binary.
+	// TODO(caprita): match identical binaries on binary metadata
+	// rather than binary object name.
+	sameBinary := i.config.Envelope != nil && envelope.Binary == i.config.Envelope.Binary
+	if err := generateBinary(workspace, envelope, !sameBinary); err != nil {
+		if err := os.RemoveAll(workspace); err != nil {
+			vlog.Errorf("RemoveAll(%v) failed: %v", workspace, err)
+		}
+		return err
+	}
+	// Populate the new workspace with a node manager script.
+	configSettings, err := i.config.Save(envelope)
+	if err != nil {
+		if err := os.RemoveAll(workspace); err != nil {
+			vlog.Errorf("RemoveAll(%v) failed: %v", workspace, err)
+		}
+		return errOperationFailed
+	}
+	if err := generateScript(workspace, configSettings, envelope); err != nil {
+		if err := os.RemoveAll(workspace); err != nil {
+			vlog.Errorf("RemoveAll(%v) failed: %v", workspace, err)
+		}
+		return err
+	}
+	if err := i.testNodeManager(workspace, envelope); err != nil {
+		if err := os.RemoveAll(workspace); err != nil {
+			vlog.Errorf("RemoveAll(%v) failed: %v", workspace, err)
+		}
+		return err
+	}
+	// If the binary has changed, update the node manager symlink.
+	if err := i.updateLink(filepath.Join(workspace, "noded.sh")); err != nil {
+		if err := os.RemoveAll(workspace); err != nil {
+			vlog.Errorf("RemoveAll(%v) failed: %v", workspace, err)
+		}
+		return err
+	}
+	rt.R().Stop()
 	return nil
 }
 
 func (i *invoker) Install(call ipc.ServerContext, von string) (string, error) {
-	vlog.VI(0).Infof("%v.Install(%q)", i.suffix, von)
+	vlog.VI(1).Infof("%v.Install(%q)", i.suffix, von)
 	// TODO(jsimsa): Implement.
 	return "", nil
 }
 
 func (i *invoker) Refresh(call ipc.ServerContext) error {
-	vlog.VI(0).Infof("%v.Refresh()", i.suffix)
+	vlog.VI(1).Infof("%v.Refresh()", i.suffix)
 	// TODO(jsimsa): Implement.
 	return nil
 }
 
 func (i *invoker) Restart(call ipc.ServerContext) error {
-	vlog.VI(0).Infof("%v.Restart()", i.suffix)
+	vlog.VI(1).Infof("%v.Restart()", i.suffix)
 	// TODO(jsimsa): Implement.
 	return nil
 }
 
 func (i *invoker) Resume(call ipc.ServerContext) error {
-	vlog.VI(0).Infof("%v.Resume()", i.suffix)
+	vlog.VI(1).Infof("%v.Resume()", i.suffix)
 	// TODO(jsimsa): Implement.
 	return nil
 }
 
 func (i *invoker) Revert(call ipc.ServerContext) error {
-	vlog.VI(0).Infof("%v.Revert()", i.suffix)
-	if i.state.previous == "" {
-		return errOperationFailed
+	vlog.VI(1).Infof("%v.Revert()", i.suffix)
+	if i.config.Previous == "" {
+		return errUpdateNoOp
 	}
-	i.state.updatingMutex.Lock()
-	if i.state.updating {
-		i.state.updatingMutex.Unlock()
+	i.internal.updatingMutex.Lock()
+	if i.internal.updating {
+		i.internal.updatingMutex.Unlock()
 		return errUpdateInProgress
 	} else {
-		i.state.updating = true
+		i.internal.updating = true
 	}
-	i.state.updatingMutex.Unlock()
+	i.internal.updatingMutex.Unlock()
 	err := i.revertNodeManager()
-	i.state.updatingMutex.Lock()
-	i.state.updating = false
-	i.state.updatingMutex.Unlock()
+	if err != nil {
+		i.internal.updatingMutex.Lock()
+		i.internal.updating = false
+		i.internal.updatingMutex.Unlock()
+	}
 	return err
 }
 
 func (i *invoker) Start(call ipc.ServerContext) ([]string, error) {
-	vlog.VI(0).Infof("%v.Start()", i.suffix)
+	vlog.VI(1).Infof("%v.Start()", i.suffix)
 	// TODO(jsimsa): Implement.
 	return make([]string, 0), nil
 }
 
 func (i *invoker) Stop(call ipc.ServerContext, deadline uint64) error {
-	vlog.VI(0).Infof("%v.Stop(%v)", i.suffix, deadline)
+	vlog.VI(1).Infof("%v.Stop(%v)", i.suffix, deadline)
 	// TODO(jsimsa): Implement.
 	return nil
 }
 
 func (i *invoker) Suspend(call ipc.ServerContext) error {
-	vlog.VI(0).Infof("%v.Suspend()", i.suffix)
+	vlog.VI(1).Infof("%v.Suspend()", i.suffix)
 	// TODO(jsimsa): Implement.
 	return nil
 }
 
 func (i *invoker) Uninstall(call ipc.ServerContext) error {
-	vlog.VI(0).Infof("%v.Uninstall()", i.suffix)
+	vlog.VI(1).Infof("%v.Uninstall()", i.suffix)
 	// TODO(jsimsa): Implement.
 	return nil
 }
 
 func (i *invoker) Update(call ipc.ServerContext) error {
-	vlog.VI(0).Infof("%v.Update()", i.suffix)
+	vlog.VI(1).Infof("%v.Update()", i.suffix)
 	switch {
 	case i.suffix == "nm":
 		// This branch attempts to update the node manager itself.
-		i.state.updatingMutex.Lock()
-		if i.state.updating {
-			i.state.updatingMutex.Unlock()
+		i.internal.updatingMutex.Lock()
+		if i.internal.updating {
+			i.internal.updatingMutex.Unlock()
 			return errUpdateInProgress
 		} else {
-			i.state.updating = true
+			i.internal.updating = true
 		}
-		i.state.updatingMutex.Unlock()
+		i.internal.updatingMutex.Unlock()
 		err := i.updateNodeManager()
-		i.state.updatingMutex.Lock()
-		i.state.updating = false
-		i.state.updatingMutex.Unlock()
+		if err != nil {
+			i.internal.updatingMutex.Lock()
+			i.internal.updating = false
+			i.internal.updatingMutex.Unlock()
+		}
 		return err
 	case appsSuffix.MatchString(i.suffix):
 		// TODO(jsimsa): Implement.
@@ -686,7 +688,7 @@
 }
 
 func (i *invoker) UpdateTo(call ipc.ServerContext, von string) error {
-	vlog.VI(0).Infof("%v.UpdateTo(%q)", i.suffix, von)
+	vlog.VI(1).Infof("%v.UpdateTo(%q)", i.suffix, von)
 	// TODO(jsimsa): Implement.
 	return nil
 }
@@ -694,15 +696,15 @@
 // CONFIG INTERFACE IMPLEMENTATION
 
 func (i *invoker) Set(_ ipc.ServerContext, key, value string) error {
-	vlog.VI(0).Infof("%v.Set(%v, %v)", i.suffix, key, value)
+	vlog.VI(1).Infof("%v.Set(%v, %v)", i.suffix, key, value)
 	// For now, only handle the child node manager name.  We'll add handling
 	// for the child's app cycle manager name later on.
 	if key != mgmt.ChildNodeManagerConfigKey {
 		return nil
 	}
-	i.state.channelsMutex.Lock()
-	channel, ok := i.state.channels[i.suffix]
-	i.state.channelsMutex.Unlock()
+	i.internal.channelsMutex.Lock()
+	channel, ok := i.internal.channels[i.suffix]
+	i.internal.channelsMutex.Unlock()
 	if !ok {
 		return errInvalidSuffix
 	}
diff --git a/services/mgmt/node/impl/mock_repo_test.go b/services/mgmt/node/impl/mock_repo_test.go
new file mode 100644
index 0000000..28eaf4a
--- /dev/null
+++ b/services/mgmt/node/impl/mock_repo_test.go
@@ -0,0 +1,130 @@
+package impl_test
+
+import (
+	"crypto/md5"
+	"encoding/hex"
+	"errors"
+	"io"
+	"io/ioutil"
+	"os"
+
+	"veyron2/ipc"
+	"veyron2/services/mgmt/application"
+	"veyron2/services/mgmt/binary"
+	"veyron2/services/mgmt/repository"
+	"veyron2/vlog"
+)
+
+// arInvoker holds the state of an application repository invocation mock.  The
+// mock returns the value of the wrapped envelope, which can be subsequently be
+// changed at any time.  Client is responsible for synchronization if desired.
+type arInvoker struct {
+	envelope application.Envelope
+}
+
+// startApplicationRepository sets up a server running the application
+// repository.  It returns a pointer to the envelope that the repository returns
+// to clients (so that it can be changed).  It also returns a cleanup function.
+func startApplicationRepository() (*application.Envelope, func()) {
+	server, _ := newServer()
+	invoker := new(arInvoker)
+	dispatcher := ipc.SoloDispatcher(repository.NewServerApplication(invoker), nil)
+	name := "ar"
+	if err := server.Serve(name, dispatcher); err != nil {
+		vlog.Fatalf("Serve(%v) failed: %v", name, err)
+	}
+	return &invoker.envelope, func() {
+		if err := server.Stop(); err != nil {
+			vlog.Fatalf("Stop() failed: %v", err)
+		}
+	}
+}
+
+// APPLICATION REPOSITORY INTERFACE IMPLEMENTATION
+func (i *arInvoker) Match(ipc.ServerContext, []string) (application.Envelope, error) {
+	vlog.VI(1).Infof("Match()")
+	return i.envelope, nil
+}
+
+// brInvoker holds the state of a binary repository invocation mock.  It always
+// serves the current running binary.
+type brInvoker struct{}
+
+// startBinaryRepository sets up a server running the binary repository and
+// returns a cleanup function.
+func startBinaryRepository() func() {
+	server, _ := newServer()
+	dispatcher := ipc.SoloDispatcher(repository.NewServerBinary(new(brInvoker)), nil)
+	name := "br"
+	if err := server.Serve(name, dispatcher); err != nil {
+		vlog.Fatalf("Serve(%q) failed: %v", name, err)
+	}
+	return func() {
+		if err := server.Stop(); err != nil {
+			vlog.Fatalf("Stop() failed: %v", err)
+		}
+	}
+}
+
+// BINARY REPOSITORY INTERFACE IMPLEMENTATION
+
+func (*brInvoker) Create(ipc.ServerContext, int32) error {
+	vlog.VI(1).Infof("Create()")
+	return nil
+}
+
+func (i *brInvoker) Delete(ipc.ServerContext) error {
+	vlog.VI(1).Infof("Delete()")
+	return nil
+}
+
+var errOperationFailed = errors.New("operation failed")
+
+func (i *brInvoker) Download(_ ipc.ServerContext, _ int32, stream repository.BinaryServiceDownloadStream) error {
+	vlog.VI(1).Infof("Download()")
+	file, err := os.Open(os.Args[0])
+	if err != nil {
+		vlog.Errorf("Open() failed: %v", err)
+		return errOperationFailed
+	}
+	defer file.Close()
+	bufferLength := 4096
+	buffer := make([]byte, bufferLength)
+	for {
+		n, err := file.Read(buffer)
+		switch err {
+		case io.EOF:
+			return nil
+		case nil:
+			if err := stream.Send(buffer[:n]); err != nil {
+				vlog.Errorf("Send() failed: %v", err)
+				return errOperationFailed
+			}
+		default:
+			vlog.Errorf("Read() failed: %v", err)
+			return errOperationFailed
+		}
+	}
+}
+
+func (*brInvoker) DownloadURL(ipc.ServerContext) (string, int64, error) {
+	vlog.VI(1).Infof("DownloadURL()")
+	return "", 0, nil
+}
+
+func (*brInvoker) Stat(ipc.ServerContext) ([]binary.PartInfo, error) {
+	vlog.VI(1).Infof("Stat()")
+	h := md5.New()
+	bytes, err := ioutil.ReadFile(os.Args[0])
+	if err != nil {
+		return []binary.PartInfo{}, errOperationFailed
+	}
+	h.Write(bytes)
+	part := binary.PartInfo{Checksum: hex.EncodeToString(h.Sum(nil)), Size: int64(len(bytes))}
+	return []binary.PartInfo{part}, nil
+}
+
+func (i *brInvoker) Upload(ipc.ServerContext, int32, repository.BinaryServiceUploadStream) error {
+	vlog.VI(1).Infof("Upload()")
+	return nil
+}
diff --git a/services/mgmt/node/impl/util_test.go b/services/mgmt/node/impl/util_test.go
new file mode 100644
index 0000000..3388b63
--- /dev/null
+++ b/services/mgmt/node/impl/util_test.go
@@ -0,0 +1,155 @@
+package impl_test
+
+import (
+	"fmt"
+	"os"
+	"testing"
+
+	mtlib "veyron/services/mounttable/lib"
+
+	"veyron/lib/testutil/blackbox"
+	"veyron/lib/testutil/security"
+	"veyron/services/mgmt/lib/exec"
+
+	"veyron2"
+	"veyron2/ipc"
+	"veyron2/naming"
+	"veyron2/rt"
+	"veyron2/services/mgmt/node"
+	"veyron2/verror"
+	"veyron2/vlog"
+)
+
+// TODO(caprita): I've had to write one too many of these, let's move it to some
+// central utility library.
+
+// setupLocalNamespace sets up a mounttable and sets the local namespace root
+// to point to it.  Returns a cleanup function.
+func setupLocalNamespace(t *testing.T) func() {
+	server, err := rt.R().NewServer(veyron2.ServesMountTableOpt(true))
+	if err != nil {
+		t.Fatalf("NewServer() failed: %v", err)
+	}
+	dispatcher, err := mtlib.NewMountTable("")
+	if err != nil {
+		t.Fatalf("NewMountTable() failed: %v", err)
+	}
+	protocol, hostname := "tcp", "localhost:0"
+	endpoint, err := server.Listen(protocol, hostname)
+	if err != nil {
+		t.Fatalf("Listen(%v, %v) failed: %v", protocol, hostname, err)
+	}
+	if err := server.Serve("", dispatcher); err != nil {
+		t.Fatalf("Serve(%v) failed: %v", dispatcher, err)
+	}
+	name := naming.JoinAddressName(endpoint.String(), "")
+	vlog.VI(1).Infof("Mount table object name: %v", name)
+	ns := rt.R().Namespace()
+	// Make the runtime's namespace rooted at the MountTable server started
+	// above.
+	ns.SetRoots(name)
+	return func() {
+		if err := server.Stop(); err != nil {
+			t.Fatalf("Stop() failed: %v", err)
+		}
+		// The runtime outlives the particular test case that invokes
+		// setupLocalNamespace. It's good practice to reset the
+		// runtime's state before the next test uses it.
+		ns.SetRoots()
+	}
+}
+
+// TODO(caprita): Move this setup into the blackbox lib.
+
+// setupChildCommand configures the child to use the right mounttable root
+// and identity.  It returns a cleanup function.
+func setupChildCommand(child *blackbox.Child) func() {
+	cmd := child.Cmd
+	for i, root := range rt.R().Namespace().Roots() {
+		cmd.Env = exec.Setenv(cmd.Env, fmt.Sprintf("NAMESPACE_ROOT%d", i), root)
+	}
+	idFile := security.SaveIdentityToFile(security.NewBlessedIdentity(rt.R().Identity(), "test"))
+	cmd.Env = exec.Setenv(cmd.Env, "VEYRON_IDENTITY", idFile)
+	return func() {
+		os.Remove(idFile)
+	}
+}
+
+func newServer() (ipc.Server, string) {
+	server, err := rt.R().NewServer()
+	if err != nil {
+		vlog.Fatalf("NewServer() failed: %v", err)
+	}
+	protocol, hostname := "tcp", "localhost:0"
+	endpoint, err := server.Listen(protocol, hostname)
+	if err != nil {
+		vlog.Fatalf("Listen(%v, %v) failed: %v", protocol, hostname, err)
+	}
+	return server, endpoint.String()
+}
+
+// resolveExpectError verifies that the given name is not in the mounttable.
+func resolveExpectError(t *testing.T, name string, errID verror.ID) {
+	if results, err := rt.R().Namespace().Resolve(rt.R().NewContext(), name); err == nil {
+		t.Errorf("Resolve(%v) succeeded with results %v when it was expected to fail", name, results)
+	} else if !verror.Is(err, errID) {
+		t.Errorf("Resolve(%v) failed with error %v, expected error ID %v", err, errID)
+	}
+}
+
+// resolve looks up the given name in the mounttable.
+func resolve(t *testing.T, name string) string {
+	results, err := rt.R().Namespace().Resolve(rt.R().NewContext(), name)
+	if err != nil {
+		t.Fatalf("Resolve(%v) failed: %v", name, err)
+	}
+	if want, got := 1, len(results); want != got {
+		t.Fatalf("Resolve(%v) expected %d result(s), got %d instead", name, want, got)
+	}
+	return results[0]
+}
+
+// The following set of functions are convenience wrappers around Update and
+// Revert.
+
+func updateExpectError(t *testing.T, name string, errID verror.ID) {
+	if err := invokeUpdate(t, name); !verror.Is(err, errID) {
+		t.Errorf("Unexpected update error %v, expected error ID %v", err, errID)
+	}
+}
+
+func update(t *testing.T, name string) {
+	if err := invokeUpdate(t, name); err != nil {
+		t.Errorf("Update() failed: %v", err)
+	}
+}
+
+func invokeUpdate(t *testing.T, name string) error {
+	address := naming.Join(name, "nm")
+	stub, err := node.BindNode(address)
+	if err != nil {
+		t.Fatalf("BindNode(%v) failed: %v", address, err)
+	}
+	return stub.Update(rt.R().NewContext())
+}
+
+func revertExpectError(t *testing.T, name string, errID verror.ID) {
+	if err := invokeRevert(t, name); !verror.Is(err, errID) {
+		t.Errorf("Unexpected revert error %v, expected error ID %v", err, errID)
+	}
+}
+
+func revert(t *testing.T, name string) {
+	if err := invokeRevert(t, name); err != nil {
+		t.Errorf("Revert() failed: %v", err)
+	}
+}
+
+func invokeRevert(t *testing.T, name string) error {
+	address := naming.Join(name, "nm")
+	stub, err := node.BindNode(address)
+	if err != nil {
+		t.Fatalf("BindNode(%v) failed: %v", address, err)
+	}
+	return stub.Revert(rt.R().NewContext())
+}
diff --git a/services/mgmt/node/noded/main.go b/services/mgmt/node/noded/main.go
index 9deb603..1fe7ff4 100644
--- a/services/mgmt/node/noded/main.go
+++ b/services/mgmt/node/noded/main.go
@@ -2,30 +2,17 @@
 
 import (
 	"flag"
-	"os"
 
 	"veyron/lib/signals"
 	vflag "veyron/security/flag"
-	"veyron/services/mgmt/lib/exec"
-	"veyron/services/mgmt/node"
+	"veyron/services/mgmt/node/config"
 	"veyron/services/mgmt/node/impl"
 
-	"veyron2/mgmt"
 	"veyron2/naming"
 	"veyron2/rt"
-	"veyron2/services/mgmt/application"
 	"veyron2/vlog"
 )
 
-func generateEnvelope() *application.Envelope {
-	return &application.Envelope{
-		Args:   os.Args,
-		Binary: os.Getenv(impl.BinaryEnv),
-		Env:    os.Environ(),
-		Title:  application.NodeManagerTitle,
-	}
-}
-
 func main() {
 	// TODO(rthellend): Remove the address and protocol flags when the config manager is working.
 	var address, protocol, publishAs string
@@ -33,9 +20,6 @@
 	flag.StringVar(&protocol, "protocol", "tcp", "network type to listen on")
 	flag.StringVar(&publishAs, "name", "", "name to publish the node manager at")
 	flag.Parse()
-	if os.Getenv(impl.OriginEnv) == "" {
-		vlog.Fatalf("Specify the node manager origin as environment variable %s=<name>", impl.OriginEnv)
-	}
 	runtime := rt.Init()
 	defer runtime.Cleanup()
 	server, err := runtime.NewServer()
@@ -47,29 +31,26 @@
 	if err != nil {
 		vlog.Fatalf("Listen(%v, %v) failed: %v", protocol, address, err)
 	}
-	suffix, envelope := "", generateEnvelope()
-	name := naming.MakeTerminal(naming.JoinAddressName(endpoint.String(), suffix))
-	vlog.VI(0).Infof("Node manager name: %v", name)
-	// TODO(jsimsa): Replace <PreviousEnv> with a command-line flag when
-	// command-line flags are supported in tests.
-	dispatcher := impl.NewDispatcher(vflag.NewAuthorizerOrDie(), envelope, name, os.Getenv(impl.PreviousEnv))
+	name := naming.MakeTerminal(naming.JoinAddressName(endpoint.String(), ""))
+	vlog.VI(0).Infof("Node manager object name: %v", name)
+	configState, err := config.Load()
+	if err != nil {
+		vlog.Fatalf("Failed to load config passed from parent: %v", err)
+		return
+	}
+	configState.Name = name
+	// TODO(caprita): We need a way to set config fields outside of the
+	// update mechanism (since that should ideally be an opaque
+	// implementation detail).
+	dispatcher, err := impl.NewDispatcher(vflag.NewAuthorizerOrDie(), configState)
+	if err != nil {
+		vlog.Fatalf("Failed to create dispatcher: %v", err)
+	}
 	if err := server.Serve(publishAs, dispatcher); err != nil {
 		vlog.Fatalf("Serve(%v) failed: %v", publishAs, err)
 	}
-	handle, _ := exec.GetChildHandle()
-	if handle != nil {
-		callbackName, err := handle.Config.Get(mgmt.ParentNodeManagerConfigKey)
-		if err != nil {
-			vlog.Fatalf("Couldn't get callback name from config: %v", err)
-		}
-		nmClient, err := node.BindConfig(callbackName)
-		if err != nil {
-			vlog.Fatalf("BindNode(%v) failed: %v", callbackName, err)
-		}
-		if err := nmClient.Set(rt.R().NewContext(), mgmt.ChildNodeManagerConfigKey, name); err != nil {
-			vlog.Fatalf("Callback(%v, %v) failed: %v", mgmt.ChildNodeManagerConfigKey, name, err)
-		}
-	}
+	impl.InvokeCallback(name)
+
 	// Wait until shutdown.
 	<-signals.ShutdownOnSignals()
 }
diff --git a/services/store/memstore/query/eval.go b/services/store/memstore/query/eval.go
index 73fa455..783fbd8 100644
--- a/services/store/memstore/query/eval.go
+++ b/services/store/memstore/query/eval.go
@@ -501,7 +501,6 @@
 		pos:          p.Pos,
 	}
 	for i, a := range p.SubPipelines {
-		// TODO(kash): Protect against aliases that have slashes in them?
 		e.subpipelines[i] = alias{convertPipeline(a.Pipeline), a.Alias, a.Hidden}
 	}
 	return e
@@ -580,10 +579,9 @@
 			case <-c.abort:
 				return false
 			case sub, ok := <-out:
-				if !ok {
-					return false
+				if ok {
+					value = sub.Value
 				}
-				value = sub.Value
 			}
 		} else {
 			value = out
diff --git a/services/store/memstore/query/eval_test.go b/services/store/memstore/query/eval_test.go
index 6367a9f..65359b2 100644
--- a/services/store/memstore/query/eval_test.go
+++ b/services/store/memstore/query/eval_test.go
@@ -40,6 +40,9 @@
 	bettyID := put(t, sn, "/players/betty", player{"betty", 23})
 	bobID := put(t, sn, "/players/bob", player{"bob", 21})
 
+	put(t, sn, "/players/betty/bio", "")
+	put(t, sn, "/players/betty/bio/hometown", "Tampa")
+
 	put(t, sn, "/teams", "")
 	put(t, sn, "/teams/cardinals", team{"cardinals", "CA"})
 	put(t, sn, "/teams/sharks", team{"sharks", "NY"})
@@ -434,6 +437,31 @@
 				},
 			},
 		},
+		// Test for selection of a nested name ('bio/hometown').  Only betty has this
+		// nested name, so other players should get a nil value.
+		{
+			"", "'players/*' | type player | {Name, 'bio/hometown'} | ? Name == 'alfred' || Name == 'betty' | sort()",
+			[]*store.QueryResult{
+				&store.QueryResult{
+					0,
+					"players/alfred",
+					map[string]vdlutil.Any{
+						"Name":         "alfred",
+						"bio/hometown": nil,
+					},
+					nil,
+				},
+				&store.QueryResult{
+					0,
+					"players/betty",
+					map[string]vdlutil.Any{
+						"Name":         "betty",
+						"bio/hometown": "Tampa",
+					},
+					nil,
+				},
+			},
+		},
 	}
 	for _, test := range tests {
 		vlog.VI(1).Infof("Testing %s\n", test.query)