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.
366 lines
13 KiB
Go
366 lines
13 KiB
Go
// claude-code's bundle (novox/hq design 36, ADR 0183, ADR 0206): a binary the node's runtime launches over
|
|
// stdio as the operator account (ADR 0193) and is the bus for (ADR 0198). It is given its state directory
|
|
// and two files the mesh renders into it (ADR 0192), beside the runtime's own words.
|
|
//
|
|
// At start it renders the agent's managed directory, reports what this node holds as the module's
|
|
// `holdings` state and again whenever the credentials file changes, watches the licence manager's
|
|
// `bindings` state for this node and fetches the token when it says so, and watches the module's
|
|
// `servers` state — every node's MCP server registrations (ADR 0201). node.go holds the logic.
|
|
//
|
|
// stdout is the MCP channel; everything this module says, it says on stderr.
|
|
package main
|
|
|
|
import (
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"os"
|
|
"os/exec"
|
|
"path/filepath"
|
|
"strings"
|
|
"time"
|
|
|
|
stdio "git.novox.be/novox/mesh-sdk/go"
|
|
)
|
|
|
|
func say(format string, args ...any) {
|
|
fmt.Fprintf(os.Stderr, "[claude-code] "+format+"\n", args...)
|
|
}
|
|
|
|
// writeManaged writes one managed file as root, only when its content changed. From a staged file, never
|
|
// /dev/stdin: a child's input may be a socket, which /dev/stdin cannot open (found on the first assignment).
|
|
func writeManaged(name, content string) (string, error) {
|
|
path := filepath.Join(ManagedDir, name)
|
|
if was, err := os.ReadFile(path); err == nil && string(was) == content {
|
|
return name + ": unchanged", nil
|
|
}
|
|
staged, err := os.MkdirTemp("", "claude-code-")
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
defer os.RemoveAll(staged)
|
|
source := filepath.Join(staged, name)
|
|
if err := os.WriteFile(source, []byte(content), 0o644); err != nil {
|
|
return "", err
|
|
}
|
|
args := []string{"install", "-D", "-m", "0644", source, path}
|
|
if os.Geteuid() != 0 {
|
|
args = append([]string{"sudo", "-n"}, args...)
|
|
}
|
|
if out, err := exec.Command(args[0], args[1:]...).CombinedOutput(); err != nil {
|
|
return "", fmt.Errorf("%s: could not be written to %s (%s); the module writes there through the operator account's passwordless sudo",
|
|
name, ManagedDir, strings.TrimSpace(string(out)))
|
|
}
|
|
return name + ": written", nil
|
|
}
|
|
|
|
// ask is a tool on the bus, through the runtime: its answer is the tool's value.
|
|
func ask(address string, args any) (json.RawMessage, error) { return stdio.Ask(address, args) }
|
|
|
|
// stateOf adapts the SDK's state to what node.go asks of one.
|
|
type stateOf struct{ s stdio.KeptState }
|
|
|
|
func (s stateOf) Put(key string, value any) error { _, err := s.s.Put(key, value); return err }
|
|
func (s stateOf) Delete(key string) error { return s.s.Delete(key) }
|
|
func (s stateOf) Keys() ([]string, error) { return s.s.Keys() }
|
|
|
|
// nodesRunningMe is the nodes claude-code runs on, from the controller's list of modules — for the register
|
|
// tool's question.
|
|
func nodesRunningMe() ([]string, error) {
|
|
raw, err := ask("seat:mesh-controller.modules", map[string]any{})
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
var answer struct {
|
|
Output string `json:"output"`
|
|
}
|
|
text := string(raw)
|
|
if json.Unmarshal(raw, &answer) == nil && answer.Output != "" {
|
|
text = answer.Output
|
|
}
|
|
for _, line := range strings.Split(text, "\n") {
|
|
if !strings.HasPrefix(line, "claude-code ") {
|
|
continue
|
|
}
|
|
_, on, ok := strings.Cut(line, " on ")
|
|
if !ok || strings.TrimSpace(on) == "nothing" {
|
|
return nil, nil
|
|
}
|
|
var out []string
|
|
for _, n := range strings.Split(on, ",") {
|
|
if n = strings.TrimSpace(n); n != "" {
|
|
out = append(out, n)
|
|
}
|
|
}
|
|
return out, nil
|
|
}
|
|
return nil, nil
|
|
}
|
|
|
|
func fingerprintOfFile(path string) any {
|
|
raw, err := os.ReadFile(path)
|
|
if err != nil {
|
|
return nil
|
|
}
|
|
return Fingerprint(string(raw))
|
|
}
|
|
|
|
func status(p Paths) map[string]any {
|
|
creds := ReadCredentials(p.credentials())
|
|
var token any
|
|
if g := GrantOf(creds); g != nil {
|
|
token = map[string]any{"fingerprint": Fingerprint(g.AccessToken), "expiresAt": stamp(g.ExpiresAt), "loginWaiting": HoldsLogin(creds)}
|
|
}
|
|
var managed []map[string]any
|
|
for _, f := range []string{"managed-mcp.json", "managed-settings.json", "CLAUDE.md"} {
|
|
path := filepath.Join(ManagedDir, f)
|
|
managed = append(managed, map[string]any{"file": path, "fingerprint": fingerprintOfFile(path)})
|
|
}
|
|
var licence any
|
|
var b Binding
|
|
if readJSON(p.binding(), &b) {
|
|
licence = b
|
|
}
|
|
names := []string{}
|
|
for n := range Registered(p) {
|
|
names = append(names, n)
|
|
}
|
|
return map[string]any{"node": p.Node, "licence": licence, "token": token, "holdings": HoldingsOf(p),
|
|
"managed": managed, "registered": names}
|
|
}
|
|
|
|
func str(description string) map[string]any {
|
|
return map[string]any{"type": "string", "description": description}
|
|
}
|
|
|
|
// nodesOf reads the tools' `nodes` argument: absent is this node, "all" every node, else a list.
|
|
func nodesOf(v any) []string {
|
|
s, _ := v.(string)
|
|
s = strings.TrimSpace(s)
|
|
switch s {
|
|
case "":
|
|
return nil
|
|
case "all":
|
|
return []string{"all"}
|
|
}
|
|
var out []string
|
|
for _, n := range strings.Split(s, ",") {
|
|
if n = strings.TrimSpace(n); n != "" {
|
|
out = append(out, n)
|
|
}
|
|
}
|
|
return out
|
|
}
|
|
|
|
func tools(p Paths, servers ServerState, view *ServerView) []stdio.Tool {
|
|
nodesArg := str(`more nodes: "all" for every node running claude-code, or a comma-separated list; absent is this node only`)
|
|
return []stdio.Tool{
|
|
{Name: "claude_code_status",
|
|
Description: "Claude Code on this machine as the mesh configured it: the licence it holds and when its token expires, what it reports holding, the managed files, the MCP servers registered here. Fingerprints only, never a token.",
|
|
Run: func(map[string]any) (any, error) { return status(p), nil }},
|
|
{Name: "claude_code_render",
|
|
Description: "Write Claude Code's managed directory now, from the mesh's facts, this module's settings and the servers registered here.",
|
|
Run: func(map[string]any) (any, error) {
|
|
out, err := RenderNow(p, writeManaged)
|
|
return map[string]any{"rendered": out}, err
|
|
}},
|
|
{Name: "claude_code_pull",
|
|
Description: "Ask the licence manager for this node's current token now and apply it, rather than waiting for its binding to change.",
|
|
Run: func(map[string]any) (any, error) { return Pull(p, ask, writeManaged) }},
|
|
{Name: "claude_code_grant",
|
|
Description: "For the licence manager (ADR 0206): the full grant in this node's credentials file — a login made here — sealed to the public key given, with the account it belongs to. Nothing when no login is waiting. Never answers a token in the clear.",
|
|
Input: map[string]any{"public_key": str("the manager's public key, PEM; the grant opens only with its private half")},
|
|
Run: func(a map[string]any) (any, error) {
|
|
key, _ := a["public_key"].(string)
|
|
if !strings.Contains(key, "PUBLIC KEY") {
|
|
return nil, errors.New("claude_code_grant seals to a public key, and none was given")
|
|
}
|
|
return GrantFor(p, key)
|
|
}},
|
|
{Name: "claude_code_mcp_list",
|
|
Description: "The MCP servers registered through this module: those that apply on this node (beside the console, `mesh`, and those set in the module's settings), and every registration on the mesh, by key — `all.<server>` for every node, `<node>.<server>` for one.",
|
|
Run: func(map[string]any) (any, error) {
|
|
keys, err := servers.Keys()
|
|
return map[string]any{"here": Registered(p), "everywhere": keys}, err
|
|
}},
|
|
{Name: "claude_code_mcp_register",
|
|
Description: "Register an MCP server with Claude Code on this node, every node, or a list — an http/sse server by url, or a stdio server by command. Kept on the bus, so a node that joins later takes it too. Never put a secret in env or headers: the mesh refuses one.",
|
|
Input: map[string]any{
|
|
"name": str("the server's name: letters, digits, - and _"),
|
|
"type": str("http, sse or stdio (default stdio when a command is given, http when a url is)"),
|
|
"url": str("an http or sse server's url"),
|
|
"command": str("a stdio server's program"),
|
|
"args": map[string]any{"type": "array", "description": "a stdio server's arguments"},
|
|
"env": map[string]any{"type": "object", "description": "a stdio server's environment"},
|
|
"headers": map[string]any{"type": "object", "description": "an http server's headers"},
|
|
"nodes": nodesArg,
|
|
},
|
|
Run: func(a map[string]any) (any, error) {
|
|
entry := map[string]any{}
|
|
if t, _ := a["type"].(string); t != "" {
|
|
entry["type"] = t
|
|
} else if _, hasURL := a["url"]; hasURL {
|
|
entry["type"] = "http"
|
|
} else {
|
|
entry["type"] = "stdio"
|
|
}
|
|
for _, k := range []string{"url", "command", "args", "env", "headers"} {
|
|
if v, ok := a[k]; ok {
|
|
entry[k] = v
|
|
}
|
|
}
|
|
name, _ := a["name"].(string)
|
|
return RegisterServer(p, Registration{Name: name, Entry: entry, Nodes: nodesOf(a["nodes"])}, servers, view, writeManaged, nodesRunningMe)
|
|
}},
|
|
{Name: "claude_code_mcp_unregister",
|
|
Description: "Remove an MCP server registered through this module, on this node or more.",
|
|
Input: map[string]any{"name": str("the server's name"), "nodes": nodesArg},
|
|
Run: func(a map[string]any) (any, error) {
|
|
name, _ := a["name"].(string)
|
|
return RegisterServer(p, Registration{Name: name, Nodes: nodesOf(a["nodes"])}, servers, view, writeManaged, nodesRunningMe)
|
|
}},
|
|
}
|
|
}
|
|
|
|
// persist asks the state again until it answers: its bucket or the bus's grant may arrive after the module.
|
|
func persist(what string, attempt func() error, done func(refusals int)) {
|
|
waits := []time.Duration{2 * time.Second, 5 * time.Second, 10 * time.Second, 30 * time.Second}
|
|
for n := 0; ; n++ {
|
|
err := attempt()
|
|
if err == nil {
|
|
done(n)
|
|
return
|
|
}
|
|
pause := time.Minute
|
|
if n < len(waits) {
|
|
pause = waits[n]
|
|
}
|
|
say("%s not yet (%v); asking again in %s", what, err, pause)
|
|
time.Sleep(pause)
|
|
}
|
|
}
|
|
|
|
func main() {
|
|
p, launched := PathsFrom(os.Getenv)
|
|
if !launched {
|
|
// Outside a launch — a build, a check — it serves nothing and says why.
|
|
say("not launched by the runtime with this module's words; serving no tools")
|
|
if err := stdio.Serve("", nil); err != nil {
|
|
os.Exit(1)
|
|
}
|
|
return
|
|
}
|
|
if _, err := Keypair(p); err != nil {
|
|
say("this module's key: %v", err)
|
|
}
|
|
if out, err := RenderNow(p, writeManaged); err != nil {
|
|
say("%v", err)
|
|
} else {
|
|
for _, line := range out {
|
|
if !strings.HasSuffix(line, "unchanged") {
|
|
say("%s", line)
|
|
}
|
|
}
|
|
}
|
|
servers := stateOf{stdio.State("servers")}
|
|
view := NewServerView(p)
|
|
go run(p, view)
|
|
if err := stdio.Serve("", tools(p, servers, view)); err != nil {
|
|
say("%v", err)
|
|
os.Exit(1)
|
|
}
|
|
}
|
|
|
|
// run is the module's long-running half, beside the tools (ADR 0198).
|
|
func run(p Paths, view *ServerView) {
|
|
// Every node's MCP servers: the whole current set first, then each change (ADR 0201).
|
|
go persist("watching the MCP servers", func() error {
|
|
return stdio.State("servers").Watch("", func(c stdio.StateChange) error {
|
|
var value map[string]any
|
|
_ = json.Unmarshal(c.Value, &value)
|
|
if done, err := OnServerChange(view, ServerChange{Key: c.Key, Op: c.Op, Value: value}, p, writeManaged); err != nil {
|
|
say("taking %s %s: %v", c.Op, c.Key, err) // the view took it; the next render writes it
|
|
} else if done != "" {
|
|
say("%s", done)
|
|
}
|
|
return nil
|
|
})
|
|
}, func(n int) { say("watching the MCP servers%s", refusals(n)) })
|
|
|
|
// What this node holds (ADR 0206): at start — a node already logged in is reported at once — and on
|
|
// every change of the credentials file, polled, because the file is replaced by rename and a watch on
|
|
// the old inode would go quiet. Fingerprints and expiries only.
|
|
holdings := stdio.State("holdings")
|
|
reported := ""
|
|
report := func() {
|
|
now := HoldingsOf(p)
|
|
raw, _ := json.Marshal(now)
|
|
if string(raw) == reported {
|
|
return
|
|
}
|
|
persist("reporting what this node holds", func() error { _, err := holdings.Put(p.Node, now); return err }, func(int) {
|
|
reported = string(raw)
|
|
account := "no account"
|
|
if now.Identity != nil && now.Identity.EmailAddress != "" {
|
|
account = now.Identity.EmailAddress
|
|
}
|
|
line := "reported: " + account
|
|
if now.Kind != nil {
|
|
line += ", " + *now.Kind
|
|
}
|
|
if now.Refresh.Present {
|
|
line += ", a login waiting"
|
|
}
|
|
if now.Licence != nil {
|
|
line += fmt.Sprintf(", licence %s g%d", *now.Licence, now.Generation)
|
|
}
|
|
say("%s", line)
|
|
})
|
|
}
|
|
|
|
// What this node should hold (ADR 0206): the manager's `bindings` key for this node; a newer
|
|
// generation is fetched with the seat's `current`, sealed to this module's key.
|
|
go persist("watching this node's licence binding", func() error {
|
|
return stdio.State(Manager+".bindings").Watch(p.Node, func(c stdio.StateChange) error {
|
|
if c.Key != p.Node {
|
|
return nil
|
|
}
|
|
var b *BindingState
|
|
if c.Op == "put" {
|
|
b = &BindingState{}
|
|
if err := json.Unmarshal(c.Value, b); err != nil {
|
|
return nil
|
|
}
|
|
}
|
|
if done, err := OnBinding(p, b, ask, writeManaged); err != nil {
|
|
say("fetching this node's token failed: %v", err)
|
|
} else if done != "" {
|
|
say("%s", done)
|
|
}
|
|
go report()
|
|
return nil
|
|
})
|
|
}, func(n int) { say("watching this node's licence binding%s", refusals(n)) })
|
|
|
|
report()
|
|
var last string
|
|
for range time.Tick(5 * time.Second) {
|
|
info, err := os.Stat(p.credentials())
|
|
now := "absent"
|
|
if err == nil {
|
|
now = fmt.Sprintf("%d/%d", info.ModTime().UnixNano(), info.Size())
|
|
}
|
|
if now != last {
|
|
last = now
|
|
report()
|
|
}
|
|
}
|
|
}
|
|
|
|
func refusals(n int) string {
|
|
if n == 0 {
|
|
return ""
|
|
}
|
|
return fmt.Sprintf(" (after %d refusal(s))", n)
|
|
}
|