Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
87 changes: 87 additions & 0 deletions internal/daemon/managed_token.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
package daemon

import (
"crypto/rand"
"encoding/hex"
"errors"
"io"
"os"
"path/filepath"
)

// AgentTokenName is persistent authentication, not per-instance management state.
const AgentTokenName = "agent.token"
const agentStageName = ".agent-token-stage"

type managedTokenError struct{ cause error }

func (e *managedTokenError) Error() string {
return "cannot initialize protected MCP token; inspect runtime storage or use --token-file"
}
func (e *managedTokenError) Unwrap() error { return e.cause }

// EnsureAgentToken reuses a protected token or exclusively publishes 256 random
// bits. It never replaces existing entries or repairs privacy. Runtime parents
// must be trusted; unsupported hard links and concurrent initialization fail closed.
func EnsureAgentToken(directory string) (string, error) {
return ensureAgentToken(directory, rand.Reader, fileOps{})
}

func ensureAgentToken(directory string, random io.Reader, ops fileOps) (token string, err error) {
defer func() {
if err != nil {
token, err = "", &managedTokenError{err}
}
}()
store, err := OpenStore(directory)
if err != nil {
return "", err
}
defer func() { err = errors.Join(err, store.Close()) }()
path := filepath.Join(directory, AgentTokenName)
if token, err = ReadAgentToken(path); err == nil || !errors.Is(err, os.ErrNotExist) {
return token, err
}
lease, err := store.Acquire()
if err != nil {
return "", err
}
defer func() { err = errors.Join(err, lease.Close()) }()
// Another cooperative initializer may have completed before acquiring the lease.
if token, err = ReadAgentToken(path); err == nil || !errors.Is(err, os.ErrNotExist) {
return token, err
}
var entropy [32]byte
if random == nil {
return "", errors.New("random source unavailable")
}
if _, err := io.ReadFull(random, entropy[:]); err != nil {
return "", err
}
token = hex.EncodeToString(entropy[:])
stage, err := createOwned(store, agentStageName, []byte(token), ops)
if stage != nil {
defer func() {
for _, name := range stage.names {
if name == agentStageName {
err = errors.Join(err, stage.remove(name))
}
}
err = errors.Join(err, stage.file.Close())
}()
}
if err != nil {
return "", err
}
if err := stage.verify(agentStageName); err != nil {
return "", err
}
if err := store.root.Link(agentStageName, AgentTokenName); err != nil {
return "", err
}
stage.names = append(stage.names, AgentTokenName)
if err := stage.remove(agentStageName); err != nil {
return "", err
}
return token, stage.verify(AgentTokenName)
}
140 changes: 140 additions & 0 deletions internal/daemon/managed_token_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
//go:build linux || darwin || windows

package daemon

import (
"bytes"
"encoding/hex"
"errors"
"io"
"os"
"path/filepath"
"strings"
"sync"
"testing"
)

func TestManagedTokenPersistsAndIsNotInstanceState(t *testing.T) {
dir := filepath.Join(t.TempDir(), "runtime")
token, err := EnsureAgentToken(dir)
data, decodeErr := hex.DecodeString(token)
if err != nil || decodeErr != nil || len(data) != 32 {
t.Fatal("invalid generated token", err)
}
store, err := OpenExistingStore(dir)
if err != nil {
t.Fatal(err)
}
defer store.Close()
lease, err := store.Acquire()
if err != nil {
t.Fatal(err)
}
defer lease.Close()
// Reuse is allowed even while the background instance holds its lease.
if got, err := ensureAgentToken(dir, nil, fileOps{}); err != nil || got != token {
t.Fatal("persisted token was not reused", err)
}
if _, _, err := Read(store); err != ErrAbsent {
t.Fatal("agent token became background instance state", err)
}
if got, err := ReadAgentToken(filepath.Join(dir, AgentTokenName)); err != nil || got != token {
t.Fatal("saved token is not owner-private", err)
}
}

