The module is one Go binary the runtime launches: the renderer (its instruction file held byte for byte to the TypeScript one it replaces), the credentials and identity files, the licence flow of ADR 0206 and the MCP servers in state. Keeps the TypeScript module's key files, so a node moving to it keeps its key. The npm package, its tests and its build go. Both binaries were run together under the real runtime on a test bus with postgres and a stub vendor: a login was adopted by one exchange, the node bound and handed an access token, its file left with no refresh token, and no token in either state.
530 lines
18 KiB
Go
530 lines
18 KiB
Go
package main
|
|
|
|
// What claude-code does on a node, written against what it is handed — a way to ask a tool on the bus, its
|
|
// own state, a way to write a managed file — so every path is tested without a bus (novox/hq design 36,
|
|
// ADR 0183, ADR 0201, ADR 0206).
|
|
//
|
|
// Over NATS, and nothing an event: what is current is state, and a secret only ever travels on a request,
|
|
// sealed to its one recipient.
|
|
// - What this node holds is the module's `holdings` state, one key per node: the account, the kind,
|
|
// fingerprints and expiries — never a token. Written at start and on every change of the credentials
|
|
// file, so the licence manager learns a login, or a node already logged in, from the state alone.
|
|
// - The grant itself leaves only when the manager asks `claude_code_grant`, sealed to the key it gives.
|
|
// - What this node should hold is the manager's `bindings` state; a newer generation for this node is
|
|
// fetched with the seat's `current` verb, sealed to this module's key, and written access-token-only.
|
|
// - An MCP server registered through this module is a key in its `servers` state — `all.<server>` for
|
|
// every node, `<node>.<server>` for one — which every node watches.
|
|
|
|
import (
|
|
"crypto/sha256"
|
|
"encoding/hex"
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"os"
|
|
"path/filepath"
|
|
"sort"
|
|
"strings"
|
|
"sync"
|
|
"time"
|
|
)
|
|
|
|
// Seat is the licence manager's role, and Manager the module whose `bindings` state this one reads.
|
|
const (
|
|
Seat = "anthropic-licence-manager"
|
|
Manager = "claude-licence-manager"
|
|
)
|
|
|
|
// SeatVerb is a seat's verb as the runtime addresses it: a role, not a module.
|
|
func SeatVerb(verb string) string { return "seat:" + Seat + "." + verb }
|
|
|
|
// Fingerprint names a token without being one: the first 16 hex of its SHA-256, as the manager computes it.
|
|
func Fingerprint(s string) string {
|
|
sum := sha256.Sum256([]byte(s))
|
|
return "sha256:" + hex.EncodeToString(sum[:])[:16]
|
|
}
|
|
|
|
// Paths are where this node's files are, from the module's words (ADR 0192).
|
|
type Paths struct {
|
|
State, Facts, Settings, Home, Node string
|
|
}
|
|
|
|
// PathsFrom reads them, or answers false outside a launch.
|
|
func PathsFrom(env func(string) string) (Paths, bool) {
|
|
p := Paths{State: env("MESH_CLAUDE_CODE_STATE"), Facts: env("MESH_CLAUDE_CODE_FACTS"),
|
|
Settings: env("MESH_CLAUDE_CODE_SETTINGS"), Home: env("MESH_OPERATOR_HOME"), Node: env("MESH_NODE")}
|
|
return p, p.State != "" && p.Facts != "" && p.Settings != "" && p.Home != "" && p.Node != ""
|
|
}
|
|
|
|
func (p Paths) credentials() string { return filepath.Join(p.Home, ".claude", ".credentials.json") }
|
|
func (p Paths) account() string { return filepath.Join(p.Home, ".claude.json") }
|
|
func (p Paths) binding() string { return filepath.Join(p.State, "licence.json") }
|
|
func (p Paths) apiKey() string { return filepath.Join(p.State, "api-key") }
|
|
func (p Paths) helper() string { return filepath.Join(p.State, "api-key-helper") }
|
|
func (p Paths) registry() string { return filepath.Join(p.State, "mcp-servers.json") }
|
|
|
|
// Ask is a tool on the bus: its address and arguments in, its JSON answer out.
|
|
type Ask func(address string, args any) (json.RawMessage, error)
|
|
|
|
// WriteManaged writes one managed file and answers what happened.
|
|
type WriteManaged func(name, content string) (string, error)
|
|
|
|
func readJSON(path string, into any) bool {
|
|
raw, err := os.ReadFile(path)
|
|
return err == nil && json.Unmarshal(raw, into) == nil
|
|
}
|
|
|
|
// Keypair is this module's own, made once in its state; the TypeScript module's files are kept, so a node
|
|
// moving to this binary keeps the key it had.
|
|
func Keypair(p Paths) (KeyPair, error) {
|
|
priv, pub := filepath.Join(p.State, "key.pem"), filepath.Join(p.State, "key.pub.pem")
|
|
if _, err := os.Stat(priv); errors.Is(err, os.ErrNotExist) {
|
|
k, err := GenerateKeyPair()
|
|
if err != nil {
|
|
return KeyPair{}, err
|
|
}
|
|
if err := os.WriteFile(priv, []byte(k.PrivateKey), 0o600); err != nil {
|
|
return KeyPair{}, err
|
|
}
|
|
if err := os.WriteFile(pub, []byte(k.PublicKey), 0o644); err != nil {
|
|
return KeyPair{}, err
|
|
}
|
|
}
|
|
a, err1 := os.ReadFile(priv)
|
|
b, err2 := os.ReadFile(pub)
|
|
return KeyPair{PrivateKey: string(a), PublicKey: string(b)}, errors.Join(err1, err2)
|
|
}
|
|
|
|
// Registered is what applies here of the servers registered through this module.
|
|
func Registered(p Paths) Servers {
|
|
s := Servers{}
|
|
readJSON(p.registry(), &s)
|
|
return s
|
|
}
|
|
|
|
// RenderNow writes the managed directory from the facts, the settings, the licence held and the servers
|
|
// registered here.
|
|
func RenderNow(p Paths, write WriteManaged) ([]string, error) {
|
|
var facts Facts
|
|
if !readJSON(p.Facts, &facts) || facts.Console == "" {
|
|
return nil, fmt.Errorf("the mesh has not rendered %s yet; nothing to write", p.Facts)
|
|
}
|
|
var settings Settings
|
|
readJSON(p.Settings, &settings)
|
|
var binding *Binding
|
|
var b Binding
|
|
if readJSON(p.binding(), &b) {
|
|
binding = &b
|
|
}
|
|
files := Render(facts, settings, binding, p.helper(), Registered(p))
|
|
names := make([]string, 0, len(files))
|
|
for n := range files {
|
|
names = append(names, n)
|
|
}
|
|
sort.Strings(names)
|
|
var out []string
|
|
for _, n := range names {
|
|
line, err := write(n, files[n])
|
|
if err != nil {
|
|
return out, err
|
|
}
|
|
out = append(out, line)
|
|
}
|
|
return out, nil
|
|
}
|
|
|
|
// ---- the licence ----------------------------------------------------------------------------------
|
|
|
|
// BindingState is what the manager's `bindings` state says one consumer should hold (ADR 0206).
|
|
type BindingState struct {
|
|
Licence string `json:"licence"`
|
|
Kind string `json:"kind"`
|
|
Generation int64 `json:"generation"`
|
|
}
|
|
|
|
// Current is what the seat answers to `current`: the licence this node is bound to and its token, sealed.
|
|
type Current struct {
|
|
Licence string `json:"licence"`
|
|
Kind string `json:"kind"`
|
|
Generation int64 `json:"generation"`
|
|
Sealed *SealedBox `json:"sealed"`
|
|
Identity *Identity `json:"identity"`
|
|
}
|
|
|
|
// Holdings is what this node holds, as the `holdings` state carries it (ADR 0206): enough for the manager
|
|
// to tell a login it has not adopted from one it has, and never a token — fingerprints and expiries only.
|
|
type Holdings struct {
|
|
Node string `json:"node"`
|
|
Identity *Identity `json:"identity"`
|
|
Kind *string `json:"kind"`
|
|
Refresh struct {
|
|
Present bool `json:"present"`
|
|
Fingerprint *string `json:"fingerprint"`
|
|
ExpiresAt *int64 `json:"expiresAt"`
|
|
} `json:"refresh"`
|
|
Access *struct {
|
|
Fingerprint string `json:"fingerprint"`
|
|
ExpiresAt int64 `json:"expiresAt"`
|
|
} `json:"access"`
|
|
Licence *string `json:"licence"`
|
|
Generation int64 `json:"generation"`
|
|
ChangedAt *string `json:"changedAt"`
|
|
}
|
|
|
|
// HoldingsOf is what this node holds now.
|
|
func HoldingsOf(p Paths) Holdings {
|
|
h := Holdings{Node: p.Node, Identity: ReadIdentity(p.account())}
|
|
creds := ReadCredentials(p.credentials())
|
|
if info, err := os.Stat(p.credentials()); err == nil {
|
|
at := info.ModTime().UTC().Format("2006-01-02T15:04:05.000Z")
|
|
h.ChangedAt = &at
|
|
}
|
|
if rt := RefreshTokenOf(creds); rt != "" {
|
|
fp := Fingerprint(rt)
|
|
h.Refresh.Present, h.Refresh.Fingerprint = true, &fp
|
|
}
|
|
if v, ok := number(creds.oauth()["refreshTokenExpiresAt"]); ok {
|
|
h.Refresh.ExpiresAt = &v
|
|
}
|
|
if g := GrantOf(creds); g != nil {
|
|
h.Access = &struct {
|
|
Fingerprint string `json:"fingerprint"`
|
|
ExpiresAt int64 `json:"expiresAt"`
|
|
}{Fingerprint(g.AccessToken), g.ExpiresAt}
|
|
}
|
|
kind := ""
|
|
if _, err := os.Stat(p.apiKey()); err == nil {
|
|
kind = "api-key"
|
|
} else if h.Access != nil {
|
|
kind = "subscription"
|
|
}
|
|
if kind != "" {
|
|
h.Kind = &kind
|
|
}
|
|
var applied Binding
|
|
if readJSON(p.binding(), &applied) && applied.Licence != "" {
|
|
h.Licence, h.Generation = &applied.Licence, applied.Generation
|
|
}
|
|
return h
|
|
}
|
|
|
|
// GrantAnswer is what `claude_code_grant` answers: a login sealed to the key given, or nothing waiting.
|
|
type GrantAnswer struct {
|
|
Sealed *SealedBox `json:"sealed,omitempty"`
|
|
Identity *Identity `json:"identity,omitempty"`
|
|
Fingerprint string `json:"fingerprint,omitempty"`
|
|
Waiting *bool `json:"waiting,omitempty"`
|
|
}
|
|
|
|
// GrantFor is the full grant in the credentials file sealed to the manager's key — the one time a refresh
|
|
// token leaves this node, for the manager to adopt by refreshing it (ADR 0206). Nothing waiting when the
|
|
// file holds no refresh token.
|
|
func GrantFor(p Paths, managerPublicKey string) (GrantAnswer, error) {
|
|
creds := ReadCredentials(p.credentials())
|
|
rt := RefreshTokenOf(creds)
|
|
if rt == "" {
|
|
no := false
|
|
return GrantAnswer{Waiting: &no}, nil
|
|
}
|
|
raw, err := json.Marshal(creds.oauth())
|
|
if err != nil {
|
|
return GrantAnswer{}, err
|
|
}
|
|
box, err := Seal(string(raw), managerPublicKey)
|
|
if err != nil {
|
|
return GrantAnswer{}, err
|
|
}
|
|
return GrantAnswer{Sealed: &box, Identity: ReadIdentity(p.account()), Fingerprint: Fingerprint(rt)}, nil
|
|
}
|
|
|
|
// Pull asks the seat for this node's current token and applies it.
|
|
func Pull(p Paths, ask Ask, write WriteManaged) (map[string]any, error) {
|
|
keys, err := Keypair(p)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
raw, err := ask(SeatVerb("current"), map[string]any{"consumer": p.Node, "public_key": keys.PublicKey})
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
var c Current
|
|
if err := json.Unmarshal(raw, &c); err != nil || c.Sealed == nil {
|
|
return map[string]any{"applied": false, "reason": "the seat holds no licence for this node"}, nil
|
|
}
|
|
return Apply(p, c, write)
|
|
}
|
|
|
|
// OnBinding takes a change to this node's key in the manager's `bindings` state (ADR 0206): the token is
|
|
// fetched when the generation is newer than the one applied. A released binding keeps the last token,
|
|
// which lives hours, and says so.
|
|
func OnBinding(p Paths, b *BindingState, ask Ask, write WriteManaged) (string, error) {
|
|
if b == nil {
|
|
return "this node's binding was released; it keeps its last token until it expires", nil
|
|
}
|
|
var applied Binding
|
|
if readJSON(p.binding(), &applied) && applied.Generation >= b.Generation {
|
|
return "", nil
|
|
}
|
|
out, err := Pull(p, ask, write)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
raw, _ := json.Marshal(out)
|
|
return string(raw), nil
|
|
}
|
|
|
|
// Apply applies what the seat handed over. A switch replaces the grant whole and cleans up after the old
|
|
// licence; whatever it is, the file is written without a refresh token, so the agent here never refreshes.
|
|
func Apply(p Paths, c Current, write WriteManaged) (map[string]any, error) {
|
|
keys, err := Keypair(p)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
plain, err := Open(*c.Sealed, keys.PrivateKey)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
var previous Binding
|
|
had := readJSON(p.binding(), &previous)
|
|
switched := !had || previous.Licence != c.Licence
|
|
out := map[string]any{"applied": true, "licence": c.Licence, "kind": c.Kind, "switched": switched}
|
|
if c.Kind == "api-key" {
|
|
if err := os.WriteFile(p.apiKey(), []byte(strings.TrimSpace(plain)+"\n"), 0o600); err != nil {
|
|
return nil, err
|
|
}
|
|
if err := os.WriteFile(p.helper(), []byte("#!/bin/sh\nexec cat '"+p.apiKey()+"'\n"), 0o700); err != nil {
|
|
return nil, err
|
|
}
|
|
_ = os.Chmod(p.helper(), 0o700)
|
|
} else {
|
|
var g Grant
|
|
if err := json.Unmarshal([]byte(plain), &g); err != nil {
|
|
return nil, err
|
|
}
|
|
local := ReadCredentials(p.credentials())
|
|
// A login waiting here was handed to the manager first (ADR 0206): what comes back is its successor,
|
|
// and the refresh token in the file is the one the manager just spent.
|
|
d := DecideApply(GrantOf(local), g, switched || HoldsLogin(local))
|
|
if d.Apply {
|
|
next := WithGrant(local, g)
|
|
if switched {
|
|
next = ReplacedBy(local, g)
|
|
}
|
|
if err := WriteCredentials(p.credentials(), next); err != nil {
|
|
return nil, err
|
|
}
|
|
} else {
|
|
out = map[string]any{"applied": false, "licence": c.Licence, "reason": d.Reason}
|
|
}
|
|
// Away from the API key: it goes, with its helper.
|
|
_ = os.Remove(p.apiKey())
|
|
_ = os.Remove(p.helper())
|
|
}
|
|
if switched && c.Identity != nil && c.Identity.AccountUUID != "" {
|
|
changed, err := WriteIdentity(p.account(), *c.Identity)
|
|
if err == nil {
|
|
out["account"] = map[bool]string{true: "updated", false: "unchanged"}[changed]
|
|
}
|
|
}
|
|
gen := c.Generation
|
|
if gen == 0 {
|
|
gen = previous.Generation
|
|
}
|
|
raw, _ := json.Marshal(Binding{Licence: c.Licence, Kind: c.Kind, Generation: gen})
|
|
if err := os.WriteFile(p.binding(), append(raw, '\n'), 0o600); err != nil {
|
|
return nil, err
|
|
}
|
|
// The key-helper comes or goes with the licence's kind.
|
|
if rendered, err := RenderNow(p, write); err != nil {
|
|
out["rendered"] = map[string]any{"failed": err.Error()}
|
|
} else {
|
|
out["rendered"] = rendered
|
|
}
|
|
return out, nil
|
|
}
|
|
|
|
// ---- MCP servers ----------------------------------------------------------------------------------
|
|
|
|
// Registration is a server registered (or, with no entry, unregistered) through this module.
|
|
type Registration struct {
|
|
Name string
|
|
Entry map[string]any
|
|
// Nodes: nil for this node, ["all"] for every node running the module, or a list.
|
|
Nodes []string
|
|
}
|
|
|
|
// ServerState is the `servers` state as this module reaches it through the runtime.
|
|
type ServerState interface {
|
|
Put(key string, value any) error
|
|
Delete(key string) error
|
|
Keys() ([]string, error)
|
|
}
|
|
|
|
// ServerChange is one change to the `servers` state, as a watch hands it over.
|
|
type ServerChange struct {
|
|
Key string
|
|
Op string // put | delete
|
|
Value map[string]any
|
|
}
|
|
|
|
// KeyOf is the key a registration lives at: `all.<server>` for every node, `<node>.<server>` for one.
|
|
func KeyOf(scope, name string) string { return scope + "." + name }
|
|
|
|
// ServerView is what this node takes from the `servers` state: the entries for every node and for this
|
|
// one, kept in memory from the watch and written through to the module's own file whenever what applies
|
|
// here changes, so the managed directory renders without the bus.
|
|
type ServerView struct {
|
|
p Paths
|
|
mu sync.Mutex
|
|
entries map[string]map[string]any
|
|
}
|
|
|
|
// NewServerView is an empty view for this node.
|
|
func NewServerView(p Paths) *ServerView {
|
|
return &ServerView{p: p, entries: map[string]map[string]any{}}
|
|
}
|
|
|
|
// Take takes one change, and answers whether what applies to this node changed.
|
|
func (v *ServerView) Take(c ServerChange) bool {
|
|
scope, name, ok := strings.Cut(c.Key, ".")
|
|
if !ok || scope == "" || (scope != "all" && scope != v.p.Node) {
|
|
return false
|
|
}
|
|
v.mu.Lock()
|
|
if c.Op == "put" && c.Value != nil && EntryProblem(name, c.Value) == "" {
|
|
v.entries[c.Key] = c.Value
|
|
} else {
|
|
delete(v.entries, c.Key)
|
|
}
|
|
v.mu.Unlock()
|
|
return v.writeThrough()
|
|
}
|
|
|
|
// Effective is what applies here: every node's entries, with this node's own laid over them by name.
|
|
func (v *ServerView) Effective() Servers {
|
|
v.mu.Lock()
|
|
defer v.mu.Unlock()
|
|
out := Servers{}
|
|
for _, scope := range []string{"all", v.p.Node} {
|
|
for key, entry := range v.entries {
|
|
if name, ok := strings.CutPrefix(key, scope+"."); ok {
|
|
out[name] = entry
|
|
}
|
|
}
|
|
}
|
|
return out
|
|
}
|
|
|
|
func (v *ServerView) writeThrough() bool {
|
|
now, _ := indented(v.Effective())
|
|
before, _ := os.ReadFile(v.p.registry())
|
|
if string(before) == string(now) {
|
|
return false
|
|
}
|
|
_ = os.WriteFile(v.p.registry(), now, 0o600)
|
|
return true
|
|
}
|
|
|
|
// OnServerChange takes a change from the watch, and renders when what applies here changed.
|
|
func OnServerChange(v *ServerView, c ServerChange, p Paths, write WriteManaged) (string, error) {
|
|
if !v.Take(c) {
|
|
return "", nil
|
|
}
|
|
if _, err := RenderNow(p, write); err != nil {
|
|
return "", err
|
|
}
|
|
what := "registered"
|
|
if c.Op != "put" {
|
|
what = "unregistered"
|
|
}
|
|
return what + " " + c.Key, nil
|
|
}
|
|
|
|
// RegisterServer registers (or, with no entry, unregisters) a server: a put (or delete) per scope in the
|
|
// `servers` state, taken into this node's view at once so the answer says what it did here; every other
|
|
// node takes it from its watch, and a node that joins later from the current state.
|
|
func RegisterServer(p Paths, r Registration, servers ServerState, v *ServerView, write WriteManaged,
|
|
others func() ([]string, error)) (map[string]any, error) {
|
|
if r.Entry != nil {
|
|
if problem := EntryProblem(r.Name, r.Entry); problem != "" {
|
|
return map[string]any{"registered": false, "reason": problem}, nil
|
|
}
|
|
}
|
|
scopes := r.Nodes
|
|
if len(scopes) == 0 {
|
|
scopes = []string{p.Node}
|
|
}
|
|
// Compared before and after rather than read from Take: this node's own watch may hand the view the
|
|
// same change first, and then Take here finds nothing new although this call made it.
|
|
before, _ := json.Marshal(v.Effective())
|
|
for _, scope := range scopes {
|
|
key := KeyOf(scope, r.Name)
|
|
var err error
|
|
if r.Entry != nil {
|
|
err = servers.Put(key, r.Entry)
|
|
} else {
|
|
err = servers.Delete(key)
|
|
}
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
op := "put"
|
|
if r.Entry == nil {
|
|
op = "delete"
|
|
}
|
|
v.Take(ServerChange{Key: key, Op: op, Value: r.Entry})
|
|
}
|
|
after, _ := json.Marshal(v.Effective())
|
|
changed := string(before) != string(after)
|
|
here := false
|
|
for _, s := range scopes {
|
|
here = here || s == "all" || s == p.Node
|
|
}
|
|
verb := "registered"
|
|
if r.Entry == nil {
|
|
verb = "unregistered"
|
|
}
|
|
answer := map[string]any{verb: r.Name, "on": scopes}
|
|
switch {
|
|
case !here:
|
|
answer["here"] = "not this node"
|
|
case changed:
|
|
answer["here"] = "changed"
|
|
default:
|
|
answer["here"] = "already so"
|
|
}
|
|
if changed {
|
|
rendered, err := RenderNow(p, write)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
answer["rendered"] = rendered
|
|
}
|
|
if r.Entry == nil {
|
|
if _, still := v.Effective()[r.Name]; still {
|
|
answer["still"] = r.Name + " still applies here from another registration (for every node, or for this one); unregister that too"
|
|
}
|
|
}
|
|
if len(r.Nodes) == 0 {
|
|
// The question the operator wanted asked: here only, or more?
|
|
var elsewhere []string
|
|
if nodes, err := others(); err == nil {
|
|
for _, n := range nodes {
|
|
if n != p.Node {
|
|
elsewhere = append(elsewhere, n)
|
|
}
|
|
}
|
|
}
|
|
if len(elsewhere) > 0 {
|
|
answer["also"] = fmt.Sprintf("claude-code also runs on %s. To %s it there too, call again with nodes: \"all\" or a list of those nodes.",
|
|
strings.Join(elsewhere, ", "), map[bool]string{true: "register", false: "unregister"}[r.Entry != nil])
|
|
} else {
|
|
answer["also"] = "To do the same on every node running claude-code, call again with nodes: \"all\"."
|
|
}
|
|
}
|
|
return answer, nil
|
|
}
|
|
|
|
// stamp is a time as the status answers it.
|
|
func stamp(ms int64) string { return time.UnixMilli(ms).UTC().Format(time.RFC3339) }
|