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.` for // every node, `.` for one — which every node watches. import ( "crypto/sha256" "encoding/hex" "encoding/json" "errors" "fmt" "os" "path/filepath" "regexp" "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). // // Home is the home of the account the agent runs as, and Agent that account (novox/hq ADR 0266): the runtime's // MESH_AGENT_ACCOUNT and MESH_AGENT_HOME, which the controller sets to the operator's account where the node // names no agent account of its own — and which a controller older than that record does not set, so the // operator's are read then. Operator is the operator's account: Confined says the agent's is another. // AgentHome and OperatorHome are the runtime's words as given, read to judge whose home the home scope writes // (novox/hq issue 340). type Paths struct { State, Facts, Settings, Home, Node string // Forge is the file the forge's read credential arrives in (MESH_CLAUDE_CODE_FORGE): named by a word of the // bundle, so the runtime's account owns it and can read it (catalogue givenTo); empty is gitea-read.secret in State. Forge string Agent, Operator, AgentHome, OperatorHome 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"), Forge: env("MESH_CLAUDE_CODE_FORGE"), Home: env("MESH_AGENT_HOME"), Node: env("MESH_NODE"), Agent: env("MESH_AGENT_ACCOUNT"), Operator: env("MESH_OPERATOR_ACCOUNT"), AgentHome: env("MESH_AGENT_HOME"), OperatorHome: env("MESH_OPERATOR_HOME")} if (p.Home == "") != (p.Agent == "") { // Half of the agent's account is not "the operator's": taken so, the agent's account rule of the guard // would be off and its home never written, and nothing would say so. Not launched, said (main). return p, false } if p.Home == "" { p.Home, p.Agent, p.AgentHome = env("MESH_OPERATOR_HOME"), p.Operator, env("MESH_OPERATOR_HOME") } return p, p.State != "" && p.Facts != "" && p.Settings != "" && p.Home != "" && p.Node != "" } // Confined says the agent runs here as an account of its own, not the operator's (novox/hq ADR 0266). func (p Paths) Confined() bool { return p.Agent != "" && p.Operator != "" && p.Agent != p.Operator } // HomeIsTheAgentsOwn says whether the home this module writes the home scope into belongs to an agent account of // its own (novox/hq issue 340): the agent is confined here, and the home is the agent's, not the operator's. // Anywhere else the home scope would write the operator's own ~/.claude, whose rules and skills load in every // session the operator starts. func (p Paths) HomeIsTheAgentsOwn() bool { if !p.Confined() || p.AgentHome == "" || p.Home == "" { return false } home := filepath.Clean(p.Home) return home == filepath.Clean(p.AgentHome) && (p.OperatorHome == "" || home != filepath.Clean(p.OperatorHome)) } // HomeChangeRefused says why this node's home may not be changed through the tools, or "": removing or putting // back an item of it changes what every session there obeys as surely as placing one — an agent could remove a // rule that constrains it — so it is done only where the home belongs to an agent account of its own (novox/hq // issue 340). The operator's own home is the operator's, changed by the person, not through a call. func HomeChangeRefused(p Paths, what string) string { if p.HomeIsTheAgentsOwn() { return "" } return what + " on " + p.Node + " would change the operator's own ~/.claude, whose rules and skills every " + "session the operator starts obeys, and the tool runner cannot tell the operator from an agent. It is done " + "only where the agent runs as an account of its own and the home is that account's (novox/hq ADR 0266, " + "issue 340); in the operator's own home it is the person's act, by hand. Nothing was changed" } // HomeScopeRefused says why the home scope is not written for a node, or "": only this node's own home, and only // where it belongs to an agent account of its own (novox/hq issue 340). Another node's home cannot be judged // from here; that node's own module judges it. func HomeScopeRefused(p Paths, target string) string { if target != p.Node { return "the home scope reaches " + target + "'s home, and whose home that is cannot be judged from " + p.Node + "; call that node's own claude-code tool, which judges it (novox/hq issue 340). Nothing was registered" } if !p.HomeIsTheAgentsOwn() { return "the home scope on " + p.Node + " would write the operator's own ~/.claude, whose rules and skills " + "load in every session the operator starts there, and the tool runner cannot tell the operator from an " + "agent. It is taken only where the agent runs as an account of its own and the home is that account's " + "(novox/hq ADR 0266, issue 340). Nothing was registered" } return "" } 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.keyDir(), "api-key") } func (p Paths) helper() string { return filepath.Join(p.keyDir(), "api-key-helper") } // helperCommand is the managed settings' apiKeyHelper: the key-helper's path in the module's state where the agent // runs as the operator. Where it runs as an account of its own, never a file under that account's home: the // managed settings apply to every account's `claude`, the operator's and root's included, so a script the agent // owns there would run as them at their next start (novox/hq ADR 0266, review of 2026-10-09). It is a command // written whole in the root-owned managed settings instead, which reads the key only when run as the agent's // account, and as it. func (p Paths) helperCommand() string { if !p.Confined() { return p.helper() } return "[ \"$(id -un)\" = " + shellQuote(p.Agent) + " ] && exec cat -- " + shellQuote(p.apiKey()) } // shellQuote is s as one word of a POSIX shell. func shellQuote(s string) string { return "'" + strings.ReplaceAll(s, "'", `'\''`) + "'" } // keyDir is where an API-key licence lives: the module's state, with the key-helper the agent runs from it as // the operator — or, where the agent runs as an account of its own, which cannot enter the module's state, a // directory of the module's in that account's agent directory (novox/hq ADR 0266), and no helper there: the // managed settings carry its command (helperCommand). Either way readable by the account the agent runs as, // and by it alone. func (p Paths) keyDir() string { if p.Confined() { return filepath.Join(p.Home, ".claude", "mesh") } return p.State } func (p Paths) registry() string { return filepath.Join(p.State, "mcp-servers.json") } func (p Paths) config() string { return filepath.Join(p.State, "config.json") } func (p Paths) placed() string { if p.Confined() { // One record per home: what was placed in the operator's is not in the agent's (ADR 0266). return filepath.Join(p.State, "home-placed-"+p.Agent+".json") } return filepath.Join(p.State, "home-placed.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 := fsys.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 := fsys.Stat(priv); errors.Is(err, os.ErrNotExist) { k, err := GenerateKeyPair() if err != nil { return KeyPair{}, err } if err := fsys.WriteFile(priv, []byte(k.PrivateKey), 0o600); err != nil { return KeyPair{}, err } if err := fsys.WriteFile(pub, []byte(k.PublicKey), 0o644); err != nil { return KeyPair{}, err } } a, err1 := fsys.ReadFile(priv) b, err2 := fsys.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 } var renderMu sync.Mutex // 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) { // One at a time: the watches and the tools all render, and the home's record of what was placed is // read and written whole. renderMu.Lock() defer renderMu.Unlock() 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) } if ForgeCredentialHeld(p) { facts.ForgeCredentials = p.forgeCredentials() } var settings Settings readJSON(p.Settings, &settings) var binding *Binding var b Binding if readJSON(p.binding(), &b) { binding = &b } var items map[string]Item _ = readJSON(p.config(), &items) config := ConfigOf(Taken(items)) var leftOut []string config.Terminal, leftOut = terminalText(p) if approved, err := ReadApproved(p); err != nil { leftOut = append(leftOut, "the sections the operator approved: "+err.Error()) } else { config.Approved = ApprovedHere(approved, p.Node) } mesh := ReadMeshTools(p) if mesh != nil { mesh.Refused = RefusedCounts(GuardDataOf(p, mesh).Log) } files := RenderWithMesh(facts, settings, binding, p.helperCommand(), Registered(p), config, mesh) tree, _ := json.Marshal(MarketplaceWith(config, guardFiles(p, mesh))) files[MarketplaceDir+"/"] = string(tree) names := make([]string, 0, len(files)) for n := range files { names = append(names, n) } // The marketplace first: the settings that enable its plugin must never name one that is not there yet. sort.Slice(names, func(i, j int) bool { if (names[i] == MarketplaceDir+"/") != (names[j] == MarketplaceDir+"/") { return names[i] == MarketplaceDir+"/" } return names[i] < names[j] }) // Every file is attempted: one that cannot be written — a registration the vendor's layout refuses, a // failed escalation — must not keep the licence, the tool servers or the instructions from landing. var out []string var failed []error for _, n := range names { line, err := write(n, files[n]) if err != nil { failed = append(failed, err) continue } out = append(out, line) } // The home scope: what this module places in the account's own agent directory (ADR 0182). // Only into a home that belongs to an agent account of its own (novox/hq issue 340): the operator's own home is // never written from the bus, whoever put an item there; what the module placed there before is taken back, // unless the person changed it since. want := HomeFiles(config) if !p.HomeIsTheAgentsOwn() { if len(want) > 0 { out = append(out, fmt.Sprintf("home: %d file(s) registered for this home not placed: it is the operator's "+ "own, and the home scope writes only an agent account's own home (novox/hq issue 340)", len(want))) } want = nil } done, left := PlaceHome(p, want) for _, line := range done { out = append(out, "home "+line) } for _, line := range left { out = append(out, "home "+line) } for _, line := range leftOut { out = append(out, "left out of the terminal's shared text: "+line) } // Where the agent moved to an account of its own, what this module placed for it under the operator's home // goes (ADR 0266). out = append(out, RetireOperatorHome(p)...) return out, errors.Join(failed...) } // ---- 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"` // Why and By are what moved the binding and who asked, as the manager recorded it (novox/hq issue 625). Why string `json:"why,omitempty"` By string `json:"by,omitempty"` } // 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 := fsys.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 := fsys.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 binding differs from the one applied. A released binding keeps the last token, which // lives hours, and says so. // // **Differs, not "is newer"** (novox/hq issue 243). The state keeps only the latest value per node, so // nothing older can arrive. A manager whose store was rebuilt counts generations from one again, and // a node that waited for a number above its own ignored every binding it was sent, its login and the // licence's rotations included, until the count caught up. Only the binding already applied is skipped. 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 && applied.Licence == b.Licence && holdsApplied(p, applied.Kind) { return "", nil } out, err := Pull(p, ask, write) if err != nil { return "", err } // The line says which binding this was and why the manager moved it, so a node's journal alone answers // what changed here and who asked (novox/hq issue 625). out["generation"] = b.Generation if b.Why != "" { out["why"] = b.Why } if b.By != "" { out["by"] = b.By } 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 := fsys.MkdirAll(p.keyDir(), 0o700); err != nil { return nil, err } if err := fsys.WriteFile(p.apiKey(), []byte(strings.TrimSpace(plain)+"\n"), 0o600); err != nil { return nil, err } if p.Confined() { // No script under the agent's home: the managed settings carry the command whole (helperCommand). _ = fsys.Remove(p.helper()) } else { if err := fsys.WriteFile(p.helper(), []byte("#!/bin/sh\nexec cat "+shellQuote(p.apiKey())+"\n"), 0o700); err != nil { return nil, err } _ = fsys.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. _ = fsys.Remove(p.apiKey()) _ = fsys.Remove(p.helper()) } // The account file follows the token: on a switch, and wherever the agent's account file names no account // yet — a home the agent has just moved to (ADR 0266) — so the agent shows the account whose token it holds. if (switched || ReadIdentity(p.account()) == nil) && 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 := fsys.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 } // apiKeyShape is an Anthropic API key; apiKeyFileMax the most a file holding one is read for. var apiKeyShape = regexp.MustCompile(`^sk-ant-[A-Za-z0-9_-]{20,300}$`) const apiKeyFileMax = 4096 // agentsOwnFile is a path under the home of the account the agent runs as, with no symbolic link between the // home and it, the file itself included; refused otherwise. func agentsOwnFile(p Paths, file string) (string, error) { if p.Home == "" || !filepath.IsAbs(file) { return "", fmt.Errorf("%s: the key file is named by its full path, or ~/…, in the agent's home", file) } home, clean := filepath.Clean(p.Home), filepath.Clean(file) rel, err := filepath.Rel(home, clean) if err != nil || rel == "." || rel == ".." || strings.HasPrefix(rel, ".."+string(filepath.Separator)) { return "", fmt.Errorf("%s is not in the agent's home (%s): the key file is taken only from there", file, home) } at := home for _, part := range strings.Split(rel, string(filepath.Separator)) { at = filepath.Join(at, part) info, err := fsys.Lstat(at) if err != nil { return "", fmt.Errorf("the key file: %w", err) } if info.Mode()&os.ModeSymlink != 0 { return "", fmt.Errorf("%s is a symbolic link: the key file is taken only from the agent's home itself", at) } } return clean, nil } // AddAPIKey adds an API key from a file on this node to the licence manager (ADR 0209): sealed to the // manager's public key, handed to the seat's `adopt` on request/reply, and the file removed once taken. With // useHere, this node is switched to the new licence. The key never crosses the bus in the clear and is // never an argument. // // **The file is the agent's, never the runtime's** (novox/hq ADR 0266). The tool reads a file and removes it, // and the runtime runs as the operator's account: a path anywhere would let whoever calls it take the runtime's // bus credential or the operator's keys off the machine, sealed, and delete the original. So the file must lie // under the home of the account the agent runs as, reached through no symbolic link, and is read as that // account; what it holds must be an Anthropic API key, or nothing is handed over and nothing removed. func AddAPIKey(p Paths, name, file string, useHere bool, ask Ask) (map[string]any, error) { file, err := agentsOwnFile(p, file) if err != nil { return nil, err } raw, err := fsys.ReadFile(file) if err != nil { return nil, fmt.Errorf("the key is read from a file in the agent's home: %w", err) } if len(raw) > apiKeyFileMax { return nil, fmt.Errorf("%s is not a key file: larger than %d bytes; nothing handed over", file, apiKeyFileMax) } key := strings.TrimSpace(string(raw)) if key == "" { return nil, fmt.Errorf("%s is empty", file) } if !apiKeyShape.MatchString(key) { // Never the content: the file may hold a secret of another kind. return nil, fmt.Errorf("%s does not hold an Anthropic API key (sk-ant-…); nothing handed over, nothing removed", file) } answer, err := ask(SeatVerb("public-key"), map[string]any{}) if err != nil { return nil, err } var pk struct { PublicKey string `json:"public_key"` } if err := json.Unmarshal(answer, &pk); err != nil || !strings.Contains(pk.PublicKey, "PUBLIC KEY") { return nil, errors.New("the licence manager did not say what key to seal to") } box, err := Seal(key, pk.PublicKey) if err != nil { return nil, err } adopted, err := ask(SeatVerb("adopt"), map[string]any{"name": name, "sealed": box, "from": p.Node}) if err != nil { return nil, err } out := map[string]any{"adopted": json.RawMessage(adopted)} // Taken: the key now lives encrypted in the manager's store alone. if err := fsys.Remove(file); err != nil { out["file"] = "could not be removed: " + err.Error() } else { out["file"] = "removed" } if useHere { switched, err := ask(SeatVerb("switch"), map[string]any{"consumer": p.Node, "licence": name}) if err != nil { return out, fmt.Errorf("adopted, and switching this node to it failed: %w", err) } out["switched"] = json.RawMessage(switched) } 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) Get(key string) (json.RawMessage, bool, 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.` for every node, `.` 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, _ := fsys.ReadFile(v.p.registry()) if string(before) == string(now) { return false } _ = fsys.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 } // ServerAtTheTerminal is why a tool server is never registered through this module (novox/hq issue 340): every // session on the nodes it reaches starts it, a stdio server as a program run as whoever started the session, and // an http one is handed what the session asks — and the tool runner is not told who called it. const ServerAtTheTerminal = "a tool server is started, or handed what the agent asks it, by every session on the nodes it " + "reaches, as whoever starts that session — the operator, whose account holds root — and the tool runner is not " + "told who called it, so an agent on another node could register one. It is set at the controller's terminal " + "only, in this module's mcp_servers setting, on the control-node: mesh-cli settings set claude-code '{\"mcp_servers\": {...}}' " + "[--node ] (novox/hq issue 340). Nothing was registered" // RegisterServer unregisters a server: a 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. A registration — an entry // given — is refused (ServerAtTheTerminal): what is in the state from before stays, and is only ever removed here. func RegisterServer(p Paths, r Registration, servers ServerState, v *ServerView, write WriteManaged, others func() ([]string, error)) (map[string]any, error) { if r.Entry != nil { return nil, errors.New(ServerAtTheTerminal) } 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) if err := servers.Delete(key); err != nil { return nil, err } v.Take(ServerChange{Key: key, Op: "delete"}) } after, _ := json.Marshal(v.Effective()) changed := string(before) != string(after) here := false for _, s := range scopes { here = here || s == "all" || s == p.Node } answer := map[string]any{"unregistered": 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 _, 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 unregister it there too, call again with nodes: \"all\" or a list of those nodes.", strings.Join(elsewhere, ", ")) } 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) }