func TestManagedTokenFailuresDoNotOverwriteOrLeak(t *testing.T) {
for _, kind := range []string{"invalid", "stage", "locked", "entropy", "write", "sync", "close", "collision"} {
t.Run(kind, func(t *testing.T) {
dir := filepath.Join(t.TempDir(), "runtime")
store, err := OpenStore(dir)
if err != nil {
t.Fatal(err)
}
defer store.Close()
var ops fileOps
random := io.Reader(bytes.NewReader(bytes.Repeat([]byte{1}, 32)))
name := ""
switch kind {
case "invalid", "stage":
name = AgentTokenName
if kind == "stage" {
name = agentStageName
}
file, err := privateCreate(store.root, name)
if err != nil {
t.Fatal(err)
}
_, _ = file.WriteString("private invalid")
_ = file.Close()
case "locked":
lease, err := store.Acquire()
if err != nil {
t.Fatal(err)
}
defer lease.Close()
case "entropy":
random = strings.NewReader("short")
case "write":
ops.write = func(*os.File, []byte) (int, error) { return 0, nil }
case "sync":
ops.sync = func(*os.File) error { return errors.New("private sync failure") }
case "close":
ops.close = func(*os.File) error { return errors.New("private close failure") }
case "collision":
name = AgentTokenName
ops.sync = func(*os.File) error {
file, err := privateCreate(store.root, name)
if err != nil {
return err
}
_, err = file.WriteString("private invalid")
return errors.Join(err, file.Close())
}
}
if token, err := ensureAgentToken(dir, random, ops); err == nil || token != "" || strings.Contains(err.Error(), "private") || strings.Contains(err.Error(), dir) {
t.Fatal("failure accepted or exposed private input", err)
}
if name == "" && kind != "locked" {
entries, _ := os.ReadDir(dir)
if len(entries) != 0 {
t.Fatal("failed generation left token, staging or lock entries")
}
}
if name != "" {
data, _ := store.root.ReadFile(name)
if string(data) != "private invalid" {
t.Fatal("existing entry changed")
}
}
})
}
}

func TestManagedTokenConcurrentInitializationNeverDisagrees(t *testing.T) {
dir := filepath.Join(t.TempDir(), "runtime")
var group sync.WaitGroup
tokens := make(chan string, 8)
for range 8 {
group.Go(func() {
if token, err := EnsureAgentToken(dir); err == nil {
tokens <- token
}
})
}
group.Wait()
close(tokens)
want, err := EnsureAgentToken(dir)
if err != nil || len(tokens) == 0 {
t.Fatal("concurrent initialization did not publish a token", err)
}
for token := range tokens {
if token != want {
t.Fatal("concurrent initializers disagreed")
}
}
entries, _ := os.ReadDir(dir)
if len(entries) != 1 || entries[0].Name() != AgentTokenName {
t.Fatal("initializer left staging or lock entries")
}
}
46 changes: 46 additions & 0 deletions internal/daemon/managed_token_unix_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
//go:build linux || darwin

package daemon

import (
"os"
"path/filepath"
"testing"
)

func TestManagedTokenRefusesUnsafeExistingEntries(t *testing.T) {
for _, kind := range []string{"public-directory", "public-file", "symlink", "hardlink"} {
t.Run(kind, func(t *testing.T) {
dir := filepath.Join(t.TempDir(), "runtime")
if _, err := EnsureAgentToken(dir); err != nil {
t.Fatal(err)
}
path := filepath.Join(dir, AgentTokenName)
switch kind {
case "public-directory":
_ = os.Chmod(dir, 0755)
case "public-file":
_ = os.Chmod(path, 0644)
case "symlink":
if err := os.Rename(path, path+"-original"); err != nil {
t.Fatal(err)
}
if err := os.Symlink(path+"-original", path); err != nil {
t.Fatal(err)
}
case "hardlink":
if err := os.Link(path, path+"-extra"); err != nil {
t.Fatal(err)
}
}
before, _ := os.Lstat(path)
if token, err := EnsureAgentToken(dir); err == nil || token != "" {
t.Fatal("unsafe entry accepted")
}
after, _ := os.Lstat(path)
if !os.SameFile(before, after) || before.Mode() != after.Mode() {
t.Fatal("unsafe entry repaired or replaced")
}
})
}
}
Loading