Compare commits

...
Author SHA1 Message Date
jschoubben 5d59b35cf7 photos authenticates against the database its user lives in (hq issue 232)
The provider creates each consumer's user in that consumer's own database.
photos asked for admin, where no such user exists; invoicing, against the
same provider, already asked for the name the mesh gave it and worked.

Invisible until hq 225 was fixed: while the provisioner could not read its
secrets, no user existed anywhere, so 'UserNotFound for db admin' was a true
and complete account of that fault. A fault that explains the symptom is not
evidence there is only one.
2026-10-04 12:33:56 +02:00
mesh-admin b485379505 Merge pull request 'The licence manager (Go) and claude-code's half of ADR 0206' (#262) from feat/the-licence-manager into main 2026-10-04 10:31:25 +00:00
jochen 306d01d74d claude-code in Go (operator: always Go)
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.
2026-10-04 12:27:36 +02:00
mesh-admin 19a4055bb5 Merge pull request 'The photo clients publish the endpoint they declare (hq issue 227)' (#263) from fix/the-photo-admin-client-publishes-the-port-it-declares into main 2026-10-04 10:27:22 +00:00
jochen 15b2e6b86e The licence manager, in Go, and claude-code's half of ADR 0206
claude-licence-manager holds the anthropic-licence-manager seat: it reads
every node's holdings state, adopts a login it does not hold by refreshing
it (newest first, once per account), keeps each grant alive under a lease,
publishes what each consumer should hold as its bindings state with a
generation, and answers current sealed to the consumer's key. Postgres
store prepared by a run-once step; grants encrypted with the vault's key.

claude-code reports what its node holds (fingerprints and account, never a
token), hands its grant over only when the manager asks, watches its
binding and fetches the token on a newer generation, and writes
access-token-only. Its ask now reads the runtime's answer as a value and
addresses seats as seats.
2026-10-04 12:18:51 +02:00
42 changed files with 4654 additions and 1229 deletions
+11 -4
View File
@@ -37,14 +37,15 @@ must see, a node that joins later included — kept, so it carries no secret eit
| what | how |
|---|---|
| the licence manager rotated a licence, or switched this node | its `licence.rotated` / `licence.switched` event; this module then asks `anthropic-licence-manager.current` for its token, sealed to the key it sends |
| this node starts | it asks `current` once, so a node that was off catches up |
| a person ran `/login` here | the credentials file gains a refresh token this module never writes; it asks `anthropic-licence-manager.adopt` at once with the grant sealed to the manager's key — the one time a refresh token travels, because the login made the manager's stale |
| what this node holds | the module's `holdings` state, one key for this node — the account, the kind, fingerprints and expiries, never a token — written at start and whenever the credentials file changes (hq ADR 0206) |
| a person ran `/login` here | the credentials file gains a refresh token this module never writes; its next report shows it, and the licence manager asks `claude_code_grant` for it, giving its key — the one time a refresh token leaves the node, for the manager to adopt by refreshing it |
| what this node should hold | the licence manager's `bindings` state, this node's key; on a newer generation this module asks `anthropic-licence-manager.current` for its token, sealed to the key it sends, and writes it access-token-only — so the agent here never refreshes. A node that was off reads its key when it is back |
| an MCP server registered through this module | a key in the module's `servers` state — `all.<server>` for every node, `<node>.<server>` for one; every node watches it and renders what applies to it, a node's own entry over the one for every node. A node that joins later, or was off, reads the whole current set at start; unregistering is a delete. An entry with a secret in its `env` or `headers` is refused by the runtime |
## Tools
`claude_code_status`, `claude_code_render`, `claude_code_pull`, `claude_code_mcp_list`,
`claude_code_status`, `claude_code_render`, `claude_code_pull`, `claude_code_grant` (for the licence
manager), `claude_code_mcp_list`,
`claude_code_mcp_register` (this node by default; `nodes: "all"` or a list for more — called for this
node alone, its answer names the other nodes running claude-code), `claude_code_mcp_unregister`.
@@ -72,3 +73,9 @@ Remove these by hand, once; the mesh removes nothing it did not make (ADR 0182):
Writing `/etc/claude-code` needs root. The runtime runs as the operator account, and the module uses
that account's passwordless `sudo`; on a machine without it, `claude_code_render` says so and nothing
is written.
## Code
Go, one binary (`cmd/claude-code`) the node's runtime launches. Tested with `go test ./...`; the managed
instruction file is held to the TypeScript renderer it replaced (`testdata/rendered-by-typescript.json`),
and the sealed box is the licence manager's own format.
@@ -0,0 +1,364 @@
package main
import (
"encoding/json"
"os"
"path/filepath"
"reflect"
"strings"
"testing"
"time"
)
var now = time.Now().UnixMilli()
func node(t *testing.T, name string) (Paths, map[string]string) {
t.Helper()
root := t.TempDir()
p := Paths{State: filepath.Join(root, "state"), Facts: filepath.Join(root, "state", "facts.json"),
Settings: filepath.Join(root, "state", "settings.json"), Home: filepath.Join(root, "home"), Node: name}
_ = os.MkdirAll(p.State, 0o700)
_ = os.MkdirAll(filepath.Join(p.Home, ".claude"), 0o700)
_ = os.WriteFile(p.Facts, []byte(`{"node":"`+name+`","console":"http://127.0.0.1:4270/mcp"}`), 0o600)
_ = os.WriteFile(p.Settings, []byte(`{"role":"","mcp_servers":{}}`), 0o600)
return p, map[string]string{}
}
func writer(w map[string]string) WriteManaged {
return func(name, content string) (string, error) { w[name] = content; return name + ": written", nil }
}
func writeFile(t *testing.T, path, content string) {
t.Helper()
if err := os.WriteFile(path, []byte(content), 0o600); err != nil {
t.Fatal(err)
}
}
func creds(t *testing.T, p Paths) map[string]any {
t.Helper()
var c map[string]any
raw, _ := os.ReadFile(p.credentials())
if err := json.Unmarshal(raw, &c); err != nil {
t.Fatal(err)
}
return c["claudeAiOauth"].(map[string]any)
}
// ---- the renderer, held to the TypeScript it replaced -------------------------------------------------
func TestTheRendererWritesWhatTheTypeScriptOneWrote(t *testing.T) {
raw, err := os.ReadFile("testdata/rendered-by-typescript.json")
if err != nil {
t.Fatal(err)
}
var f struct {
Facts Facts `json:"facts"`
Settings Settings `json:"settings"`
Registered Servers `json:"registered"`
WithKey map[string]string `json:"withKey"`
Plain map[string]string `json:"plain"`
}
if err := json.Unmarshal(raw, &f); err != nil {
t.Fatal(err)
}
same := func(label string, got, want map[string]string) {
if got["CLAUDE.md"] != want["CLAUDE.md"] {
t.Errorf("%s: CLAUDE.md differs from the TypeScript's:\n--- go\n%s\n--- typescript\n%s", label, got["CLAUDE.md"], want["CLAUDE.md"])
}
for _, file := range []string{"managed-mcp.json", "managed-settings.json"} {
var a, b any
_ = json.Unmarshal([]byte(got[file]), &a)
_ = json.Unmarshal([]byte(want[file]), &b)
if !reflect.DeepEqual(a, b) {
t.Errorf("%s: %s means something else:\n--- go\n%s\n--- typescript\n%s", label, file, got[file], want[file])
}
}
}
same("with an API key", Render(f.Facts, f.Settings, &Binding{Licence: "api", Kind: "api-key"}, "/state/api-key-helper", f.Registered), f.WithKey)
same("plain", Render(f.Facts, Settings{}, nil, "/h", Servers{}), f.Plain)
}
func TestASettingCannotReplaceTheMeshsOwnEntryAndABadNameIsLeftOut(t *testing.T) {
out := Render(Facts{Node: "w", Console: "http://127.0.0.1:4270/mcp"},
Settings{MCPServers: map[string]map[string]any{"mesh": {"type": "http", "url": "http://evil"}, "bad name": {}}}, nil, "/h", nil)
var mcp struct {
MCPServers map[string]map[string]any `json:"mcpServers"`
}
_ = json.Unmarshal([]byte(out["managed-mcp.json"]), &mcp)
if mcp.MCPServers["mesh"]["url"] != "http://127.0.0.1:4270/mcp" || mcp.MCPServers["bad name"] != nil {
t.Fatalf("%v", mcp.MCPServers)
}
if !reflect.DeepEqual(Render(Facts{Console: "x"}, Settings{}, nil, "/h", nil), Render(Facts{Console: "x"}, Settings{}, nil, "/h", nil)) {
t.Fatal("rendering is not deterministic")
}
}
// ---- the credentials file -----------------------------------------------------------------------------
func i64(v int64) *int64 { return &v }
func TestTheLineageRules(t *testing.T) {
const hour = 3_600_000
g := func(at string, exp int64, rtExp int64) Grant {
return Grant{AccessToken: at, ExpiresAt: exp, RefreshTokenExpiresAt: i64(rtExp)}
}
month := now + 30*24*hour
if d := DecideApply(&Grant{AccessToken: "A", ExpiresAt: now + hour, RefreshTokenExpiresAt: i64(month)}, g("B", now+2*hour, month), false); !d.Apply {
t.Fatal("a newer rotation was refused")
}
if d := DecideApply(&Grant{AccessToken: "new", ExpiresAt: now + 2*hour, RefreshTokenExpiresAt: i64(month)}, g("old", now+hour, month), false); d.Apply || d.Reason != "not-newer" {
t.Fatalf("a late older rotation: %+v", d)
}
if d := DecideApply(&Grant{AccessToken: "A", ExpiresAt: now + 8*hour, RefreshTokenExpiresAt: i64(month)}, g("re", now+hour, now+5*24*hour), false); !d.Apply || !d.Reissued {
t.Fatalf("a re-issued grant: %+v", d)
}
if d := DecideApply(&Grant{AccessToken: "A", ExpiresAt: now + 8*hour}, g("other", now+hour, month), true); !d.Apply {
t.Fatal("a switch was refused")
}
if d := DecideApply(&Grant{AccessToken: "A"}, Grant{AccessToken: "A"}, true); d.Apply || d.Reason != "already-current" {
t.Fatalf("the same token: %+v", d)
}
}
func TestALoginIsSeenAndStrippedWhenTheNodesOwnGrantIsWritten(t *testing.T) {
p, _ := node(t, "laptop")
writeFile(t, p.credentials(), `{"claudeAiOauth":{"accessToken":"at-login","refreshToken":"rt-login","expiresAt":1700000000000},"other":1}`)
login := ReadCredentials(p.credentials())
if !HoldsLogin(login) {
t.Fatal("a login was not seen")
}
if err := WriteCredentials(p.credentials(), WithGrant(login, Grant{AccessToken: "at-mesh", ExpiresAt: 1, Scopes: []string{"user:inference"}})); err != nil {
t.Fatal(err)
}
back := ReadCredentials(p.credentials())
raw, _ := os.ReadFile(p.credentials())
info, _ := os.Stat(p.credentials())
if HoldsLogin(back) || GrantOf(back).AccessToken != "at-mesh" || back["other"] == nil || strings.Contains(string(raw), "rt-login") || info.Mode().Perm() != 0o600 {
t.Fatalf("written %s (mode %v)", raw, info.Mode())
}
}
func TestTheAccountIsReadFromTheAgentsStateFileAndNeverGuessed(t *testing.T) {
p, _ := node(t, "laptop")
writeFile(t, p.account(), `{"oauthAccount":{"accountUuid":"u-1","emailAddress":"a@example.org"},"other":2}`)
if id := ReadIdentity(p.account()); id == nil || id.AccountUUID != "u-1" || id.EmailAddress != "a@example.org" {
t.Fatalf("%+v", id)
}
if ReadIdentity("/nonexistent/.claude.json") != nil {
t.Fatal("an identity from nothing")
}
writeFile(t, p.account(), `{}`)
if ReadIdentity(p.account()) != nil {
t.Fatal("an identity from an empty file")
}
}
// ---- the licence, ADR 0206 ----------------------------------------------------------------------------
func TestWhatANodeHoldsIsReportedWithFingerprintsAndItsAccountNeverAToken(t *testing.T) {
p, _ := node(t, "laptop")
writeFile(t, p.credentials(), `{"claudeAiOauth":{"accessToken":"at-secret","refreshToken":"rt-secret","expiresAt":2000,"refreshTokenExpiresAt":9000}}`)
writeFile(t, p.account(), `{"oauthAccount":{"accountUuid":"u-1","emailAddress":"a@example.org"}}`)
h := HoldingsOf(p)
if h.Node != "laptop" || h.Identity.AccountUUID != "u-1" || *h.Kind != "subscription" || !h.Refresh.Present ||
!strings.HasPrefix(*h.Refresh.Fingerprint, "sha256:") || h.Access.ExpiresAt != 2000 || h.ChangedAt == nil {
t.Fatalf("%+v", h)
}
raw, _ := json.Marshal(h)
if strings.Contains(string(raw), "at-secret") || strings.Contains(string(raw), "rt-secret") {
t.Fatalf("a token is in the report: %s", raw)
}
var keys map[string]any
_ = json.Unmarshal(raw, &keys)
for k := range keys {
if strings.Contains(strings.ToLower(k), "token") || strings.Contains(strings.ToLower(k), "secret") {
t.Fatalf("a field the runtime would refuse: %s", k)
}
}
// The manager reads exactly this shape.
if _, err := time.Parse(time.RFC3339Nano, *h.ChangedAt); err != nil {
t.Fatalf("the manager cannot read the report's time: %v", err)
}
}
func TestTheGrantAnswersOnlyAWaitingLoginSealedToTheManagersKey(t *testing.T) {
p, _ := node(t, "laptop")
manager, _ := GenerateKeyPair()
if a, _ := GrantFor(p, manager.PublicKey); a.Sealed != nil || a.Waiting == nil || *a.Waiting {
t.Fatalf("%+v", a)
}
writeFile(t, p.credentials(), `{"claudeAiOauth":{"accessToken":"at","refreshToken":"rt-login","expiresAt":1}}`)
writeFile(t, p.account(), `{"oauthAccount":{"accountUuid":"u-9"}}`)
a, err := GrantFor(p, manager.PublicKey)
if err != nil || a.Identity.AccountUUID != "u-9" {
t.Fatalf("%+v %v", a, err)
}
plain, _ := Open(*a.Sealed, manager.PrivateKey)
if !strings.Contains(plain, `"refreshToken":"rt-login"`) {
t.Fatalf("opened %s", plain)
}
raw, _ := json.Marshal(a)
if strings.Contains(string(raw), "rt-login") {
t.Fatal("the refresh token crossed in the clear")
}
}
// seat answers `current` as the manager does: the grant sealed to the key the node sent.
func seat(t *testing.T, licence, token string, gen int64, asked *[]string) Ask {
return func(address string, args any) (json.RawMessage, error) {
*asked = append(*asked, address)
key := args.(map[string]any)["public_key"].(string)
g, _ := json.Marshal(Grant{AccessToken: token, ExpiresAt: now + 3_600_000})
box, err := Seal(string(g), key)
if err != nil {
t.Fatal(err)
}
return json.Marshal(Current{Licence: licence, Kind: "subscription", Generation: gen, Sealed: &box})
}
}
func TestANewerGenerationFetchesTheTokenOnceByTheSeatsVerb(t *testing.T) {
p, w := node(t, "laptop")
var asked []string
ask := seat(t, "personal", "at-1", 3, &asked)
if _, err := OnBinding(p, &BindingState{Licence: "personal", Kind: "subscription", Generation: 3}, ask, writer(w)); err != nil {
t.Fatal(err)
}
if len(asked) != 1 || asked[0] != "seat:anthropic-licence-manager.current" || creds(t, p)["accessToken"] != "at-1" {
t.Fatalf("asked %v, credentials %v", asked, creds(t, p))
}
if done, _ := OnBinding(p, &BindingState{Licence: "personal", Kind: "subscription", Generation: 3}, ask, writer(w)); done != "" || len(asked) != 1 {
t.Fatal("an equal generation asked again")
}
if HoldingsOf(p).Generation != 3 || w["managed-mcp.json"] == "" {
t.Fatal("the generation or the managed files were not written")
}
}
func TestTheTokenANodeIsHandedReplacesALoginsGrantAndLeavesNoRefreshToken(t *testing.T) {
p, w := node(t, "laptop")
writeFile(t, p.credentials(), `{"claudeAiOauth":{"accessToken":"at-old","refreshToken":"rt-spent","expiresAt":`+
strings.TrimSpace(string(mustJSON(now+7_200_000)))+`}}`)
var asked []string
out, err := Pull(p, seat(t, "personal", "at-new", 1, &asked), writer(w))
if err != nil || out["applied"] != true {
t.Fatalf("%v %v", out, err)
}
c := creds(t, p)
if c["accessToken"] != "at-new" || c["refreshToken"] != nil || HoldingsOf(p).Refresh.Present {
t.Fatalf("%v", c)
}
}
func mustJSON(v any) []byte { b, _ := json.Marshal(v); return b }
// ---- MCP servers in state, ADR 0201 -------------------------------------------------------------------
// bus is the `servers` state as every node in a test shares it, with each node's watch.
type bus struct {
kept map[string]map[string]any
watchers []func(ServerChange)
}
func (b *bus) Put(key string, value any) error {
v := value.(map[string]any)
b.kept[key] = v
for _, w := range b.watchers {
w(ServerChange{Key: key, Op: "put", Value: v})
}
return nil
}
func (b *bus) Delete(key string) error {
delete(b.kept, key)
for _, w := range b.watchers {
w(ServerChange{Key: key, Op: "delete"})
}
return nil
}
func (b *bus) Keys() ([]string, error) {
var out []string
for k := range b.kept {
out = append(out, k)
}
return out, nil
}
// join is a node joining: its view takes the current state, then every change.
func (b *bus) join(p Paths, w map[string]string) *ServerView {
v := NewServerView(p)
for k, val := range b.kept {
_, _ = OnServerChange(v, ServerChange{Key: k, Op: "put", Value: val}, p, writer(w))
}
b.watchers = append(b.watchers, func(c ServerChange) { _, _ = OnServerChange(v, c, p, writer(w)) })
return v
}
func noOthers() ([]string, error) { return nil, nil }
func TestRegisteringHerePutsItUnderThisNodesKeyAndAsksAboutTheOthers(t *testing.T) {
p, w := node(t, "laptop")
b := &bus{kept: map[string]map[string]any{}}
v := b.join(p, w)
r, err := RegisterServer(p, Registration{Name: "search", Entry: map[string]any{"type": "http", "url": "https://s.example/mcp"}}, b, v, writer(w),
func() ([]string, error) { return []string{"laptop", "server", "desktop"}, nil })
if err != nil || r["here"] != "changed" || !strings.Contains(r["also"].(string), "server, desktop") || b.kept["laptop.search"] == nil {
t.Fatalf("%v %v %v", r, err, b.kept)
}
if !strings.Contains(w["managed-mcp.json"], `"search"`) {
t.Fatal("not rendered")
}
}
func TestEveryNodeRegistrationReachesTheOthersAndALateNodeReadsIt(t *testing.T) {
a, wa := node(t, "laptop")
s, ws := node(t, "server")
b := &bus{kept: map[string]map[string]any{}}
va := b.join(a, wa)
b.join(s, ws)
_, _ = RegisterServer(a, Registration{Name: "docs", Entry: map[string]any{"type": "stdio", "command": "docs-mcp"}, Nodes: []string{"all"}}, b, va, writer(wa), noOthers)
if Registered(s)["docs"] == nil || !strings.Contains(ws["managed-mcp.json"], "docs-mcp") {
t.Fatalf("the other node did not take it: %v", Registered(s))
}
late, wl := node(t, "desktop")
b.join(late, wl)
if Registered(late)["docs"] == nil {
t.Fatal("a node joining later did not read the current set")
}
_, _ = RegisterServer(a, Registration{Name: "docs", Nodes: []string{"all"}}, b, va, writer(wa), noOthers)
if Registered(s)["docs"] != nil || Registered(late)["docs"] != nil {
t.Fatal("an unregistration did not reach every node")
}
}
func TestANodesOwnRegistrationOverridesTheOneForEveryNode(t *testing.T) {
a, wa := node(t, "laptop")
s, ws := node(t, "server")
b := &bus{kept: map[string]map[string]any{}}
va := b.join(a, wa)
b.join(s, ws)
_, _ = RegisterServer(a, Registration{Name: "x", Entry: map[string]any{"type": "http", "url": "https://all"}, Nodes: []string{"all"}}, b, va, writer(wa), noOthers)
_, _ = RegisterServer(a, Registration{Name: "x", Entry: map[string]any{"type": "http", "url": "https://laptop"}}, b, va, writer(wa), noOthers)
if Registered(a)["x"]["url"] != "https://laptop" || Registered(s)["x"]["url"] != "https://all" {
t.Fatalf("%v %v", Registered(a), Registered(s))
}
r, _ := RegisterServer(a, Registration{Name: "x"}, b, va, writer(wa), noOthers)
if !strings.Contains(r["still"].(string), "still applies here") || Registered(a)["x"]["url"] != "https://all" {
t.Fatalf("%v", r)
}
}
func TestABadEntryIsRefusedBeforeAnythingIsPut(t *testing.T) {
p, w := node(t, "laptop")
b := &bus{kept: map[string]map[string]any{}}
v := b.join(p, w)
r, _ := RegisterServer(p, Registration{Name: "mesh", Entry: map[string]any{"type": "http", "url": "https://x"}}, b, v, writer(w), noOthers)
if r["registered"] != false || len(b.kept) != 0 {
t.Fatalf("%v %v", r, b.kept)
}
if done, _ := OnServerChange(v, ServerChange{Key: "server.b", Op: "put", Value: map[string]any{"type": "http", "url": "https://b"}}, p, writer(w)); done != "" {
t.Fatal("another node's key changed this one")
}
}
@@ -0,0 +1,198 @@
package main
// The agent's credentials file, and whether an offered grant may replace what it holds (novox/hq ADR 0183,
// ADR 0206, design 36 §5). Pure where it decides, so the rules are tested without a file.
//
// The file is the vendor's: `{ claudeAiOauth: { accessToken, expiresAt, refreshTokenExpiresAt?, scopes?,
// subscriptionType?, rateLimitTier? }, ... }`. A node bound to a licence never holds a refresh token, so
// the one this module writes never carries one; a refresh token found there is a person's login.
//
// The lineage rule is the predecessor's, with the incidents that earned it: a rotation of the same licence
// is applied only if newer; a grant re-issued by a login is adopted whatever its expiry; a switch to another
// licence is applied regardless, because across licences the expiries are unrelated numbers.
import (
"bytes"
"encoding/json"
"math"
"os"
"path/filepath"
)
// Grant is what the manager hands a node: an access token and its expiries, never a refresh token.
type Grant struct {
AccessToken string `json:"accessToken"`
ExpiresAt int64 `json:"expiresAt"`
RefreshTokenExpiresAt *int64 `json:"refreshTokenExpiresAt,omitempty"`
Scopes []string `json:"scopes,omitempty"`
SubscriptionType string `json:"subscriptionType,omitempty"`
RateLimitTier string `json:"rateLimitTier,omitempty"`
}
// Decision is whether a handed grant is applied, and why not.
type Decision struct {
Apply bool
Reissued bool
Reason string // already-current | not-newer
}
// generationTolerance: two refresh-token expiries within a day are one lineage; a login starts a fresh
// window weeks away.
const generationTolerance = 24 * 60 * 60 * 1000
func sameGeneration(a, b *int64) bool {
if a == nil || b == nil {
return true
}
return math.Abs(float64(*a-*b)) <= generationTolerance
}
// DecideApply says whether an offered grant replaces the one held; switch is a move to another licence.
func DecideApply(local *Grant, offered Grant, switching bool) Decision {
if local == nil || local.AccessToken == "" {
return Decision{Apply: true}
}
if local.AccessToken == offered.AccessToken {
return Decision{Reason: "already-current"}
}
reissued := !sameGeneration(local.RefreshTokenExpiresAt, offered.RefreshTokenExpiresAt)
if !switching && !reissued && local.ExpiresAt >= offered.ExpiresAt {
return Decision{Reason: "not-newer"}
}
return Decision{Apply: true, Reissued: reissued}
}
// Credentials is the file as found, every key kept — the vendor's other keys are not this module's.
type Credentials map[string]any
func (c Credentials) oauth() map[string]any {
o, _ := c["claudeAiOauth"].(map[string]any)
return o
}
// ReadCredentials reads the file, keeping numbers as written; nil when there is none.
func ReadCredentials(path string) Credentials {
raw, err := os.ReadFile(path)
if err != nil {
return nil
}
dec := json.NewDecoder(bytes.NewReader(raw))
dec.UseNumber()
var c Credentials
if dec.Decode(&c) != nil {
return nil
}
return c
}
func number(v any) (int64, bool) {
switch n := v.(type) {
case json.Number:
i, err := n.Int64()
if err != nil {
f, err := n.Float64()
return int64(f), err == nil
}
return i, true
case float64:
return int64(n), true
case int64:
return n, true
}
return 0, false
}
// GrantOf is the grant the file holds, or nil.
func GrantOf(c Credentials) *Grant {
o := c.oauth()
at, _ := o["accessToken"].(string)
if at == "" {
return nil
}
g := &Grant{AccessToken: at}
g.ExpiresAt, _ = number(o["expiresAt"])
if v, ok := number(o["refreshTokenExpiresAt"]); ok {
g.RefreshTokenExpiresAt = &v
}
return g
}
// HoldsLogin says the file holds a refresh token — which this module never writes, so a person's login.
func HoldsLogin(c Credentials) bool {
rt, _ := c.oauth()["refreshToken"].(string)
return rt != ""
}
// RefreshTokenOf is the refresh token a login left, or "".
func RefreshTokenOf(c Credentials) string {
rt, _ := c.oauth()["refreshToken"].(string)
return rt
}
// WithGrant lays the handed grant over what is there, and deletes any refresh token.
func WithGrant(local Credentials, g Grant) Credentials {
next := Credentials{}
for k, v := range local {
next[k] = v
}
oauth := map[string]any{}
for k, v := range local.oauth() {
oauth[k] = v
}
oauth["accessToken"] = g.AccessToken
oauth["expiresAt"] = g.ExpiresAt
if g.RefreshTokenExpiresAt != nil {
oauth["refreshTokenExpiresAt"] = *g.RefreshTokenExpiresAt
}
if len(g.Scopes) > 0 {
oauth["scopes"] = g.Scopes
}
if g.SubscriptionType != "" {
oauth["subscriptionType"] = g.SubscriptionType
}
if g.RateLimitTier != "" {
oauth["rateLimitTier"] = g.RateLimitTier
}
delete(oauth, "refreshToken")
next["claudeAiOauth"] = oauth
return next
}
// ReplacedBy is the handed grant in place of the old licence's, whole — scopes and subscription included;
// only keys outside the grant stay. No refresh token survives.
func ReplacedBy(local Credentials, g Grant) Credentials {
next := Credentials{}
for k, v := range local {
if k != "claudeAiOauth" {
next[k] = v
}
}
return WithGrant(next, g)
}
// WriteCredentials writes atomically at 0600: a partial credentials file must never be read as a whole one.
func WriteCredentials(path string, c Credentials) error {
if err := os.MkdirAll(filepath.Dir(path), 0o700); err != nil {
return err
}
raw, err := indented(c)
if err != nil {
return err
}
tmp := path + ".mesh-tmp"
if err := os.WriteFile(tmp, raw, 0o600); err != nil {
return err
}
return os.Rename(tmp, path)
}
// indented is JSON as the agent's own files are written: two-space indent, a trailing newline, nothing
// escaped that need not be.
func indented(v any) ([]byte, error) {
var b bytes.Buffer
enc := json.NewEncoder(&b)
enc.SetEscapeHTML(false)
enc.SetIndent("", " ")
err := enc.Encode(v)
return b.Bytes(), err
}
@@ -0,0 +1,84 @@
package main
// Which account the agent is logged in as (novox/hq ADR 0183): not in the token, but in the agent's own
// state file beside the home, `~/.claude.json` → `oauthAccount`. Read to report and attribute a login;
// written, three keys and nothing else, when a licence is switched, so the account Claude Code shows is
// the one whose token it now holds.
import (
"bytes"
"encoding/json"
"os"
)
// Identity is an account as the agent's state file names it.
type Identity struct {
AccountUUID string `json:"accountUuid"`
EmailAddress string `json:"emailAddress,omitempty"`
OrganizationUUID string `json:"organizationUuid,omitempty"`
}
func readState(path string) (map[string]any, bool) {
raw, err := os.ReadFile(path)
if err != nil {
return nil, false
}
dec := json.NewDecoder(bytes.NewReader(raw))
dec.UseNumber()
var m map[string]any
if dec.Decode(&m) != nil || m == nil {
return nil, false
}
return m, true
}
// ReadIdentity is the account the state file names, or nil — never a guess.
func ReadIdentity(path string) *Identity {
m, ok := readState(path)
if !ok {
return nil
}
a, _ := m["oauthAccount"].(map[string]any)
uuid, _ := a["accountUuid"].(string)
if uuid == "" {
return nil
}
id := &Identity{AccountUUID: uuid}
id.EmailAddress, _ = a["emailAddress"].(string)
id.OrganizationUUID, _ = a["organizationUuid"].(string)
return id
}
// WriteIdentity points the state file's account at id, keeping every other key as found; answers whether
// the file changed. A file that is there and cannot be read as an object is left alone.
func WriteIdentity(path string, id Identity) (bool, error) {
m, ok := readState(path)
if !ok {
if _, err := os.Stat(path); err == nil {
return false, nil
}
m = map[string]any{}
}
current, _ := m["oauthAccount"].(map[string]any)
if current == nil {
current = map[string]any{}
}
e, _ := current["emailAddress"].(string)
o, _ := current["organizationUuid"].(string)
if current["accountUuid"] == id.AccountUUID && e == id.EmailAddress && o == id.OrganizationUUID {
return false, nil
}
current["accountUuid"] = id.AccountUUID
current["emailAddress"] = id.EmailAddress
current["organizationUuid"] = id.OrganizationUUID
m["oauthAccount"] = current
raw, err := indented(m)
if err != nil {
return false, err
}
tmp := path + ".mesh-tmp"
if err := os.WriteFile(tmp, raw, 0o600); err != nil {
return false, err
}
return true, os.Rename(tmp, path)
}
@@ -0,0 +1,12 @@
package main
// instructionsText is the managed instruction file, generated from the TypeScript renderer it replaced so
// the file under the agent's managed directory did not change by a byte when the module moved to Go;
// a test holds it to that renderer's own output (testdata/rendered-by-typescript.json).
func instructionsText(node, role string) string {
return "# This machine is a node of a Novox mesh\n\nWritten by the mesh's `claude-code` module. Edit the module's settings or the catalogue, never this file:\nit is rewritten whenever the module renders.\n\n## Who this node is\n\n- **Node:** `" +
node +
"`\n- **Role:** " +
role +
"\n- The other nodes, their roles and what runs where: ask the controller (`mesh-controller.nodes`,\n `mesh-controller.node`). Nothing here lists them, because a copy drifts.\n\n## How a session on this mesh works\n\nThe console is the only way to the mesh: the MCP server named `mesh`. It offers five tools, and\neverything else is an address you find and call through them:\n\n- `mesh_search` — words in, matching addresses out. `mesh_describe` — one address's arguments.\n- `mesh_call` — call an address. A seat the mesh holds once is `<seat>.<verb>` (the mesh's own verbs\n are `mesh-controller.<verb>`: `status`, `plan`, `node`, `assign`, `push`, `settings`);\n a module on a machine is `<node>/<module>.<tool>`.\n- `mesh_overview` and `mesh_machine` — the mesh's seats and machines, and what one machine runs.\n\n- **Symptom first.** For an error, a failing service or anything unexpected, search the record with the\n literal text before forming a hypothesis: the records module's `records_search`, then\n `records_read`.\n- **Ask the mesh before changing it**, and change it through the controller's verbs or the catalogue.\n- **A licence** through the `anthropic-licence-manager` seat's verbs. Never edit the agent's credentials\n file by hand, never print or ask for a token.\n\n## Hard rules\n\n- A file the mesh manages is changed through the verb or the catalogue that owns it, never on disk. If\n unsure, `mesh-controller.plan` for the node says what the mesh writes there.\n- Never write to a store's database by hand; schema changes are numbered migrations.\n- Never push to a main branch: a branch, a pull request, and a human approval for every merge.\n- The mesh creates no symlinks, and nobody else does either.\n- A package is declared in a module, never installed by hand.\n\n## Conventions\n\n- Commit messages are concise, in the imperative, about why.\n- Test before pushing: nodes update unattended.\n- The playbooks in the record say how research, decisions, designs, issues and hand-offs are done.\n"
}
+365
View File
@@ -0,0 +1,365 @@
// 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)
}
+529
View File
@@ -0,0 +1,529 @@
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) }
@@ -0,0 +1,121 @@
package main
// What the module writes into the agent's machine-wide managed directory (novox/hq design 36 §1–§4).
// Pure: composed from the facts the mesh rendered, the settings the operator set and the licence the node
// holds, so what lands under /etc is tested without a machine.
//
// Three files, owned whole by this module:
//
// managed-mcp.json the tool servers every session loads: the mesh's console as `mesh`, and the
// servers the operator declared or registered through this module. Exclusive by
// the vendor's rule — a server not listed here does not load (operator's choice,
// 2026-10-03).
// managed-settings.json the mesh's keys only: the repositories' attribution convention, the claude.ai
// connectors kept beside the managed servers, and — for an API-key licence only —
// the key-helper. A person's preferences are theirs.
// CLAUDE.md how a session on this mesh works, who this node is, the conventions.
import (
"bytes"
"encoding/json"
"fmt"
"regexp"
"strings"
)
// ManagedDir is the agent's machine-wide managed directory.
const ManagedDir = "/etc/claude-code"
const meshEntry = "mesh"
// Facts are what the mesh rendered for this node.
type Facts struct {
Node string `json:"node"`
Console string `json:"console"`
}
// Settings are the operator's, for the mesh or this node.
type Settings struct {
Role string `json:"role"`
MCPServers map[string]map[string]any `json:"mcp_servers"`
}
// Binding is the licence this node holds, as it was last applied.
type Binding struct {
Licence string `json:"licence"`
Kind string `json:"kind"` // subscription | api-key
Generation int64 `json:"generation,omitempty"`
}
// Servers are tool server entries by name, in the vendor's `.mcp.json` shape.
type Servers map[string]map[string]any
var serverName = regexp.MustCompile(`^[A-Za-z0-9_-]+$`)
// EntryProblem says why the vendor's managed file would not take an entry, or "" when it would: a name of
// letters, digits, `-` and `_`, and an http/sse server with a url or a stdio server with a command.
func EntryProblem(name string, entry map[string]any) string {
if !serverName.MatchString(name) {
return fmt.Sprintf("%q is not a name the agent takes: letters, digits, - and _", name)
}
if name == meshEntry {
return fmt.Sprintf("%q is the mesh's own entry", meshEntry)
}
kind, _ := entry["type"].(string)
if kind == "" {
kind = "stdio"
}
switch kind {
case "http", "sse", "streamable-http":
if u, _ := entry["url"].(string); u != "" {
return ""
}
return fmt.Sprintf("an %s server needs a url", kind)
case "stdio":
if c, _ := entry["command"].(string); c != "" {
return ""
}
return "a stdio server needs a command"
}
return fmt.Sprintf("%q is not a server type the agent knows (http, sse, stdio)", kind)
}
// jsonFile is a value as the managed files are written: two-space indent, a trailing newline, nothing
// escaped that need not be.
func jsonFile(v any) string {
var b bytes.Buffer
enc := json.NewEncoder(&b)
enc.SetEscapeHTML(false)
enc.SetIndent("", " ")
_ = enc.Encode(v)
return b.String()
}
// Render composes the three files. registered — what was registered through this module and applies
// here — is laid over the servers the operator set in its settings.
func Render(facts Facts, settings Settings, binding *Binding, helperPath string, registered Servers) map[string]string {
servers := map[string]any{}
for _, layer := range []map[string]map[string]any{settings.MCPServers, registered} {
for name, entry := range layer {
if EntryProblem(name, entry) != "" {
continue // the mesh's own entry, or one the agent would refuse
}
servers[name] = entry
}
}
servers[meshEntry] = map[string]any{"type": "http", "url": facts.Console}
managed := map[string]any{"attribution": map[string]any{"commit": "", "pr": ""}, "allowAllClaudeAiMcps": true}
if binding != nil && binding.Kind == "api-key" {
managed["apiKeyHelper"] = helperPath
}
role := strings.TrimSpace(settings.Role)
if role == "" {
role = "not stated — set it in this module's settings for the node"
}
return map[string]string{
"managed-mcp.json": jsonFile(map[string]any{"mcpServers": servers}),
"managed-settings.json": jsonFile(managed),
"CLAUDE.md": instructionsText(facts.Node, role),
}
}
+189
View File
@@ -0,0 +1,189 @@
package main
// Sealing to one recipient (novox/hq ADR 0183, ADR 0206): the manager seals what it hands a consumer to
// the key that consumer sent, and a node seals a waiting login to the key the manager gives. The same box
// the agent module's TypeScript makes and opens, byte for byte — X25519 for the agreement, HKDF-SHA256 for
// the key, AES-256-GCM for the box — so `testdata/sealed-by-typescript.json` is opened here, and a test
// reopens what this seals with the same derivation.
//
// A box is `{ v: 1, eph, iv, tag, ct }`, every field base64; `eph` is the one-time public key as SPKI DER,
// and the key is bound to it and to the recipient's raw public key, so a box cannot be re-addressed.
import (
"crypto/aes"
"crypto/cipher"
"crypto/ecdh"
"crypto/hkdf"
"crypto/rand"
"crypto/sha256"
"crypto/x509"
"encoding/base64"
"encoding/pem"
"errors"
"fmt"
)
// SealedBox is a value sealed to one recipient.
type SealedBox struct {
V int `json:"v"`
Eph string `json:"eph"`
IV string `json:"iv"`
Tag string `json:"tag"`
Ct string `json:"ct"`
}
// KeyPair is a recipient's keypair as the two PEM strings it is kept and sent as.
type KeyPair struct {
PublicKey string `json:"publicKey"`
PrivateKey string `json:"privateKey"`
}
const sealInfo = "novox-mesh sealed box v1"
// GenerateKeyPair makes an X25519 keypair, PEM-encoded as the agent module's are.
func GenerateKeyPair() (KeyPair, error) {
priv, err := ecdh.X25519().GenerateKey(rand.Reader)
if err != nil {
return KeyPair{}, err
}
pubDER, err := x509.MarshalPKIXPublicKey(priv.PublicKey())
if err != nil {
return KeyPair{}, err
}
privDER, err := x509.MarshalPKCS8PrivateKey(priv)
if err != nil {
return KeyPair{}, err
}
return KeyPair{
PublicKey: string(pem.EncodeToMemory(&pem.Block{Type: "PUBLIC KEY", Bytes: pubDER})),
PrivateKey: string(pem.EncodeToMemory(&pem.Block{Type: "PRIVATE KEY", Bytes: privDER})),
}, nil
}
func publicFromPEM(p string) (*ecdh.PublicKey, error) {
block, _ := pem.Decode([]byte(p))
if block == nil {
return nil, errors.New("not a PEM public key")
}
k, err := x509.ParsePKIXPublicKey(block.Bytes)
if err != nil {
return nil, err
}
pub, ok := k.(*ecdh.PublicKey)
if !ok || pub.Curve() != ecdh.X25519() {
return nil, errors.New("not an X25519 public key")
}
return pub, nil
}
func privateFromPEM(p string) (*ecdh.PrivateKey, error) {
block, _ := pem.Decode([]byte(p))
if block == nil {
return nil, errors.New("not a PEM private key")
}
k, err := x509.ParsePKCS8PrivateKey(block.Bytes)
if err != nil {
return nil, err
}
priv, ok := k.(*ecdh.PrivateKey)
if !ok || priv.Curve() != ecdh.X25519() {
return nil, errors.New("not an X25519 private key")
}
return priv, nil
}
func boxKey(secret, ephDER, recipientRaw []byte) ([]byte, error) {
salt := append(append([]byte{}, ephDER...), recipientRaw...)
return hkdf.Key(sha256.New, secret, salt, sealInfo, 32)
}
// Seal seals plaintext to the recipient's public key.
func Seal(plaintext, recipientPEM string) (SealedBox, error) {
recipient, err := publicFromPEM(recipientPEM)
if err != nil {
return SealedBox{}, err
}
eph, err := ecdh.X25519().GenerateKey(rand.Reader)
if err != nil {
return SealedBox{}, err
}
secret, err := eph.ECDH(recipient)
if err != nil {
return SealedBox{}, err
}
ephDER, err := x509.MarshalPKIXPublicKey(eph.PublicKey())
if err != nil {
return SealedBox{}, err
}
key, err := boxKey(secret, ephDER, recipient.Bytes())
if err != nil {
return SealedBox{}, err
}
gcm, err := newGCM(key)
if err != nil {
return SealedBox{}, err
}
iv := make([]byte, 12)
if _, err := rand.Read(iv); err != nil {
return SealedBox{}, err
}
out := gcm.Seal(nil, iv, []byte(plaintext), nil)
ct, tag := out[:len(out)-gcm.Overhead()], out[len(out)-gcm.Overhead():]
b64 := base64.StdEncoding.EncodeToString
return SealedBox{V: 1, Eph: b64(ephDER), IV: b64(iv), Tag: b64(tag), Ct: b64(ct)}, nil
}
// Open opens a box with the recipient's private key; it fails for a box to another key or one tampered with.
func Open(box SealedBox, privatePEM string) (string, error) {
if box.V != 1 {
return "", errors.New("not a sealed box this module can open")
}
priv, err := privateFromPEM(privatePEM)
if err != nil {
return "", err
}
d := base64.StdEncoding.DecodeString
ephDER, err := d(box.Eph)
if err != nil {
return "", fmt.Errorf("the box's eph: %w", err)
}
ephKey, err := x509.ParsePKIXPublicKey(ephDER)
if err != nil {
return "", err
}
eph, ok := ephKey.(*ecdh.PublicKey)
if !ok {
return "", errors.New("the box's eph is not an X25519 key")
}
secret, err := priv.ECDH(eph)
if err != nil {
return "", err
}
key, err := boxKey(secret, ephDER, priv.PublicKey().Bytes())
if err != nil {
return "", err
}
iv, err1 := d(box.IV)
tag, err2 := d(box.Tag)
ct, err3 := d(box.Ct)
if err := errors.Join(err1, err2, err3); err != nil {
return "", err
}
gcm, err := newGCM(key)
if err != nil {
return "", err
}
plain, err := gcm.Open(nil, iv, append(ct, tag...), nil)
if err != nil {
return "", errors.New("the box does not open with this key")
}
return string(plain), nil
}
func newGCM(key []byte) (cipher.AEAD, error) {
block, err := aes.NewCipher(key)
if err != nil {
return nil, err
}
return cipher.NewGCM(block)
}
@@ -0,0 +1,42 @@
{
"facts": {
"node": "workstation",
"console": "http://127.0.0.1:4270/mcp"
},
"settings": {
"role": "the laptop",
"mcp_servers": {
"search": {
"type": "http",
"url": "https://s.example/mcp"
},
"docs": {
"type": "stdio",
"command": "docs-mcp",
"args": [
"--x"
]
}
}
},
"registered": {
"anton": {
"type": "stdio",
"command": "node",
"args": [
"/a/b.js"
],
"env": {}
}
},
"withKey": {
"managed-mcp.json": "{\n \"mcpServers\": {\n \"anton\": {\n \"type\": \"stdio\",\n \"command\": \"node\",\n \"args\": [\n \"/a/b.js\"\n ],\n \"env\": {}\n },\n \"docs\": {\n \"type\": \"stdio\",\n \"command\": \"docs-mcp\",\n \"args\": [\n \"--x\"\n ]\n },\n \"mesh\": {\n \"type\": \"http\",\n \"url\": \"http://127.0.0.1:4270/mcp\"\n },\n \"search\": {\n \"type\": \"http\",\n \"url\": \"https://s.example/mcp\"\n }\n }\n}\n",
"managed-settings.json": "{\n \"attribution\": {\n \"commit\": \"\",\n \"pr\": \"\"\n },\n \"allowAllClaudeAiMcps\": true,\n \"apiKeyHelper\": \"/state/api-key-helper\"\n}\n",
"CLAUDE.md": "# This machine is a node of a Novox mesh\n\nWritten by the mesh's `claude-code` module. Edit the module's settings or the catalogue, never this file:\nit is rewritten whenever the module renders.\n\n## Who this node is\n\n- **Node:** `workstation`\n- **Role:** the laptop\n- The other nodes, their roles and what runs where: ask the controller (`mesh-controller.nodes`,\n `mesh-controller.node`). Nothing here lists them, because a copy drifts.\n\n## How a session on this mesh works\n\nThe console is the only way to the mesh: the MCP server named `mesh`. It offers five tools, and\neverything else is an address you find and call through them:\n\n- `mesh_search` — words in, matching addresses out. `mesh_describe` — one address's arguments.\n- `mesh_call` — call an address. A seat the mesh holds once is `<seat>.<verb>` (the mesh's own verbs\n are `mesh-controller.<verb>`: `status`, `plan`, `node`, `assign`, `push`, `settings`);\n a module on a machine is `<node>/<module>.<tool>`.\n- `mesh_overview` and `mesh_machine` — the mesh's seats and machines, and what one machine runs.\n\n- **Symptom first.** For an error, a failing service or anything unexpected, search the record with the\n literal text before forming a hypothesis: the records module's `records_search`, then\n `records_read`.\n- **Ask the mesh before changing it**, and change it through the controller's verbs or the catalogue.\n- **A licence** through the `anthropic-licence-manager` seat's verbs. Never edit the agent's credentials\n file by hand, never print or ask for a token.\n\n## Hard rules\n\n- A file the mesh manages is changed through the verb or the catalogue that owns it, never on disk. If\n unsure, `mesh-controller.plan` for the node says what the mesh writes there.\n- Never write to a store's database by hand; schema changes are numbered migrations.\n- Never push to a main branch: a branch, a pull request, and a human approval for every merge.\n- The mesh creates no symlinks, and nobody else does either.\n- A package is declared in a module, never installed by hand.\n\n## Conventions\n\n- Commit messages are concise, in the imperative, about why.\n- Test before pushing: nodes update unattended.\n- The playbooks in the record say how research, decisions, designs, issues and hand-offs are done.\n"
},
"plain": {
"managed-mcp.json": "{\n \"mcpServers\": {\n \"mesh\": {\n \"type\": \"http\",\n \"url\": \"http://127.0.0.1:4270/mcp\"\n }\n }\n}\n",
"managed-settings.json": "{\n \"attribution\": {\n \"commit\": \"\",\n \"pr\": \"\"\n },\n \"allowAllClaudeAiMcps\": true\n}\n",
"CLAUDE.md": "# This machine is a node of a Novox mesh\n\nWritten by the mesh's `claude-code` module. Edit the module's settings or the catalogue, never this file:\nit is rewritten whenever the module renders.\n\n## Who this node is\n\n- **Node:** `workstation`\n- **Role:** not stated — set it in this module's settings for the node\n- The other nodes, their roles and what runs where: ask the controller (`mesh-controller.nodes`,\n `mesh-controller.node`). Nothing here lists them, because a copy drifts.\n\n## How a session on this mesh works\n\nThe console is the only way to the mesh: the MCP server named `mesh`. It offers five tools, and\neverything else is an address you find and call through them:\n\n- `mesh_search` — words in, matching addresses out. `mesh_describe` — one address's arguments.\n- `mesh_call` — call an address. A seat the mesh holds once is `<seat>.<verb>` (the mesh's own verbs\n are `mesh-controller.<verb>`: `status`, `plan`, `node`, `assign`, `push`, `settings`);\n a module on a machine is `<node>/<module>.<tool>`.\n- `mesh_overview` and `mesh_machine` — the mesh's seats and machines, and what one machine runs.\n\n- **Symptom first.** For an error, a failing service or anything unexpected, search the record with the\n literal text before forming a hypothesis: the records module's `records_search`, then\n `records_read`.\n- **Ask the mesh before changing it**, and change it through the controller's verbs or the catalogue.\n- **A licence** through the `anthropic-licence-manager` seat's verbs. Never edit the agent's credentials\n file by hand, never print or ask for a token.\n\n## Hard rules\n\n- A file the mesh manages is changed through the verb or the catalogue that owns it, never on disk. If\n unsure, `mesh-controller.plan` for the node says what the mesh writes there.\n- Never write to a store's database by hand; schema changes are numbered migrations.\n- Never push to a main branch: a branch, a pull request, and a human approval for every merge.\n- The mesh creates no symlinks, and nobody else does either.\n- A package is declared in a module, never installed by hand.\n\n## Conventions\n\n- Commit messages are concise, in the imperative, about why.\n- Test before pushing: nodes update unattended.\n- The playbooks in the record say how research, decisions, designs, issues and hand-offs are done.\n"
}
}
+5
View File
@@ -0,0 +1,5 @@
module claude-code
go 1.25.0
require git.novox.be/novox/mesh-sdk/go v0.1.7
+2
View File
@@ -0,0 +1,2 @@
git.novox.be/novox/mesh-sdk/go v0.1.7 h1:C0sTQmtTiyYH7bnqZb7PusXnqA37gKuT7Nqjn9gG47w=
git.novox.be/novox/mesh-sdk/go v0.1.7/go.mod h1:GFuZUElBZ9A++mxgIKo97aXXo+kV0uJ/UkbhQPPIbrY=
-112
View File
@@ -1,112 +0,0 @@
// The agent's credentials file, and whether an offered grant may replace what it holds (novox/hq
// ADR 0183, design 36 §5). Pure where it decides, so the rules are tested without a file.
//
// The file is the vendor's: `{ claudeAiOauth: { accessToken, expiresAt, refreshTokenExpiresAt?,
// scopes?, subscriptionType?, rateLimitTier? }, ... }`. A node never holds a refresh token, so the
// one this module writes never carries one, and a full grant a login left behind is stripped the
// moment the manager hands the node its own.
//
// The lineage rule is the predecessor's, with the incidents that earned it: a rotation of the same
// licence is applied only if newer; a grant re-issued by a login is adopted whatever its expiry; a
// switch to another licence is applied regardless, because across licences the expiries are
// unrelated numbers.
import { readFileSync, renameSync, writeFileSync, mkdirSync } from "node:fs";
import { dirname } from "node:path";
export interface Grant {
readonly accessToken: string;
readonly expiresAt: number;
readonly refreshTokenExpiresAt?: number | null;
readonly scopes?: readonly string[] | null;
readonly subscriptionType?: string | null;
readonly rateLimitTier?: string | null;
}
export type ApplySource = "rotation" | "switch";
export type ApplyDecision =
| { apply: true; reissued?: boolean }
| { apply: false; reason: "already-current" }
| { apply: false; reason: "not-newer"; localExpiresAt: number };
/** Two refresh-token expiries within a day are one lineage; a login starts a fresh window weeks away. */
export const GENERATION_TOLERANCE_MS = 24 * 60 * 60 * 1000;
export function sameGeneration(a?: number | null, b?: number | null): boolean {
if (a == null || b == null) return true;
return Math.abs(Number(a) - Number(b)) <= GENERATION_TOLERANCE_MS;
}
export function decideApply(local: Grant | null | undefined, offered: Grant, source: ApplySource): ApplyDecision {
if (!local?.accessToken) return { apply: true };
if (local.accessToken === offered.accessToken) return { apply: false, reason: "already-current" };
const reissued = !sameGeneration(local.refreshTokenExpiresAt, offered.refreshTokenExpiresAt);
if (source === "rotation" && !reissued && Number(local.expiresAt) >= Number(offered.expiresAt)) {
return { apply: false, reason: "not-newer", localExpiresAt: Number(local.expiresAt) };
}
return reissued ? { apply: true, reissued: true } : { apply: true };
}
type Oauth = Record<string, unknown> & { accessToken?: string; refreshToken?: string; expiresAt?: number };
type Credentials = Record<string, unknown> & { claudeAiOauth?: Oauth };
export function readCredentials(path: string): Credentials | null {
try {
const parsed = JSON.parse(readFileSync(path, "utf8")) as Credentials;
return parsed && typeof parsed === "object" ? parsed : null;
} catch {
return null;
}
}
/** The grant the file holds, or null. */
export function grantOf(creds: Credentials | null): Grant | null {
const o = creds?.claudeAiOauth;
if (!o?.accessToken) return null;
return {
accessToken: o.accessToken,
expiresAt: Number(o.expiresAt ?? 0),
refreshTokenExpiresAt: o.refreshTokenExpiresAt == null ? null : Number(o.refreshTokenExpiresAt),
};
}
/** Does the file hold a full grant — a refresh token this module never writes, so a person's login? */
export function holdsLogin(creds: Credentials | null): boolean {
return typeof creds?.claudeAiOauth?.refreshToken === "string" && creds.claudeAiOauth.refreshToken.length > 0;
}
/**
* The handed grant laid over what is there — a rotation of the licence the node already holds — or,
* for a switch, in place of it: the old licence's grant goes whole, scopes and subscription included,
* and only keys outside the grant (another kind of credential the vendor keeps in the file) stay.
* Either way, no refresh token survives.
*/
export function replacedBy(local: Credentials | null, grant: Grant): Credentials {
const next: Credentials = { ...(local ?? {}) };
delete next.claudeAiOauth;
return withGrant(next, grant);
}
/** Overlay the handed grant on what is there, and delete any refresh token. */
export function withGrant(local: Credentials | null, grant: Grant): Credentials {
const next: Credentials = { ...(local ?? {}) };
const oauth: Oauth = { ...(local?.claudeAiOauth ?? {}) };
oauth.accessToken = grant.accessToken;
oauth.expiresAt = grant.expiresAt;
for (const k of ["refreshTokenExpiresAt", "scopes", "subscriptionType", "rateLimitTier"] as const) {
const v = grant[k];
if (v != null) oauth[k] = v as unknown;
}
delete oauth.refreshToken;
next.claudeAiOauth = oauth;
return next;
}
/** Write atomically at 0600: a partial credentials file must never be read as a whole one. */
export function writeCredentials(path: string, creds: Credentials): void {
mkdirSync(dirname(path), { recursive: true, mode: 0o700 });
const tmp = `${path}.mesh-tmp`;
writeFileSync(tmp, JSON.stringify(creds, null, 2) + "\n", { mode: 0o600 });
renameSync(tmp, path);
}
-50
View File
@@ -1,50 +0,0 @@
// Which account the agent is logged in as (novox/hq ADR 0183): not in the token, but in the agent's
// own state file beside the home, `~/.claude.json` → `oauthAccount`. Read to attribute a login; written,
// three keys and nothing else, when a licence is switched, so the file Claude Code shows the account from
// names the account whose token it now holds (as the predecessor learned: two files that disagree make
// a later login look like the wrong account).
import { readFileSync, renameSync, writeFileSync } from "node:fs";
export interface Identity {
readonly accountUuid: string;
readonly emailAddress?: string;
readonly organizationUuid?: string;
}
export function readIdentity(stateFile: string): Identity | null {
try {
const raw = JSON.parse(readFileSync(stateFile, "utf8")) as { oauthAccount?: Record<string, unknown> };
const a = raw.oauthAccount;
if (!a || typeof a.accountUuid !== "string") return null;
return {
accountUuid: a.accountUuid,
emailAddress: typeof a.emailAddress === "string" ? a.emailAddress : undefined,
organizationUuid: typeof a.organizationUuid === "string" ? a.organizationUuid : undefined,
};
} catch {
return null;
}
}
/**
* Point the state file's account at `id`, keeping every other key as found. Returns whether the file
* changed; a file that cannot be read as an object is left alone rather than replaced.
*/
export function writeIdentity(stateFile: string, id: Identity): boolean {
let raw: Record<string, unknown>;
try {
raw = JSON.parse(readFileSync(stateFile, "utf8")) as Record<string, unknown>;
if (!raw || typeof raw !== "object") return false;
} catch {
raw = {};
}
const current = (raw.oauthAccount ?? {}) as Record<string, unknown>;
if (current.accountUuid === id.accountUuid && current.emailAddress === id.emailAddress
&& current.organizationUuid === id.organizationUuid) return false;
raw.oauthAccount = { ...current, accountUuid: id.accountUuid, emailAddress: id.emailAddress, organizationUuid: id.organizationUuid };
const tmp = `${stateFile}.mesh-tmp`;
writeFileSync(tmp, JSON.stringify(raw, null, 2), { mode: 0o600 });
renameSync(tmp, stateFile);
return true;
}
+12 -8
View File
@@ -11,17 +11,18 @@
"binds": {
"mcp-endpoint": "${dir:state}/mcp-endpoint.json"
},
"consumes": [
"claude-licence-manager.licence.rotated",
"claude-licence-manager.licence.switched"
],
"state": [
"servers"
"servers",
"holdings"
],
"reads": [
"claude-licence-manager.bindings"
],
"tools": [
"claude_code_status",
"claude_code_render",
"claude_code_pull",
"claude_code_grant",
"claude_code_mcp_list",
"claude_code_mcp_register",
"claude_code_mcp_unregister"
@@ -75,9 +76,12 @@
{
"name": "tools",
"kind": "bundle",
"language": "typescript",
"entrypoints": [
"tools/index.js"
"language": "go",
"system": "arch",
"from": "cmd/claude-code",
"binary": "claude-code",
"loads": [
"claude-code"
],
"env": {
"MESH_CLAUDE_CODE_STATE": "${dir:state}",
-271
View File
@@ -1,271 +0,0 @@
// What claude-code does on a node, written against two things it is handed — a way to ask a tool on the
// bus and a way to emit an event — so every path is tested without a bus (novox/hq design 36 §4–§5,
// ADR 0183, ADR 0198).
//
// **Over NATS, in two kinds** (design 32 §10): an event says that something happened and carries no
// secret, because a stream keeps it; a token travels on a request, which nothing keeps. So:
// - the licence manager's `licence.rotated` and `licence.switched` events tell this module to ask the
// seat for its current token, sealed to the key it sends with the request;
// - a login a person made here — a refresh token this module never writes — is offered to the seat at
// once, sealed to the seat's key: the one moment a refresh token travels, because the login made the
// manager's stale;
// - an MCP server registered through this module is **state, not an event** (novox/hq ADR 0201): one
// key per server in the module's `servers` bucket — `all.<server>` for every node, `<node>.<server>`
// for one — which every node watches. A node that joins later, or was off, reads the whole current set
// at start; unregistering is a delete. A secret never goes in an entry: the runtime refuses one.
import { chmodSync, existsSync, readFileSync, rmSync, writeFileSync } from "node:fs";
import { join } from "node:path";
import { render, entryProblem, MANAGED_DIR, type Binding, type Facts, type Settings, type Servers } from "./render.js";
import { generateKeyPair, open, seal, type SealedBox } from "./seal.js";
import { decideApply, grantOf, holdsLogin, readCredentials, replacedBy, withGrant, writeCredentials, type Grant } from "./grant.js";
import { readIdentity, writeIdentity, type Identity } from "./identity.js";
export const SEAT = "anthropic-licence-manager";
export interface Paths {
state: string;
facts: string;
settings: string;
home: string;
node: string;
}
/** A tool on the bus: its address and arguments in, its JSON answer out. */
export type Ask = (address: string, args: Record<string, unknown>) => Promise<unknown>;
/** An event of this module's, by its local name. */
export type Emit = (type: string, body: unknown) => Promise<void>;
/** Write one managed file; answers what happened. */
export type WriteManaged = (name: string, content: string) => string;
export const readJson = <T>(p: string, fallback: T): T => {
try {
return JSON.parse(readFileSync(p, "utf8")) as T;
} catch {
return fallback;
}
};
const credentialsPath = (p: Paths) => join(p.home, ".claude", ".credentials.json");
const accountPath = (p: Paths) => join(p.home, ".claude.json");
const bindingPath = (p: Paths) => join(p.state, "licence.json");
const apiKeyPath = (p: Paths) => join(p.state, "api-key");
export const helperPath = (p: Paths) => join(p.state, "api-key-helper");
const keyPath = (p: Paths) => join(p.state, "key.pem");
const pubPath = (p: Paths) => join(p.state, "key.pub.pem");
const registryPath = (p: Paths) => join(p.state, "mcp-servers.json");
export function keypair(p: Paths): { publicKey: string; privateKey: string } {
if (!existsSync(keyPath(p))) {
const k = generateKeyPair();
writeFileSync(keyPath(p), k.privateKey, { mode: 0o600 });
writeFileSync(pubPath(p), k.publicKey, { mode: 0o644 });
}
return { privateKey: readFileSync(keyPath(p), "utf8"), publicKey: readFileSync(pubPath(p), "utf8") };
}
export function registered(p: Paths): Servers {
return readJson<Servers>(registryPath(p), {});
}
export function renderNow(p: Paths, write: WriteManaged): string[] {
const facts = readJson<Facts | null>(p.facts, null);
if (!facts?.console) throw new Error(`the mesh has not rendered ${p.facts} yet; nothing to write`);
const files = render(facts, readJson<Settings>(p.settings, {}), readJson<Binding | null>(bindingPath(p), null),
helperPath(p), registered(p));
return Object.entries(files).map(([name, content]) => write(name, content));
}
// ---- the licence ----------------------------------------------------------------------------------
/** What the seat answers to `current`: the licence this node is bound to and its token, sealed. */
export interface Current {
licence: string;
kind: "subscription" | "api-key";
sealed: SealedBox;
identity?: Identity | null;
}
/** Ask the seat for this node's current token and apply it. */
export async function pull(p: Paths, ask: Ask, write: WriteManaged): Promise<Record<string, unknown>> {
const answer = (await ask(`${SEAT}.current`, { node: p.node, public_key: keypair(p).publicKey })) as Current | null;
if (!answer?.sealed) return { applied: false, reason: "the seat holds no licence for this node" };
return apply(p, answer, write);
}
/** Apply what the seat handed over. A switch replaces the grant whole and cleans up after the old licence. */
export function apply(p: Paths, handed: Current, write: WriteManaged): Record<string, unknown> {
const plain = open(handed.sealed, keypair(p).privateKey);
const previous = readJson<Binding | null>(bindingPath(p), null);
const switched = previous?.licence !== handed.licence;
let outcome: Record<string, unknown> = { applied: true, licence: handed.licence, kind: handed.kind, switched };
if (handed.kind === "api-key") {
writeFileSync(apiKeyPath(p), plain.trim() + "\n", { mode: 0o600 });
writeFileSync(helperPath(p), `#!/bin/sh\nexec cat '${apiKeyPath(p)}'\n`, { mode: 0o700 });
chmodSync(helperPath(p), 0o700);
} else {
const grant = JSON.parse(plain) as Grant;
const local = readCredentials(credentialsPath(p));
const d = decideApply(grantOf(local), grant, switched ? "switch" : "rotation");
if (d.apply) writeCredentials(credentialsPath(p), switched ? replacedBy(local, grant) : withGrant(local, grant));
else outcome = { applied: false, licence: handed.licence, reason: "reason" in d ? d.reason : undefined }; // narrowed by hand: the build compiles without strict
// Away from the API key: it goes, with its helper.
rmSync(apiKeyPath(p), { force: true });
rmSync(helperPath(p), { force: true });
}
if (switched && handed.identity?.accountUuid) {
outcome.account = writeIdentity(accountPath(p), handed.identity) ? "updated" : "unchanged";
}
writeFileSync(bindingPath(p), JSON.stringify({ licence: handed.licence, kind: handed.kind }) + "\n", { mode: 0o600 });
try {
outcome.rendered = renderNow(p, write); // the key-helper comes or goes with the licence's kind
} catch (err) {
outcome.rendered = { failed: err instanceof Error ? err.message : String(err) };
}
return outcome;
}
/** A licence event from the manager: is it for this node? */
export function concerns(p: Paths, type: string, body: { licence?: string; node?: string }): boolean {
if (type.endsWith("licence.switched")) return body.node === p.node;
if (type.endsWith("licence.rotated")) return body.licence === readJson<Binding | null>(bindingPath(p), null)?.licence;
return false;
}
/** A refresh token in the credentials file is a login: this module never writes one. Offer it to the seat. */
export async function offerLogin(p: Paths, ask: Ask): Promise<Record<string, unknown> | null> {
const creds = readCredentials(credentialsPath(p));
if (!holdsLogin(creds)) return null;
const key = (await ask(`${SEAT}.public_key`, {})) as { public_key?: string } | null;
if (!key?.public_key) throw new Error("the licence manager did not say what key to seal a login to");
return (await ask(`${SEAT}.adopt`, {
node: p.node,
identity: readIdentity(accountPath(p)),
sealed: seal(JSON.stringify(creds!.claudeAiOauth), key.public_key),
})) as Record<string, unknown>;
}
// ---- MCP servers ----------------------------------------------------------------------------------
export interface Registration {
name: string;
entry?: Record<string, unknown>;
/** Which nodes: this one (absent), every node running the module ("all"), or a list. */
nodes?: "all" | string[];
}
/** The `servers` state, as this module reaches it through the runtime (`state("servers")` in the SDK). */
export interface ServerState {
put(key: string, value: Record<string, unknown>): Promise<number>;
delete(key: string): Promise<void>;
keys(): Promise<string[]>;
}
/** One change to the `servers` state, as a watch hands it over. */
export interface ServerChange {
key: string;
op: "put" | "delete";
value?: Record<string, unknown>;
}
/** The key a registration lives at: `all.<server>` for every node, `<node>.<server>` for one. */
export const keyOf = (scope: string, name: string) => `${scope}.${name}`;
/**
* What this node takes from the `servers` state: the entries for every node and for this one, by key —
* kept in memory from the watch, and written through to the module's own file whenever what applies here
* changes, so the managed directory can be rendered without the bus.
*/
export class ServerView {
private readonly entries = new Map<string, Record<string, unknown>>();
constructor(private readonly p: Paths) {}
/** Take one change; answers whether what applies to this node changed. */
take(c: ServerChange): boolean {
const dot = c.key.indexOf(".");
const scope = c.key.slice(0, dot), name = c.key.slice(dot + 1);
if (dot <= 0 || (scope !== "all" && scope !== this.p.node)) return false;
if (c.op === "put" && c.value && entryProblem(name, c.value) === null) this.entries.set(c.key, c.value);
else this.entries.delete(c.key);
return this.writeThrough();
}
/** What applies here: every node's entries, with this node's own laid over them by server name. */
effective(): Servers {
const out: Record<string, Record<string, unknown>> = {};
for (const scope of ["all", this.p.node]) {
for (const [key, entry] of [...this.entries].sort(([a], [b]) => a.localeCompare(b))) {
if (key.startsWith(scope + ".")) out[key.slice(scope.length + 1)] = entry;
}
}
return out;
}
private writeThrough(): boolean {
const now = JSON.stringify(this.effective(), null, 2) + "\n";
let before = "";
try {
before = readFileSync(registryPath(this.p), "utf8");
} catch {
/* none yet */
}
if (now === before) return false;
writeFileSync(registryPath(this.p), now, { mode: 0o600 });
return true;
}
}
/** A change from the watch: take it, and render when what applies here changed. */
export function onServerChange(view: ServerView, c: ServerChange, p: Paths, write: WriteManaged): string | null {
if (!view.take(c)) return null;
renderNow(p, write);
return `${c.op === "put" ? "registered" : "unregistered"} ${c.key}`;
}
const scopesOf = (p: Paths, nodes: Registration["nodes"]): string[] =>
nodes === undefined ? [p.node] : nodes === "all" ? ["all"] : nodes;
/**
* Register (or with no entry, unregister) 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.
*/
export async function registerServer(p: Paths, r: Registration, servers: ServerState, view: ServerView,
write: WriteManaged, others: () => Promise<string[]>): Promise<Record<string, unknown>> {
if (r.entry) {
const problem = entryProblem(r.name, r.entry);
if (problem) return { registered: false, reason: problem };
}
const scopes = scopesOf(p, r.nodes);
// 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.
const before = JSON.stringify(view.effective());
for (const scope of scopes) {
const key = keyOf(scope, r.name);
if (r.entry) await servers.put(key, r.entry);
else await servers.delete(key);
view.take({ key, op: r.entry ? "put" : "delete", value: r.entry });
}
const changedHere = JSON.stringify(view.effective()) !== before;
const here = scopes.includes("all") || scopes.includes(p.node);
const answer: Record<string, unknown> = {
[r.entry ? "registered" : "unregistered"]: r.name,
on: r.nodes === undefined ? [p.node] : r.nodes,
here: here ? (changedHere ? "changed" : "already so") : "not this node",
rendered: changedHere ? renderNow(p, write) : [],
};
if (!r.entry && view.effective()[r.name]) {
answer.still = `${r.name} still applies here from another registration (for every node, or for this one); unregister that too`;
}
if (r.nodes === undefined) {
// The question the operator wanted asked: here only, or more?
const elsewhere = (await others().catch(() => [] as string[])).filter((n) => n !== p.node);
answer.also = elsewhere.length
? `claude-code also runs on ${elsewhere.join(", ")}. To ${r.entry ? "register" : "unregister"} it there too, call again with nodes: "all" or a list of those nodes.`
: `To do the same on every node running claude-code, call again with nodes: "all".`;
}
return answer;
}
export { MANAGED_DIR };
-18
View File
@@ -1,18 +0,0 @@
{
"name": "@novox/module-claude-code",
"version": "0.1.0",
"description": "claude-code — the operator's agent on a machine: its managed configuration, and the consumer side of the Anthropic licence manager (novox/hq design 36).",
"type": "module",
"private": true,
"scripts": {
"build": "tsc seal.ts grant.ts identity.ts render.ts node.ts tools/index.ts --module NodeNext --moduleResolution NodeNext --target ES2022 --rootDir . --outDir dist",
"test": "npm run build && node --test --experimental-strip-types 'test/*.test.ts'"
},
"dependencies": {
"@novox/mesh-sdk": "^0.1.7"
},
"devDependencies": {
"@types/node": "^22.0.0",
"typescript": "^5.6.0"
}
}
-135
View File
@@ -1,135 +0,0 @@
// What the module writes into the agent's machine-wide managed directory (novox/hq design 36 §1–§4).
// Pure: composed from the facts the mesh rendered, the settings the operator set and the licence the
// node holds, so what lands under /etc is tested without a machine.
//
// Three files, owned whole by this module:
// managed-mcp.json the tool servers every session loads: the mesh's console as `mesh`, and the
// servers the operator declared for the mesh or this node. Exclusive by the
// vendor's rule — a server not listed here does not load — which is why the
// list is the module's settings and nothing else (operator's choice, 2026-10-03).
// managed-settings.json the mesh's keys only: the repositories' attribution convention, the
// claude.ai connectors kept beside the managed servers, and — for an API-key
// licence only — the key-helper. A person's preferences are theirs.
// CLAUDE.md how a session on this mesh works, who this node is, the conventions.
export const MANAGED_DIR = "/etc/claude-code";
export interface Facts {
readonly node: string;
readonly console: string;
}
export interface Settings {
readonly role?: string;
/** Extra tool servers, in the vendor's `.mcp.json` entry shape, keyed by name. */
readonly mcp_servers?: Readonly<Record<string, Record<string, unknown>>>;
}
export interface Binding {
readonly licence: string;
readonly kind: "subscription" | "api-key";
}
export interface Rendered {
readonly [file: string]: string;
}
const MESH_ENTRY = "mesh";
export type Servers = Readonly<Record<string, Record<string, unknown>>>;
/** Whether an entry is one the vendor's managed file takes: a name of letters, digits, `-` and `_`, and
* an http/sse server with a url or a stdio server with a command. Returns why not, or null. */
export function entryProblem(name: string, entry: Record<string, unknown>): string | null {
if (!/^[A-Za-z0-9_-]+$/.test(name)) return `"${name}" is not a name the agent takes: letters, digits, - and _`;
if (name === MESH_ENTRY) return `"${MESH_ENTRY}" is the mesh's own entry`;
const type = entry?.type ?? "stdio";
if (type === "http" || type === "sse" || type === "streamable-http") {
return typeof entry.url === "string" && entry.url ? null : `an ${type} server needs a url`;
}
if (type === "stdio") return typeof entry.command === "string" && entry.command ? null : "a stdio server needs a command";
return `"${String(type)}" is not a server type the agent knows (http, sse, stdio)`;
}
/**
* Compose the three files. `registered` is the module's own list on this node — what was registered
* through its tools — laid over the servers the operator set in its settings.
*/
export function render(facts: Facts, settings: Settings, binding: Binding | null, helperPath: string,
registered: Servers = {}): Rendered {
const servers: Record<string, unknown> = {};
for (const [name, entry] of Object.entries({ ...(settings.mcp_servers ?? {}), ...registered })) {
if (entryProblem(name, entry) !== null) continue; // the mesh's own entry, or one the agent would refuse
servers[name] = entry;
}
servers[MESH_ENTRY] = { type: "http", url: facts.console };
const managed: Record<string, unknown> = {
attribution: { commit: "", pr: "" },
allowAllClaudeAiMcps: true,
};
if (binding?.kind === "api-key") managed.apiKeyHelper = helperPath;
return {
"managed-mcp.json": json({ mcpServers: sortKeys(servers) }),
"managed-settings.json": json(managed),
"CLAUDE.md": instructions(facts, settings),
};
}
function json(v: unknown): string {
return JSON.stringify(v, null, 2) + "\n";
}
function sortKeys(o: Record<string, unknown>): Record<string, unknown> {
return Object.fromEntries(Object.keys(o).sort().map((k) => [k, o[k]]));
}
export function instructions(facts: Facts, settings: Settings): string {
const role = settings.role?.trim() ? settings.role.trim() : "not stated — set it in this module's settings for the node";
return `# This machine is a node of a Novox mesh
Written by the mesh's \`claude-code\` module. Edit the module's settings or the catalogue, never this file:
it is rewritten whenever the module renders.
## Who this node is
- **Node:** \`${facts.node}\`
- **Role:** ${role}
- The other nodes, their roles and what runs where: ask the controller (\`mesh-controller.nodes\`,
\`mesh-controller.node\`). Nothing here lists them, because a copy drifts.
## How a session on this mesh works
The console is the only way to the mesh: the MCP server named \`mesh\`. It offers five tools, and
everything else is an address you find and call through them:
- \`mesh_search\` — words in, matching addresses out. \`mesh_describe\` — one address's arguments.
- \`mesh_call\` — call an address. A seat the mesh holds once is \`<seat>.<verb>\` (the mesh's own verbs
are \`mesh-controller.<verb>\`: \`status\`, \`plan\`, \`node\`, \`assign\`, \`push\`, \`settings\`);
a module on a machine is \`<node>/<module>.<tool>\`.
- \`mesh_overview\` and \`mesh_machine\` — the mesh's seats and machines, and what one machine runs.
- **Symptom first.** For an error, a failing service or anything unexpected, search the record with the
literal text before forming a hypothesis: the records module's \`records_search\`, then
\`records_read\`.
- **Ask the mesh before changing it**, and change it through the controller's verbs or the catalogue.
- **A licence** through the \`anthropic-licence-manager\` seat's verbs. Never edit the agent's credentials
file by hand, never print or ask for a token.
## Hard rules
- A file the mesh manages is changed through the verb or the catalogue that owns it, never on disk. If
unsure, \`mesh-controller.plan\` for the node says what the mesh writes there.
- Never write to a store's database by hand; schema changes are numbered migrations.
- Never push to a main branch: a branch, a pull request, and a human approval for every merge.
- The mesh creates no symlinks, and nobody else does either.
- A package is declared in a module, never installed by hand.
## Conventions
- Commit messages are concise, in the imperative, about why.
- Test before pushing: nodes update unattended.
- The playbooks in the record say how research, decisions, designs, issues and hand-offs are done.
`;
}
-77
View File
@@ -1,77 +0,0 @@
// Sealing a token to one recipient (novox/hq ADR 0183): the manager seals what it hands a node to that
// node's agent module key, and a node seals a waiting login to the key the manager names. X25519 for
// the agreement, HKDF-SHA256 for the key, AES-256-GCM for the box — all from Node's own library, so a
// bundle carries no dependency and no secret ever crosses the bus in the clear.
//
// A sealed box is `{ v: 1, eph, iv, tag, ct }`, every field base64. `eph` is a one-time public key, so
// two boxes of one value to one recipient share nothing, and only the recipient's private key opens it.
import {
createCipheriv, createDecipheriv, createPrivateKey, createPublicKey, diffieHellman,
generateKeyPairSync, hkdfSync, randomBytes, type KeyObject,
} from "node:crypto";
export interface SealedBox {
readonly v: 1;
readonly eph: string;
readonly iv: string;
readonly tag: string;
readonly ct: string;
}
/** A recipient's keypair, as the two PEM strings it is kept and published as. */
export interface KeyPairPem {
readonly publicKey: string;
readonly privateKey: string;
}
const INFO = Buffer.from("novox-mesh sealed box v1");
export function generateKeyPair(): KeyPairPem {
const { publicKey, privateKey } = generateKeyPairSync("x25519");
return {
publicKey: publicKey.export({ type: "spki", format: "pem" }).toString(),
privateKey: privateKey.export({ type: "pkcs8", format: "pem" }).toString(),
};
}
function keyFor(secret: Buffer, eph: Buffer, recipient: Buffer): Buffer {
// The ephemeral and the recipient's public halves are bound into the key, so a box cannot be
// re-addressed to another recipient by swapping its `eph`.
return Buffer.from(hkdfSync("sha256", secret, Buffer.concat([eph, recipient]), INFO, 32));
}
function rawPublic(key: KeyObject): Buffer {
return key.export({ type: "spki", format: "der" }).subarray(-32);
}
export function seal(plaintext: string, recipientPublicPem: string): SealedBox {
const recipient = createPublicKey(recipientPublicPem);
const eph = generateKeyPairSync("x25519");
const secret = diffieHellman({ privateKey: eph.privateKey, publicKey: recipient });
const ephRaw = eph.publicKey.export({ type: "spki", format: "der" });
const key = keyFor(secret, ephRaw, rawPublic(recipient));
const iv = randomBytes(12);
const cipher = createCipheriv("aes-256-gcm", key, iv);
const ct = Buffer.concat([cipher.update(plaintext, "utf8"), cipher.final()]);
return {
v: 1,
eph: ephRaw.toString("base64"),
iv: iv.toString("base64"),
tag: cipher.getAuthTag().toString("base64"),
ct: ct.toString("base64"),
};
}
/** Open a box with the recipient's private key. Throws on a box for another key or one tampered with. */
export function open(box: SealedBox, privateKeyPem: string): string {
if (!box || box.v !== 1) throw new Error("not a sealed box this module can open");
const priv = createPrivateKey(privateKeyPem);
const ephRaw = Buffer.from(box.eph, "base64");
const eph = createPublicKey({ key: ephRaw, format: "der", type: "spki" });
const secret = diffieHellman({ privateKey: priv, publicKey: eph });
const key = keyFor(secret, ephRaw, rawPublic(createPublicKey(priv)));
const decipher = createDecipheriv("aes-256-gcm", key, Buffer.from(box.iv, "base64"));
decipher.setAuthTag(Buffer.from(box.tag, "base64"));
return Buffer.concat([decipher.update(Buffer.from(box.ct, "base64")), decipher.final()]).toString("utf8");
}
-54
View File
@@ -1,54 +0,0 @@
import { test } from "node:test";
import assert from "node:assert/strict";
import { mkdtempSync, readFileSync, statSync, writeFileSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
import {
decideApply, grantOf, holdsLogin, readCredentials, withGrant, writeCredentials, type Grant,
} from "../dist/grant.js";
const NOW = 1_700_000_000_000;
const HOUR = 3_600_000;
const g = (over: Partial<Grant> = {}): Grant => ({
accessToken: "tok-A", expiresAt: NOW + HOUR, refreshTokenExpiresAt: NOW + 30 * 24 * HOUR, ...over,
});
test("a rotation applies a newer grant of the same licence", () => {
assert.deepEqual(decideApply(g(), g({ accessToken: "tok-B", expiresAt: NOW + 2 * HOUR }), "rotation"), { apply: true });
});
test("a rotation refuses a grant that arrived late and is older", () => {
const d = decideApply(g({ accessToken: "new", expiresAt: NOW + 2 * HOUR }), g({ accessToken: "old" }), "rotation");
assert.equal(d.apply === false && d.reason, "not-newer");
});
test("a grant re-issued by a login is adopted even though it expires sooner (2026-09-05)", () => {
const local = g({ expiresAt: NOW + 8 * HOUR, refreshTokenExpiresAt: NOW + 30 * 24 * HOUR });
const offered = g({ accessToken: "reissued", expiresAt: NOW + HOUR, refreshTokenExpiresAt: NOW + 5 * 24 * HOUR });
assert.deepEqual(decideApply(local, offered, "rotation"), { apply: true, reissued: true });
});
test("a switch to another licence applies whatever the expiries say", () => {
const local = g({ expiresAt: NOW + 8 * HOUR });
assert.equal(decideApply(local, g({ accessToken: "other", expiresAt: NOW + HOUR }), "switch").apply, true);
});
test("the same token is not rewritten", () => {
assert.deepEqual(decideApply(g(), g(), "switch"), { apply: false, reason: "already-current" });
});
test("a full grant left by a login is seen as a login, and stripped when the node's own is written", () => {
const dir = mkdtempSync(join(tmpdir(), "claude-code-"));
const path = join(dir, ".claude", ".credentials.json");
writeFileSync(join(dir, "x"), "");
const login = { claudeAiOauth: { accessToken: "at-login", refreshToken: "rt-login", expiresAt: NOW }, other: 1 };
assert.equal(holdsLogin(login), true);
writeCredentials(path, withGrant(login, g({ accessToken: "at-mesh", scopes: ["user:inference"] })));
const back = readCredentials(path)!;
assert.equal(holdsLogin(back), false);
assert.equal(grantOf(back)!.accessToken, "at-mesh");
assert.deepEqual(back.claudeAiOauth!.scopes, ["user:inference"]);
assert.equal(back.other, 1, "a key the module does not know was lost");
assert.ok(!readFileSync(path, "utf8").includes("rt-login"));
assert.equal(statSync(path).mode & 0o777, 0o600);
});
-19
View File
@@ -1,19 +0,0 @@
import { test } from "node:test";
import assert from "node:assert/strict";
import { mkdtempSync, writeFileSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { readIdentity } from "../dist/identity.js";
test("the account is read from the agent's state file", () => {
const p = join(mkdtempSync(join(tmpdir(), "cc-id-")), ".claude.json");
writeFileSync(p, JSON.stringify({ oauthAccount: { accountUuid: "u-1", emailAddress: "a@example.org" }, other: 2 }));
assert.deepEqual(readIdentity(p), { accountUuid: "u-1", emailAddress: "a@example.org", organizationUuid: undefined });
});
test("no state file, or no account in it, is no identity rather than a guess", () => {
assert.equal(readIdentity("/nonexistent/.claude.json"), null);
const p = join(mkdtempSync(join(tmpdir(), "cc-id-")), ".claude.json");
writeFileSync(p, "{}");
assert.equal(readIdentity(p), null);
});
-173
View File
@@ -1,173 +0,0 @@
import { test } from "node:test";
import assert from "node:assert/strict";
import { existsSync, mkdirSync, mkdtempSync, readFileSync, writeFileSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
import {
apply, concerns, keypair, offerLogin, onServerChange, pull, registerServer, registered, ServerView, type Paths,
type ServerChange, type ServerState,
} from "../dist/node.js";
import { generateKeyPair, open, seal } from "../dist/seal.js";
const NOW = Date.now();
function node(name = "laptop"): { p: Paths; written: Record<string, string> } {
const root = mkdtempSync(join(tmpdir(), "cc-node-"));
const p = { state: join(root, "state"), facts: join(root, "state", "facts.json"), settings: join(root, "state", "settings.json"), home: join(root, "home"), node: name };
mkdirSync(p.state, { recursive: true });
mkdirSync(join(p.home, ".claude"), { recursive: true });
writeFileSync(p.facts, JSON.stringify({ node: name, console: "http://127.0.0.1:4270/mcp" }));
writeFileSync(p.settings, JSON.stringify({ role: "", mcp_servers: {} }));
return { p, written: {} };
}
const writer = (w: Record<string, string>) => (name: string, content: string) => { w[name] = content; return `${name}: written`; };
const creds = (p: Paths) => JSON.parse(readFileSync(join(p.home, ".claude", ".credentials.json"), "utf8"));
const grantFor = (p: Paths, licence: string, token: string, kind: "subscription" | "api-key" = "subscription", identity?: object) => ({
licence, kind, identity,
sealed: seal(kind === "api-key" ? token : JSON.stringify({ accessToken: token, expiresAt: NOW + 3_600_000, refreshTokenExpiresAt: NOW + 86_400_000, subscriptionType: licence }), keypair(p).publicKey),
});
test("a pull asks the seat with this node's key and applies what it answers", async () => {
const { p, written } = node();
let asked: [string, Record<string, unknown>] | null = null;
const r = await pull(p, async (address, args) => { asked = [address, args]; return grantFor(p, "personal", "at-1"); }, writer(written));
assert.equal(asked![0], "anthropic-licence-manager.current");
assert.equal(asked![1].node, "laptop");
assert.match(String(asked![1].public_key), /BEGIN PUBLIC KEY/);
assert.equal(r.applied, true);
assert.equal(creds(p).claudeAiOauth.accessToken, "at-1");
assert.ok(written["managed-mcp.json"]);
});
test("a switch replaces the old licence's grant whole and points the account at the new one", () => {
const { p, written } = node();
writeFileSync(join(p.home, ".claude.json"), JSON.stringify({ oauthAccount: { accountUuid: "old" }, projects: { keep: 1 } }));
apply(p, grantFor(p, "personal", "at-1"), writer(written));
const r = apply(p, grantFor(p, "work", "at-2", "subscription", { accountUuid: "new", emailAddress: "w@example.org" }), writer(written));
assert.equal(r.switched, true);
assert.equal(creds(p).claudeAiOauth.accessToken, "at-2");
assert.equal(creds(p).claudeAiOauth.subscriptionType, "work", "the old licence's subscription type survived the switch");
const account = JSON.parse(readFileSync(join(p.home, ".claude.json"), "utf8"));
assert.equal(account.oauthAccount.accountUuid, "new");
assert.deepEqual(account.projects, { keep: 1 });
});
test("switching to the API key adds the key-helper; switching away removes the key and the helper", () => {
const { p, written } = node();
apply(p, grantFor(p, "api", "sk-key", "api-key"), writer(written));
assert.ok(JSON.parse(written["managed-settings.json"]).apiKeyHelper);
assert.ok(existsSync(join(p.state, "api-key")));
apply(p, grantFor(p, "personal", "at-1"), writer(written));
assert.ok(!("apiKeyHelper" in JSON.parse(written["managed-settings.json"])));
assert.ok(!existsSync(join(p.state, "api-key")) && !existsSync(join(p.state, "api-key-helper")));
});
test("a rotation event concerns the node bound to that licence; a switch event the node it names", () => {
const { p, written } = node();
apply(p, grantFor(p, "personal", "at-1"), writer(written));
assert.equal(concerns(p, "claude-licence-manager.licence.rotated", { licence: "personal" }), true);
assert.equal(concerns(p, "claude-licence-manager.licence.rotated", { licence: "work" }), false);
assert.equal(concerns(p, "claude-licence-manager.licence.switched", { node: "laptop", licence: "work" }), true);
assert.equal(concerns(p, "claude-licence-manager.licence.switched", { node: "server" }), false);
});
test("a login is offered to the seat sealed to the seat's key, with the account it belongs to", async () => {
const { p } = node();
const manager = generateKeyPair();
writeFileSync(join(p.home, ".claude", ".credentials.json"), JSON.stringify({ claudeAiOauth: { accessToken: "at-login", refreshToken: "rt-login", expiresAt: NOW } }));
writeFileSync(join(p.home, ".claude.json"), JSON.stringify({ oauthAccount: { accountUuid: "u-9" } }));
const calls: [string, Record<string, unknown>][] = [];
await offerLogin(p, async (address, args) => { calls.push([address, args]); return address.endsWith("public_key") ? { public_key: manager.publicKey } : { adopted: true }; });
assert.deepEqual(calls.map((c) => c[0]), ["anthropic-licence-manager.public_key", "anthropic-licence-manager.adopt"]);
const adopt = calls[1][1] as { identity: { accountUuid: string }; sealed: never };
assert.equal(adopt.identity.accountUuid, "u-9");
assert.equal(JSON.parse(open(adopt.sealed, manager.privateKey)).refreshToken, "rt-login");
assert.ok(!JSON.stringify(adopt).includes("rt-login"), "the refresh token crossed in the clear");
});
test("no refresh token in the file is no login, and nothing is asked", async () => {
const { p } = node();
writeFileSync(join(p.home, ".claude", ".credentials.json"), JSON.stringify({ claudeAiOauth: { accessToken: "at", expiresAt: NOW } }));
assert.equal(await offerLogin(p, async () => { throw new Error("asked"); }), null);
});
/** The `servers` state as the bus holds it, shared by every node in a test, with each node's watch. */
function bus() {
const kept = new Map<string, Record<string, unknown>>();
const watchers: ((c: ServerChange) => void)[] = [];
const state: ServerState = {
put: async (key, value) => { kept.set(key, value); watchers.forEach((w) => w({ key, op: "put", value })); return kept.size; },
delete: async (key) => { kept.delete(key); watchers.forEach((w) => w({ key, op: "delete" })); },
keys: async () => [...kept.keys()].sort(),
};
/** A node joining: its view takes the current state, then every change. */
const join = (n: { p: Paths; written: Record<string, string> }) => {
const view = new ServerView(n.p);
for (const [key, value] of kept) onServerChange(view, { key, op: "put", value }, n.p, writer(n.written));
watchers.push((c) => onServerChange(view, c, n.p, writer(n.written)));
return view;
};
return { state, join, kept };
}
test("registering a server here puts it under this node's key, renders it, and asks about the other nodes", async () => {
const n = node();
const b = bus();
const view = b.join(n);
const r = await registerServer(n.p, { name: "search", entry: { type: "http", url: "https://s.example/mcp" } },
b.state, view, writer(n.written), async () => ["laptop", "server", "desktop"]);
assert.equal(r.here, "changed");
assert.match(String(r.also), /server, desktop/);
assert.deepEqual([...b.kept.keys()], ["laptop.search"]);
assert.ok(JSON.parse(n.written["managed-mcp.json"]).mcpServers.search);
});
test("registering for every node reaches the others through their watch, and a node joining later reads it", async () => {
const a = node("laptop"), s = node("server");
const b = bus();
const va = b.join(a);
b.join(s);
await registerServer(a.p, { name: "docs", entry: { type: "stdio", command: "docs-mcp" }, nodes: "all" },
b.state, va, writer(a.written), async () => []);
assert.deepEqual([...b.kept.keys()], ["all.docs"]);
assert.deepEqual(registered(s.p).docs, { type: "stdio", command: "docs-mcp" });
assert.ok(JSON.parse(s.written["managed-mcp.json"]).mcpServers.docs);
// The gap events left: a node assigned after the registration takes the whole current set at start.
const late = node("desktop");
b.join(late);
assert.deepEqual(registered(late.p).docs, { type: "stdio", command: "docs-mcp" });
// Unregistering is a delete, and every node's view drops it.
await registerServer(a.p, { name: "docs", nodes: "all" }, b.state, va, writer(a.written), async () => []);
assert.equal(registered(s.p).docs, undefined);
assert.equal(registered(late.p).docs, undefined);
});
test("a node's own registration overrides the one for every node; other nodes' keys leave this one alone", async () => {
const a = node("laptop"), s = node("server");
const b = bus();
const va = b.join(a);
const vs = b.join(s);
await registerServer(a.p, { name: "x", entry: { type: "http", url: "https://all" }, nodes: "all" }, b.state, va, writer(a.written), async () => []);
await registerServer(a.p, { name: "x", entry: { type: "http", url: "https://laptop" } }, b.state, va, writer(a.written), async () => []);
assert.equal(registered(a.p).x.url, "https://laptop");
assert.equal(registered(s.p).x.url, "https://all");
await registerServer(a.p, { name: "only", entry: { type: "http", url: "https://o" }, nodes: ["server"] }, b.state, va, writer(a.written), async () => []);
assert.equal(registered(a.p).only, undefined);
assert.equal(registered(s.p).only.url, "https://o");
// Unregistering here leaves the every-node one applying, and says so.
const r = await registerServer(a.p, { name: "x" }, b.state, va, writer(a.written), async () => []);
assert.match(String(r.still), /still applies here/);
assert.equal(registered(a.p).x.url, "https://all");
assert.equal(vs.effective().x.url, "https://all");
});
test("a bad entry is refused before anything is put; a repeated change changes nothing", async () => {
const n = node();
const b = bus();
const view = b.join(n);
const r = await registerServer(n.p, { name: "mesh", entry: { type: "http", url: "https://x" } }, b.state, view, writer(n.written), async () => []);
assert.equal(r.registered, false);
assert.equal(b.kept.size, 0);
assert.equal(onServerChange(view, { key: "all.a", op: "put", value: { type: "http", url: "https://a" } }, n.p, writer(n.written)), "registered all.a");
assert.equal(onServerChange(view, { key: "all.a", op: "put", value: { type: "http", url: "https://a" } }, n.p, writer(n.written)), null);
assert.equal(onServerChange(view, { key: "server.b", op: "put", value: { type: "http", url: "https://b" } }, n.p, writer(n.written)), null);
});
-40
View File
@@ -1,40 +0,0 @@
import { test } from "node:test";
import assert from "node:assert/strict";
import { render } from "../dist/render.js";
const facts = { node: "workstation", console: "http://127.0.0.1:4270/mcp" };
test("the console is the `mesh` server, and an operator's servers are listed beside it", () => {
const out = render(facts, { mcp_servers: { search: { type: "http", url: "https://s.example/mcp" } } }, null, "/h");
const mcp = JSON.parse(out["managed-mcp.json"]);
assert.deepEqual(Object.keys(mcp.mcpServers), ["mesh", "search"]);
assert.deepEqual(mcp.mcpServers.mesh, { type: "http", url: facts.console });
});
test("a setting cannot replace the mesh's own entry, and a name the vendor refuses is left out", () => {
const out = render(facts, { mcp_servers: { mesh: { type: "http", url: "http://evil" }, "bad name": {} } }, null, "/h");
const mcp = JSON.parse(out["managed-mcp.json"]);
assert.equal(mcp.mcpServers.mesh.url, facts.console);
assert.ok(!("bad name" in mcp.mcpServers));
});
test("managed settings carry the mesh's keys only, and the key-helper only for an API-key licence", () => {
const sub = JSON.parse(render(facts, {}, { licence: "personal", kind: "subscription" }, "/h")["managed-settings.json"]);
assert.deepEqual(sub, { attribution: { commit: "", pr: "" }, allowAllClaudeAiMcps: true });
const key = JSON.parse(render(facts, {}, { licence: "api", kind: "api-key" }, "/state/api-key-helper")["managed-settings.json"]);
assert.equal(key.apiKeyHelper, "/state/api-key-helper");
assert.ok(!("model" in key), "a preference is the person's");
});
test("the instruction file names the node and its role, and no other node", () => {
const md = render(facts, { role: "the laptop" }, null, "/h")["CLAUDE.md"];
assert.match(md, /\*\*Node:\*\* `workstation`/);
assert.match(md, /\*\*Role:\*\* the laptop/);
assert.match(md, /mesh_call/);
assert.match(md, /records_search/);
});
test("rendering is deterministic, so an unchanged input writes nothing", () => {
const s = { mcp_servers: { b: { type: "http", url: "https://b" }, a: { type: "http", url: "https://a" } } };
assert.deepEqual(render(facts, s, null, "/h"), render(facts, s, null, "/h"));
});
-31
View File
@@ -1,31 +0,0 @@
import { test } from "node:test";
import assert from "node:assert/strict";
import { generateKeyPair, open, seal } from "../dist/seal.js";
test("a box opens with its recipient's key and yields the value", () => {
const k = generateKeyPair();
assert.equal(open(seal("at-secret", k.publicKey), k.privateKey), "at-secret");
});
test("a box sealed for one node does not open with another node's key", () => {
const a = generateKeyPair();
const b = generateKeyPair();
assert.throws(() => open(seal("at-secret", a.publicKey), b.privateKey));
});
test("a tampered box is refused, not opened to garbage", () => {
const k = generateKeyPair();
const box = seal("at-secret", k.publicKey);
const ct = Buffer.from(box.ct, "base64");
ct[0] ^= 0xff;
assert.throws(() => open({ ...box, ct: ct.toString("base64") }, k.privateKey));
});
test("two boxes of one value share nothing a reader could compare", () => {
const k = generateKeyPair();
const x = seal("at-secret", k.publicKey);
const y = seal("at-secret", k.publicKey);
assert.notEqual(x.ct, y.ct);
assert.notEqual(x.eph, y.eph);
assert.ok(!JSON.stringify(x).includes("at-secret"));
});
-224
View File
@@ -1,224 +0,0 @@
// claude-code's bundle (novox/hq design 36, ADR 0183). The node's runtime launches it over stdio, as the
// operator account (ADR 0193), and is its bus (ADR 0198): it asks tools, emits and consumes through the
// runtime. It is given its state directory and two files the mesh renders into it (ADR 0192), beside the
// runtime's own words. **stdout is the MCP channel**: everything this module says, it says on stderr.
//
// At start it renders the agent's managed directory, asks the licence manager for this node's token,
// begins watching the credentials file for a login, takes the manager's licence events, and watches the
// module's `servers` state — every node's MCP server registrations (novox/hq ADR 0201). node.ts holds the
// logic.
import { mkdtempSync, readFileSync, rmSync, watchFile, writeFileSync } from "node:fs";
import { tmpdir } from "node:os";
import { spawnSync } from "node:child_process";
import { join } from "node:path";
import { registerModuleTools, type ToolDefinition } from "@novox/mesh-sdk/tools";
import { broker } from "@novox/mesh-sdk/messaging";
import { on } from "@novox/mesh-sdk/events";
import { state } from "@novox/mesh-sdk/state";
import {
MANAGED_DIR, SEAT, ServerView, concerns, keypair, offerLogin, onServerChange, pull, readJson, registerServer,
registered, renderNow, type Ask, type Paths, type Registration, type ServerChange, type ServerState, type WriteManaged,
} from "../node.js";
import { grantOf, holdsLogin, readCredentials } from "../grant.js";
import { createHash } from "node:crypto";
const say = (line: string) => console.error(`[claude-code] ${line}`);
const fingerprint = (s: string) => "sha256:" + createHash("sha256").update(s).digest("hex").slice(0, 16);
function pathsFrom(env: NodeJS.ProcessEnv): Paths | null {
const state = env.MESH_CLAUDE_CODE_STATE, facts = env.MESH_CLAUDE_CODE_FACTS;
const settings = env.MESH_CLAUDE_CODE_SETTINGS, home = env.MESH_OPERATOR_HOME, node = env.MESH_NODE;
if (!state || !facts || !settings || !home || !node) return null;
return { state, facts, settings, home, node };
}
/** Write one managed file as root, only when its content changed. */
const writeManaged: WriteManaged = (name, content) => {
const path = join(MANAGED_DIR, name);
try {
if (readFileSync(path, "utf8") === content) return `${name}: unchanged`;
} catch {
/* absent */
}
// From a file, never /dev/stdin: Node hands a child its input over a socket, which /dev/stdin cannot
// open (ENXIO) — found on the first assignment, where nothing under /etc/claude-code was ever written.
const staged = mkdtempSync(join(tmpdir(), "claude-code-"));
const source = join(staged, name);
writeFileSync(source, content, { mode: 0o644 });
const asRoot = process.getuid?.() === 0;
const cmd = asRoot ? ["install", "-D", "-m", "0644", source, path] : ["sudo", "-n", "install", "-D", "-m", "0644", source, path];
const r = spawnSync(cmd[0], cmd.slice(1), { encoding: "utf8" });
rmSync(staged, { recursive: true, force: true });
if (r.status !== 0) {
throw new Error(`${name}: could not be written to ${MANAGED_DIR} (${(r.stderr || r.error?.message || "").trim()}); ` +
`the module writes there through the operator account's passwordless sudo`);
}
return `${name}: written`;
};
/** A tool on the bus, through the runtime; its MCP answer read back as JSON where it is JSON. */
const ask: Ask = async (address, args) => {
const answer = (await broker().request<Record<string, unknown>, { content?: { text?: string }[]; isError?: boolean }>(address, args)) ?? {};
const text = answer.content?.map((c) => c.text ?? "").join("") ?? "";
if (answer.isError) throw new Error(`${address}: ${text}`);
try {
return JSON.parse(text);
} catch {
return text;
}
};
/** The nodes claude-code runs on, from the controller's list of modules — for the register tool's question. */
async function nodesRunningMe(): Promise<string[]> {
const out = await ask("mesh-controller.modules", {});
const text = typeof out === "string" ? out : String((out as { output?: string })?.output ?? "");
const line = text.split("\n").find((l) => /^claude-code\s/.test(l)) ?? "";
const on = line.split(" on ")[1] ?? "";
return on.trim() === "nothing" ? [] : on.split(",").map((s) => s.trim()).filter(Boolean);
}
function status(p: Paths): Record<string, unknown> {
const creds = readCredentials(join(p.home, ".claude", ".credentials.json"));
const grant = grantOf(creds);
const managed = ["managed-mcp.json", "managed-settings.json", "CLAUDE.md"].map((f) => {
try {
return { file: join(MANAGED_DIR, f), fingerprint: fingerprint(readFileSync(join(MANAGED_DIR, f), "utf8")) };
} catch {
return { file: join(MANAGED_DIR, f), fingerprint: null };
}
});
return {
node: p.node,
licence: readJson(join(p.state, "licence.json"), null),
token: grant ? { fingerprint: fingerprint(grant.accessToken), expiresAt: new Date(grant.expiresAt).toISOString(),
loginWaiting: holdsLogin(creds) } : null,
managed,
registered: Object.keys(registered(p)),
};
}
/** The module's MCP servers on the bus (ADR 0201): its own state, which every node of it watches. */
const servers = () => state<Record<string, unknown>>("servers") as unknown as ServerState;
/** What this node takes from that state, kept from the watch. One per process. */
let view: ServerView | null = null;
const viewOf = (p: Paths) => (view ??= new ServerView(p));
function tools(p: Paths): ToolDefinition[] {
const nodesArg = { type: "string", description: 'more nodes: "all" for every node running claude-code, or a comma-separated list; absent is this node only' };
const nodesOf = (v: unknown): Registration["nodes"] =>
v === undefined || v === "" ? undefined : v === "all" ? "all" : String(v).split(",").map((s) => s.trim()).filter(Boolean);
return [
{
name: "claude_code_status",
description: "Claude Code on this machine as the mesh configured it: the licence it holds and when its token expires, the managed files, the MCP servers registered here. Fingerprints only, never a token.",
input: {},
run: async () => status(p),
},
{
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.",
input: {},
run: async () => ({ rendered: renderNow(p, writeManaged) }),
},
{
name: "claude_code_pull",
description: "Ask the licence manager for this node's current token now and apply it, rather than waiting for its next event.",
input: {},
run: async () => pull(p, ask, writeManaged),
},
{
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.",
input: {},
run: async () => ({ here: registered(p), everywhere: await servers().keys() }),
},
{
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: {
name: { type: "string", description: "the server's name: letters, digits, - and _" },
type: { type: "string", description: "http, sse or stdio (default stdio when a command is given, http when a url is)" },
url: { type: "string", description: "an http or sse server's url" },
command: { type: "string", description: "a stdio server's program" },
args: { type: "array", description: "a stdio server's arguments" },
env: { type: "object", description: "a stdio server's environment" },
headers: { type: "object", description: "an http server's headers" },
nodes: nodesArg,
},
run: async (a) => {
const entry: Record<string, unknown> = { type: a.type ?? (a.url ? "http" : "stdio") };
for (const k of ["url", "command", "args", "env", "headers"]) if (a[k] !== undefined) entry[k] = a[k];
return registerServer(p, { name: String(a.name ?? ""), entry, nodes: nodesOf(a.nodes) }, servers(), viewOf(p), writeManaged, nodesRunningMe);
},
},
{
name: "claude_code_mcp_unregister",
description: "Remove an MCP server registered through this module, on this node or more.",
input: { name: { type: "string", description: "the server's name" }, nodes: nodesArg },
run: async (a) => registerServer(p, { name: String(a.name ?? ""), nodes: nodesOf(a.nodes) }, servers(), viewOf(p), writeManaged, nodesRunningMe),
},
];
}
registerModuleTools("claude-code", (env) => {
const p = pathsFrom(env);
if (!p) return [];
try {
keypair(p);
for (const line of renderNow(p, writeManaged)) if (!line.endsWith("unchanged")) say(line);
} catch (err) {
say(err instanceof Error ? err.message : String(err));
}
return tools(p);
});
// Launched by the runtime: the bus is there from the first line (ADR 0198). Outside it — a test, a
// build — nothing below runs.
const p = process.env.MESH_SERVED_MODULE ? pathsFrom(process.env) : null;
if (p) {
const loud = (what: string) => (err: unknown) => say(`${what}: ${err instanceof Error ? err.message : String(err)}`);
void on<{ licence?: string; node?: string }>("claude-licence-manager.licence.*", async (event) => {
if (!concerns(p, event.type, event.body ?? {})) return;
say(`${event.type} — asking ${SEAT} for this node's token`);
say(JSON.stringify(await pull(p, ask, writeManaged).catch((e) => ({ failed: String(e) }))));
}).catch(loud("the licence events"));
// Every node's MCP servers: the whole current set first, then each change (ADR 0201). **Not awaited
// where the module is imported**: the runtime waits on the handshake, and a bucket that is not on the
// bus yet — or a grant the bus has not reloaded — answers late; awaited here, that left the bundle
// unable to answer `initialize` in time and the module unserved (found on its first assignment). So it
// watches beside the handshake and asks again until the state answers; until then the managed
// directory holds what the file kept from the last run.
const watchServers = (attempt = 0): void => {
state<Record<string, unknown>>("servers").watch((c) => {
try {
const done = onServerChange(viewOf(p), c as ServerChange, p, writeManaged);
if (done) say(done);
} catch (err) {
loud(`taking ${c.op} ${c.key}`)(err); // the view took it; the next render writes it
}
}).then(
() => say(`watching the MCP servers${attempt ? ` (after ${attempt} refusal(s))` : ""}`),
(err) => {
const wait = [2, 5, 10, 30][attempt] ?? 60;
say(`the MCP servers cannot be watched yet (${err instanceof Error ? err.message : String(err)}); asking again in ${wait}s`);
setTimeout(() => watchServers(attempt + 1), wait * 1000);
});
};
watchServers();
// Catch up once at start: a node that was off takes its current token now.
void pull(p, ask, writeManaged).then((r) => say(`at start: ${JSON.stringify(r)}`), loud("asking for this node's token at start"));
// A login: a refresh token appears in the credentials file. Polled, because the file is replaced by
// rename and a watch on the old inode would go quiet.
const credentials = join(p.home, ".claude", ".credentials.json");
watchFile(credentials, { interval: 5000 }, () => {
void offerLogin(p, ask).then((r) => { if (r) say(`a login here was offered to ${SEAT}: ${JSON.stringify(r)}`); },
loud("offering a login to the licence manager"));
});
}
-12
View File
@@ -1,12 +0,0 @@
{
"compilerOptions": {
"target": "ES2022",
"module": "NodeNext",
"moduleResolution": "NodeNext",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"noEmit": true
},
"include": ["seal.ts", "grant.ts", "identity.ts", "render.ts", "node.ts", "tools/index.ts"]
}
+55
View File
@@ -0,0 +1,55 @@
# claude-licence-manager
Holds the `anthropic-licence-manager` seat: every Anthropic licence the mesh has, kept alive by one
rotation source, and handed to each consumer sealed (novox/hq ADR 0183, ADR 0206, design 39).
## How a licence comes to exist
Nothing is configured. Every node running `claude-code` reports what it holds as that module's
`holdings` state — the account, fingerprints and expiries, never a token. This module reads every report
when it starts and watches them:
1. A report with a refresh token it does not hold is a **candidate**.
2. It asks that node's `claude_code_grant`, giving its public key, and receives the grant sealed to it.
3. **It refreshes it.** If the vendor exchanges the token, the grant is this module's — encrypted at rest
with the key the vault made for it — and from then on it is the only refresher. If not, the candidate
is recorded dead and nothing is adopted.
4. Several nodes logged in to one account: newest login first; the rest are never exchanged.
5. A node reporting that account and bound to nothing is bound to it.
Each node is then handed an access token only, so the agent there never refreshes, and a refresh token
appearing on a node later can only be a person's login — which wins if it refreshes.
An API key enters through `adopt`, from a file on this module's node.
## What each consumer holds
This module's `bindings` state: one key per consumer (a node's name) with the licence, its kind and a
generation that grows with every rotation and switch. `claude-code` watches its own key and, on a newer
generation, asks `current` with its public key.
## The seat's verbs
`licences`, `bindings`, `bind`, `switch`, `release`, `refresh`, `usage`, `adopt`, `current` — through
the console as `anthropic-licence-manager.<verb>`. No answer carries a token.
## Settings
`settings.json` in the state directory: `cadence_minutes` (240), `floor_minutes` (60),
`failures_to_notify` (3), `cooldown_hours` (24), `refresh_warn_days` (3).
## Events
`licence.adopted`, `licence.refused`, `licence.failing`, `usage.read` — none carries a secret.
## Code and tests
Go, one binary (`cmd/claude-licence-manager`): the seat's verbs and the daemon in one launched bundle,
`prepare` as the run-once preparation step. The sealed box is `claude-code`'s own format, byte for byte —
the two modules carry the same `seal.go` — and a test opens one sealed by the TypeScript agent module the
Go one replaced, so the format is the one already on the machines.
go test ./...
# the store against a real postgres:
docker run -d --rm --name licmgr-pg -e POSTGRES_PASSWORD=t -p 15498:5432 postgres:16-alpine
MESH_TEST_POSTGRES=postgres://postgres:t@127.0.0.1:15498/postgres go test ./...
@@ -0,0 +1,64 @@
package main
// The grants at rest (novox/hq ADR 0183): encrypted with a key the vault made for this module, so the
// store holds ciphertext and only this module, reading its own secret, can open a row. AES-256-GCM, the
// key derived from the vault's secret by SHA-256 so a secret of any length serves.
import (
"crypto/rand"
"crypto/sha256"
"encoding/base64"
"errors"
"os"
"strings"
)
// Crypt seals and opens what the store keeps.
type Crypt struct{ key []byte }
// NewCrypt is the store's cipher from the vault's secret.
func NewCrypt(secret string) (*Crypt, error) {
secret = strings.TrimSpace(secret)
if secret == "" {
return nil, errors.New("the key the grants are encrypted with is empty: the vault has not delivered it yet")
}
sum := sha256.Sum256([]byte(secret))
return &Crypt{key: sum[:]}, nil
}
// CryptFromFile reads the vault's secret from the file the mesh delivered it to.
func CryptFromFile(path string) (*Crypt, error) {
raw, err := os.ReadFile(path)
if err != nil {
return nil, err
}
return NewCrypt(string(raw))
}
// Seal encrypts a plaintext as `v1.<iv>.<ciphertext and tag>`.
func (c *Crypt) Seal(plaintext string) string {
gcm, _ := newGCM(c.key)
iv := make([]byte, 12)
_, _ = rand.Read(iv)
b64 := base64.StdEncoding.EncodeToString
return "v1." + b64(iv) + "." + b64(gcm.Seal(nil, iv, []byte(plaintext), nil))
}
// Open decrypts what Seal made with the same key.
func (c *Crypt) Open(sealed string) (string, error) {
parts := strings.Split(sealed, ".")
if len(parts) != 3 || parts[0] != "v1" {
return "", errors.New("not a grant this module sealed")
}
iv, err1 := base64.StdEncoding.DecodeString(parts[1])
ct, err2 := base64.StdEncoding.DecodeString(parts[2])
if err := errors.Join(err1, err2); err != nil {
return "", err
}
gcm, _ := newGCM(c.key)
plain, err := gcm.Open(nil, iv, ct, nil)
if err != nil {
return "", errors.New("the grant does not open with this module's key")
}
return string(plain), nil
}
@@ -0,0 +1,348 @@
// claude-licence-manager (novox/hq ADR 0183, ADR 0206, design 39): one binary, launched by the control
// node's runtime and speaking MCP to it over stdio through the Go SDK (ADR 0193, ADR 0198). It serves the
// `anthropic-licence-manager` seat's verbs and, beside them, runs long: it watches what every node reports
// holding and adopts a login it does not hold, keeps every grant alive under a lease, and reads usage.
//
// `claude-licence-manager prepare` is the preparation step (ADR 0135): the host runs it once before the
// version that needs it, with the module's words and no bus, and it brings the store's schema to shape.
//
// stdout is the MCP channel; everything this module says, it says on stderr.
package main
import (
"context"
"encoding/json"
"errors"
"fmt"
"os"
"path/filepath"
"sort"
"strings"
"sync"
"time"
stdio "git.novox.be/novox/mesh-sdk/go"
)
// Seat is the role this module holds.
const Seat = "anthropic-licence-manager"
func say(format string, args ...any) {
fmt.Fprintf(os.Stderr, "[claude-licence-manager] "+format+"\n", args...)
}
func main() {
if len(os.Args) > 1 && os.Args[1] == "prepare" {
if err := prepare(); err != nil {
say("preparing the store failed: %v", err)
os.Exit(1)
}
say("the store's schema is what this version needs")
return
}
go daemon()
if err := stdio.Serve("", tools()); err != nil {
say("%v", err)
os.Exit(1)
}
}
func prepare() error {
ctx, cancel := context.WithTimeout(context.Background(), time.Minute)
defer cancel()
store, err := PgStoreFromEnv(ctx)
if err != nil {
return err
}
defer store.Close()
return store.Migrate(ctx)
}
// ---- what the binary is handed -----------------------------------------------------------------------
var (
built *Manager
buildMu sync.Mutex
)
// manager builds the rules' dependencies once, from the module's words (ADR 0192): file paths, never
// values. A failure is said and tried again on the next call, so a database that arrives late is not fatal.
func manager() (*Manager, error) {
buildMu.Lock()
defer buildMu.Unlock()
if built != nil {
return built, nil
}
dir, keyFile := os.Getenv("MESH_LICENCE_STATE"), os.Getenv("MESH_LICENCE_KEY_FILE")
if dir == "" || keyFile == "" {
return nil, errors.New("MESH_LICENCE_STATE and MESH_LICENCE_KEY_FILE are not set: the mesh renders them for this module")
}
crypt, err := CryptFromFile(keyFile)
if err != nil {
return nil, err
}
keys, err := keypair(dir)
if err != nil {
return nil, err
}
store, err := PgStoreFromEnv(context.Background())
if err != nil {
return nil, err
}
node, _ := os.Hostname()
if n := os.Getenv("MESH_NODE"); n != "" {
node = n
}
bindings := stdio.State("bindings")
built = &Manager{
Store: store,
Vendor: LiveVendor(),
Crypt: crypt,
Keys: keys,
AskGrant: func(_ context.Context, node, publicKey string) (GrantAnswer, error) {
var a GrantAnswer
raw, err := stdio.Ask("claude-code.claude_code_grant@"+node, map[string]any{"public_key": publicKey})
if err != nil {
return a, err
}
return a, json.Unmarshal(raw, &a)
},
PutBinding: func(_ context.Context, consumer string, b BindingState) error {
_, err := bindings.Put(consumer, b)
return err
},
DeleteBinding: func(_ context.Context, consumer string) error { return bindings.Delete(consumer) },
Emit: func(event string, body map[string]any) error { return stdio.Emit(event, body) },
Now: time.Now,
Log: say,
Holder: fmt.Sprintf("%s:%d", node, os.Getpid()),
Settings: SettingsFrom(os.Getenv("MESH_LICENCE_SETTINGS")),
}
return built, nil
}
// keypair is this module's own, made once in its state directory; the private half never leaves it.
// Written whole, then linked into place, so a second process reads the first's key and never half of it.
func keypair(dir string) (KeyPair, error) {
file := filepath.Join(dir, "manager-key.json")
if _, err := os.Stat(file); errors.Is(err, os.ErrNotExist) {
k, err := GenerateKeyPair()
if err != nil {
return KeyPair{}, err
}
raw, _ := json.Marshal(k)
tmp := filepath.Join(dir, fmt.Sprintf(".manager-key.%d.json", os.Getpid()))
if err := os.WriteFile(tmp, raw, 0o600); err != nil {
return KeyPair{}, err
}
_ = os.Link(tmp, file) // fails when another made it first, which is right
_ = os.Remove(tmp)
}
raw, err := os.ReadFile(file)
if err != nil {
return KeyPair{}, err
}
var k KeyPair
return k, json.Unmarshal(raw, &k)
}
// ---- the long-running half ---------------------------------------------------------------------------
func daemon() {
var mu sync.Mutex
reports := map[string]Holdings{}
var passing sync.Mutex
pass := func() {
passing.Lock() // one pass at a time: each account is leased, and a pass is cheap
defer passing.Unlock()
m, err := manager()
if err != nil {
say("not ready: %v", err)
return
}
mu.Lock()
all := make([]Holdings, 0, len(reports))
for _, r := range reports {
all = append(all, r)
}
mu.Unlock()
sort.Slice(all, func(i, j int) bool { return all[i].Node < all[j].Node })
adopted, err := m.Consider(context.Background(), all)
if err != nil {
say("considering the reports failed: %v", err)
}
if len(adopted) > 0 {
say("adopted %s", strings.Join(adopted, ", "))
}
}
// What every node holds (ADR 0206): the whole current set, then each change. Asked again until it
// answers — the channel to the runtime opens as the bundle starts, and the agent module's state may
// arrive after this one.
go func() {
waits := []time.Duration{2 * time.Second, 5 * time.Second, 10 * time.Second, 30 * time.Second}
for attempt := 0; ; attempt++ {
err := stdio.State("claude-code.holdings").Watch("", func(c stdio.StateChange) error {
mu.Lock()
if c.Op == "put" {
var h Holdings
if json.Unmarshal(c.Value, &h) == nil {
reports[c.Key] = h
}
} else {
delete(reports, c.Key)
}
mu.Unlock()
// During the current values the pass waits for the whole set: newest login first needs all.
if !c.Current {
go pass()
}
return nil
})
if err == nil {
mu.Lock()
n := len(reports)
mu.Unlock()
say("watching what %d node(s) hold", n)
pass()
return
}
pause := time.Minute
if attempt < len(waits) {
pause = waits[attempt]
}
say("what the nodes hold cannot be watched yet (%v); asking again in %s", err, pause)
time.Sleep(pause)
}
}()
refresh := time.NewTicker(time.Minute)
usage := time.NewTicker(5 * time.Minute)
for {
select {
case <-refresh.C:
if m, err := manager(); err == nil {
out, err := m.RotateDue(context.Background())
for _, r := range out {
b, _ := json.Marshal(r)
say("%s", b)
}
if err != nil {
say("refreshing failed: %v", err)
}
}
pass() // a login whose node did not answer last time is asked again
case <-usage.C:
if m, err := manager(); err == nil {
if err := m.ReadUsage(context.Background()); err != nil {
say("reading usage failed: %v", err)
}
}
}
}
}
// ---- the seat's verbs --------------------------------------------------------------------------------
func text(a map[string]any, k string) (string, error) {
v, _ := a[k].(string)
if strings.TrimSpace(v) == "" {
return "", fmt.Errorf("%s is required", k)
}
return strings.TrimSpace(v), nil
}
// verb is one of the seat's verbs: listed as `<seat>.<verb>`, so the runtime serves it on the seat's subject.
func verb(name, description string, input map[string]any, run func(ctx context.Context, m *Manager, a map[string]any) (any, error)) stdio.Tool {
return stdio.Tool{Name: Seat + "." + name, Description: description, Input: input,
Run: func(a map[string]any) (any, error) {
m, err := manager()
if err != nil {
return nil, err
}
return run(context.Background(), m, a)
}}
}
func str(description string) map[string]any {
return map[string]any{"type": "string", "description": description}
}
func two(a map[string]any, k1, k2 string) (string, string, error) {
v1, err1 := text(a, k1)
v2, err2 := text(a, k2)
return v1, v2, errors.Join(err1, err2)
}
func tools() []stdio.Tool {
consumer := str("the node's name")
return []stdio.Tool{
verb("licences", "Every licence the manager holds — account, kind, when its token and its refresh token expire, failures in a row, which consumers are bound to it. Never a token.",
nil, func(ctx context.Context, m *Manager, _ map[string]any) (any, error) { return m.Licences(ctx) }),
verb("bindings", "Which licence each consumer (a node's agent, by the node's name) is bound to, and the generation it was last given.",
nil, func(ctx context.Context, m *Manager, _ map[string]any) (any, error) { return m.Store.Bindings(ctx) }),
verb("bind", "Bind a consumer — a node's agent, by the node's name — to a licence. Its node fetches the licence's token at once.",
map[string]any{"consumer": consumer, "licence": str("a licence, as `licences` names it")},
func(ctx context.Context, m *Manager, a map[string]any) (any, error) {
c, l, err := two(a, "consumer", "licence")
if err != nil {
return nil, err
}
return m.Bind(ctx, c, l, "bind")
}),
verb("switch", "Move a consumer to another licence. Its node fetches the new licence's token at once and points the agent's account at it.",
map[string]any{"consumer": consumer, "licence": str("the licence to move to")},
func(ctx context.Context, m *Manager, a map[string]any) (any, error) {
c, l, err := two(a, "consumer", "licence")
if err != nil {
return nil, err
}
return m.Bind(ctx, c, l, "switch")
}),
verb("release", "Unbind a consumer. Its node keeps its last token, which expires within hours.",
map[string]any{"consumer": consumer},
func(ctx context.Context, m *Manager, a map[string]any) (any, error) {
c, err := text(a, "consumer")
if err != nil {
return nil, err
}
return m.Release(ctx, c, "release")
}),
verb("refresh", "Refresh a licence now, or every due licence when none is named; under each licence's lease, so it never races the daemon. Answers the outcome, never a token.",
map[string]any{"licence": str("a licence; absent for every due one")},
func(ctx context.Context, m *Manager, a map[string]any) (any, error) {
if l, _ := a["licence"].(string); l != "" {
return m.Rotate(ctx, l, true)
}
return m.RotateDue(ctx)
}),
verb("usage", "Usage readings, the latest first, for every licence or one.",
map[string]any{"licence": str("a licence; absent for all"), "limit": map[string]any{"type": "number", "description": "how many readings (default 20)"}},
func(ctx context.Context, m *Manager, a map[string]any) (any, error) {
l, _ := a["licence"].(string)
limit := 20
if v, ok := a["limit"].(float64); ok && v > 0 {
limit = int(v)
}
return m.Store.Usage(ctx, l, limit)
}),
verb("adopt", "Adopt an API key from a file on the manager's node, never as an argument. Subscriptions are adopted from the nodes' logins by themselves.",
map[string]any{"name": str("the licence's name"), "file": str("a file on the manager's node holding the key")},
func(ctx context.Context, m *Manager, a map[string]any) (any, error) {
n, f, err := two(a, "name", "file")
if err != nil {
return nil, err
}
return m.AdoptKey(ctx, n, f)
}),
verb("current", "For a consumer's agent module (ADR 0206): its token, sealed to the public key it sends, with the licence, kind and generation. Null when it is bound to nothing.",
map[string]any{"consumer": consumer, "public_key": str("the consumer's public key, PEM")},
func(ctx context.Context, m *Manager, a map[string]any) (any, error) {
c, k, err := two(a, "consumer", "public_key")
if err != nil {
return nil, err
}
return m.Current(ctx, c, k)
}),
}
}
@@ -0,0 +1,640 @@
package main
// The Anthropic licence manager's rules (novox/hq ADR 0183, ADR 0206, design 39), written against what it
// is handed — a store, the vendor, a way to ask a node, its own state, a way to emit — so every rule is
// tested without a bus, a database or the vendor.
//
// - A licence is an account, learned from what the nodes report (`holdings`, the agent module's state).
// A report with a refresh token the manager does not hold is a candidate.
// - The secret is asked for, sealed to this module's key, never published.
// - Adopting is refreshing: newest login first, once per account; a failure adopts nothing.
// - What each consumer should hold is this module's `bindings` state, with a generation that grows with
// every rotation and switch; the consumer fetches its token by asking `current`.
// - One rotation source: every exchange with the vendor runs under a lease.
import (
"context"
"crypto/sha256"
"encoding/hex"
"encoding/json"
"errors"
"fmt"
"os"
"regexp"
"sort"
"strings"
"time"
)
// Fingerprint names a token without being one: the agent module's own fingerprint, so the two compare.
func Fingerprint(s string) string {
sum := sha256.Sum256([]byte(s))
return "sha256:" + hex.EncodeToString(sum[:])[:16]
}
// Identity is the account a grant belongs to, as the agent's own state file names it.
type Identity struct {
AccountUUID string `json:"accountUuid"`
EmailAddress string `json:"emailAddress,omitempty"`
OrganizationUUID string `json:"organizationUuid,omitempty"`
}
// Holdings is one node's report, as the agent module writes it to its `holdings` state (ADR 0206).
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"`
} `json:"refresh"`
ChangedAt string `json:"changedAt"`
}
// BindingState is what `bindings` holds for one consumer: no secret, only what it should hold and which
// generation.
type BindingState struct {
Licence string `json:"licence"`
Kind string `json:"kind"`
Generation int64 `json:"generation"`
}
// Settings are the manager's own, declared with defaults (design 39 §8).
type Settings struct {
Cadence time.Duration // rotate a grant once older than this
Floor time.Duration // refresh in any case with less than this left
FailuresToNotify int
Cooldown time.Duration // at most one notification per licence in this window
RefreshWarn time.Duration // warn this long before a refresh token itself expires
}
// Defaults are the settings a fresh mesh runs with.
var Defaults = Settings{Cadence: 4 * time.Hour, Floor: time.Hour, FailuresToNotify: 3, Cooldown: 24 * time.Hour, RefreshWarn: 72 * time.Hour}
// SettingsFrom reads the settings file, keeping a default for anything absent or not positive.
func SettingsFrom(path string) Settings {
s := Defaults
raw, err := os.ReadFile(path)
if err != nil {
return s
}
var f map[string]float64
if json.Unmarshal(raw, &f) != nil {
return s
}
set := func(k string, unit time.Duration, into *time.Duration) {
if v := f[k]; v > 0 {
*into = time.Duration(v * float64(unit))
}
}
set("cadence_minutes", time.Minute, &s.Cadence)
set("floor_minutes", time.Minute, &s.Floor)
set("cooldown_hours", time.Hour, &s.Cooldown)
if v := f["refresh_warn_days"]; v > 0 {
s.RefreshWarn = time.Duration(v * float64(24*time.Hour))
}
if v := f["failures_to_notify"]; v > 0 {
s.FailuresToNotify = int(v)
}
return s
}
// GrantAnswer is what a node's `claude_code_grant` answers: a login sealed to the key given, or nothing.
type GrantAnswer struct {
Sealed *SealedBox `json:"sealed"`
Identity *Identity `json:"identity"`
Fingerprint string `json:"fingerprint"`
}
// Manager is the rules and what they are handed.
type Manager struct {
Store Store
Vendor Vendor
Crypt *Crypt
Keys KeyPair
// AskGrant asks a node's agent module for the grant a login left there, sealed to publicKey. An error
// is the node not answering; a nil Sealed is no login waiting.
AskGrant func(ctx context.Context, node, publicKey string) (GrantAnswer, error)
// PutBinding and DeleteBinding are this module's `bindings` state.
PutBinding func(ctx context.Context, consumer string, b BindingState) error
DeleteBinding func(ctx context.Context, consumer string) error
Emit func(event string, body map[string]any) error
Now func() time.Time
Log func(format string, args ...any)
// Holder names this process in a lease, so a second run is told apart.
Holder string
Settings Settings
}
const leaseFor = 2 * time.Minute
// NameFor is a licence's name: the account's address where it has one, else its id.
func NameFor(id Identity) string {
if e := strings.TrimSpace(id.EmailAddress); e != "" {
return e
}
return id.AccountUUID
}
// ---- learning licences from what the nodes hold ------------------------------------------------------
// Candidate is a report the manager should try.
type Candidate struct {
Node string
Identity Identity
Fingerprint string
ChangedAt int64
}
// CandidatesIn is the candidates in a set of reports by account, newest login first (ADR 0206 §2, §4). A
// report without an identity is not one: a grant is filed under its account or not at all.
func (m *Manager) CandidatesIn(ctx context.Context, reports []Holdings) (map[string][]Candidate, error) {
out := map[string][]Candidate{}
for _, r := range reports {
if !r.Refresh.Present || r.Refresh.Fingerprint == "" || r.Identity == nil || r.Identity.AccountUUID == "" {
continue
}
settled, err := m.Store.Outcome(ctx, r.Refresh.Fingerprint)
if err != nil {
return nil, err
}
if settled != "" {
continue // adopted, dead, skipped, refused or gone: settled once
}
held, err := m.Store.LicenceForAccount(ctx, r.Identity.AccountUUID)
if err != nil {
return nil, err
}
if held != nil && held.RefreshFingerprint == r.Refresh.Fingerprint {
continue
}
var changed int64
if t, err := time.Parse(time.RFC3339Nano, r.ChangedAt); err == nil {
changed = t.UnixMilli()
}
// The latest login wins: one no newer than the grant held is not a newer login.
if held != nil && changed <= held.AdoptedAt {
continue
}
out[r.Identity.AccountUUID] = append(out[r.Identity.AccountUUID],
Candidate{Node: r.Node, Identity: *r.Identity, Fingerprint: r.Refresh.Fingerprint, ChangedAt: changed})
}
for _, list := range out {
sort.SliceStable(list, func(i, j int) bool { return list[i].ChangedAt > list[j].ChangedAt })
}
return out, nil
}
// Consider every report (ADR 0206): for each account with candidates, under that account's lease, try them
// newest first; the first that refreshes is adopted and the rest are settled as skipped without being
// exchanged. Answers what was adopted.
func (m *Manager) Consider(ctx context.Context, reports []Holdings) ([]string, error) {
byAccount, err := m.CandidatesIn(ctx, reports)
if err != nil {
return nil, err
}
accounts := make([]string, 0, len(byAccount))
for a := range byAccount {
accounts = append(accounts, a)
}
sort.Strings(accounts)
var adopted []string
for _, account := range accounts {
list := byAccount[account]
key := "account:" + account
ok, err := m.Store.Lease(ctx, key, m.Holder, leaseFor)
if err != nil || !ok {
continue
}
for i, c := range list {
name, err := m.adoptOne(ctx, c, reports)
if err != nil {
m.Log("adopting %s's login failed: %v", c.Node, err)
continue
}
if name != "" {
adopted = append(adopted, name)
for _, rest := range list[i+1:] {
_ = m.Store.RecordOutcome(ctx, rest.Fingerprint, rest.Node, account, Skipped, c.Node+"'s newer login was adopted first")
}
break
}
}
_ = m.Store.Unlease(ctx, key, m.Holder)
}
return adopted, nil
}
func (m *Manager) adoptOne(ctx context.Context, c Candidate, reports []Holdings) (string, error) {
answer, err := m.AskGrant(ctx, c.Node, m.Keys.PublicKey)
if err != nil {
// Not answering is not an answer: asked again on the next pass.
m.Log("%s did not hand over its login: %v", c.Node, err)
return "", nil
}
if answer.Sealed == nil {
return "", m.Store.RecordOutcome(ctx, c.Fingerprint, c.Node, c.Identity.AccountUUID, Gone, "no login was waiting when asked")
}
plain, err := Open(*answer.Sealed, m.Keys.PrivateKey)
if err != nil {
return "", m.Store.RecordOutcome(ctx, c.Fingerprint, c.Node, c.Identity.AccountUUID, Refused, "the grant did not open with this module's key")
}
var offered FullGrant
if err := json.Unmarshal([]byte(plain), &offered); err != nil || offered.RefreshToken == "" {
return "", m.Store.RecordOutcome(ctx, c.Fingerprint, c.Node, c.Identity.AccountUUID, Refused, "the grant holds no refresh token")
}
if Fingerprint(offered.RefreshToken) != c.Fingerprint {
m.Log("%s's login changed while it was asked for; waiting for its next report", c.Node)
return "", nil
}
// Adopting is refreshing (ADR 0206 §4): the exchange is the check, and from here this module is the
// only holder of a live refresh token for the account.
r := m.Vendor.Refresh(ctx, offered)
if !r.OK {
_ = m.Store.RecordOutcome(ctx, c.Fingerprint, c.Node, c.Identity.AccountUUID, Dead, fmt.Sprintf("%d %s", r.Status, r.Reason))
_ = m.Store.Audit(ctx, "refused", map[string]any{"node": c.Node, "account": c.Identity.AccountUUID, "status": r.Status, "reason": r.Reason})
_ = m.Emit("licence.refused", map[string]any{"node": c.Node, "account": NameFor(c.Identity),
"reason": fmt.Sprintf("the login's refresh token did not refresh (%d)", r.Status)})
m.Log("%s's login for %s did not refresh: %d %s", c.Node, NameFor(c.Identity), r.Status, r.Reason)
return "", nil
}
// The identity guard, on two sources (ADR 0206 §8).
if r.Account != "" && r.Account != c.Identity.AccountUUID {
_ = m.Store.RecordOutcome(ctx, c.Fingerprint, c.Node, c.Identity.AccountUUID, Refused,
"the node says "+c.Identity.AccountUUID+", the vendor says "+r.Account)
_ = m.Store.Audit(ctx, "refused", map[string]any{"node": c.Node, "reported": c.Identity.AccountUUID, "vendor": r.Account})
_ = m.Emit("licence.refused", map[string]any{"node": c.Node, "account": NameFor(c.Identity),
"reason": "the account the node reported is not the one the vendor answered for"})
return "", nil
}
held, err := m.Store.LicenceForAccount(ctx, c.Identity.AccountUUID)
if err != nil {
return "", err
}
now := m.Now().UnixMilli()
l := Licence{Name: NameFor(c.Identity), Kind: "subscription", AccountUUID: c.Identity.AccountUUID,
Email: c.Identity.EmailAddress, OrganizationUUID: c.Identity.OrganizationUUID}
if held != nil {
l.Name, l.NotifiedAt = held.Name, held.NotifiedAt
if l.Email == "" {
l.Email = held.Email
}
if l.OrganizationUUID == "" {
l.OrganizationUUID = held.OrganizationUUID
}
}
m.keep(&l, r.Grant)
l.AdoptedAt = max(now, c.ChangedAt)
if err := m.Store.SaveLicence(ctx, l); err != nil {
return "", err
}
why := "a new licence"
if held != nil {
why = "a newer login"
}
_ = m.Store.RecordOutcome(ctx, c.Fingerprint, c.Node, c.Identity.AccountUUID, Adopted, why)
_ = m.Store.Audit(ctx, "adopted", map[string]any{"licence": l.Name, "node": c.Node, "account": c.Identity.AccountUUID,
"vendorNamedAccount": r.Account != ""})
// A first binding follows the login (ADR 0206 §7): every node reporting this account and bound to nothing.
for _, rep := range reports {
if rep.Identity == nil || rep.Identity.AccountUUID != c.Identity.AccountUUID {
continue
}
if b, err := m.Store.Binding(ctx, rep.Node); err != nil || b != nil {
continue
}
if _, err := m.Store.Bind(ctx, rep.Node, l.Name); err == nil {
_ = m.Store.Audit(ctx, "bound", map[string]any{"consumer": rep.Node, "licence": l.Name, "by": "its login"})
}
}
if err := m.publishAdvance(ctx, l); err != nil {
return "", err
}
_ = m.Emit("licence.adopted", map[string]any{"licence": l.Name, "from": c.Node, "replaced": held != nil})
m.Log("adopted %s from %s (%s)", l.Name, c.Node, why)
return l.Name, nil
}
// keep stores a grant on its licence: encrypted, fingerprinted, its expiries, fresh.
func (m *Manager) keep(l *Licence, g FullGrant) {
raw, _ := json.Marshal(g)
l.Sealed = m.Crypt.Seal(string(raw))
l.RefreshFingerprint = Fingerprint(g.RefreshToken)
l.AccessExpiresAt = g.ExpiresAt
if g.RefreshTokenExpiresAt != nil {
l.RefreshExpiresAt = *g.RefreshTokenExpiresAt
}
l.Failures = 0
l.RotatedAt = m.Now().UnixMilli()
}
// publishAdvance gives every consumer of a licence a new generation, and tells each in the state.
func (m *Manager) publishAdvance(ctx context.Context, l Licence) error {
bindings, err := m.Store.Advance(ctx, l.Name)
if err != nil {
return err
}
for _, b := range bindings {
if err := m.PutBinding(ctx, b.Consumer, BindingState{Licence: b.Licence, Kind: l.Kind, Generation: b.Generation}); err != nil {
return err
}
}
return nil
}
// ---- keeping grants alive ----------------------------------------------------------------------------
// Due says whether a licence needs a refresh now: near expiry, or older than the cadence.
func Due(l Licence, now time.Time, s Settings) bool {
if l.Kind != "subscription" || l.Sealed == "" {
return false
}
ms := now.UnixMilli()
if l.AccessExpiresAt != 0 && l.AccessExpiresAt-ms < s.Floor.Milliseconds() {
return true
}
return l.RotatedAt == 0 || ms-l.RotatedAt >= s.Cadence.Milliseconds()
}
// Rotate refreshes one licence under its lease (design 39 §3). A second run started together finds the
// lease live and does nothing; one started just after finds a fresh grant and is not due. force refreshes
// whatever the age — the seat's `refresh` verb.
func (m *Manager) Rotate(ctx context.Context, name string, force bool) (map[string]any, error) {
key := "licence:" + name
ok, err := m.Store.Lease(ctx, key, m.Holder, leaseFor)
if err != nil {
return nil, err
}
if !ok {
return map[string]any{"licence": name, "refreshed": false, "reason": "another run holds its lease"}, nil
}
defer func() { _ = m.Store.Unlease(ctx, key, m.Holder) }()
l, err := m.Store.Licence(ctx, name)
if err != nil {
return nil, err
}
if l == nil {
return map[string]any{"licence": name, "refreshed": false, "reason": "no such licence"}, nil
}
if l.Kind != "subscription" || l.Sealed == "" {
return map[string]any{"licence": name, "refreshed": false, "reason": "an API key does not refresh"}, nil
}
now := m.Now()
if !force && !Due(*l, now, m.Settings) {
return map[string]any{"licence": name, "refreshed": false, "reason": "not due"}, nil
}
plain, err := m.Crypt.Open(l.Sealed)
if err != nil {
return nil, err
}
var g FullGrant
if err := json.Unmarshal([]byte(plain), &g); err != nil {
return nil, err
}
r := m.Vendor.Refresh(ctx, g)
if !r.OK {
l.Failures++
m.Log("%s did not refresh (%d in a row): %d %s", name, l.Failures, r.Status, r.Reason)
_ = m.Store.Audit(ctx, "failed", map[string]any{"licence": name, "status": r.Status, "reason": r.Reason, "failures": l.Failures})
m.notify(l, fmt.Sprintf("refresh failed %d time(s) in a row: %d", l.Failures, r.Status), l.Failures >= m.Settings.FailuresToNotify)
if err := m.Store.SaveLicence(ctx, *l); err != nil {
return nil, err
}
return map[string]any{"licence": name, "refreshed": false, "reason": fmt.Sprintf("%d %s", r.Status, r.Reason), "failures": l.Failures}, nil
}
m.keep(l, r.Grant)
if l.RefreshExpiresAt != 0 {
left := time.Duration(l.RefreshExpiresAt-now.UnixMilli()) * time.Millisecond
m.notify(l, fmt.Sprintf("its refresh token expires in %.1f day(s): a person must log in again", left.Hours()/24),
left < m.Settings.RefreshWarn)
}
if err := m.Store.SaveLicence(ctx, *l); err != nil {
return nil, err
}
_ = m.Store.Audit(ctx, "rotated", map[string]any{"licence": name, "expiresAt": l.AccessExpiresAt})
if err := m.publishAdvance(ctx, *l); err != nil {
return nil, err
}
return map[string]any{"licence": name, "refreshed": true, "expiresAt": time.UnixMilli(l.AccessExpiresAt).UTC().Format(time.RFC3339)}, nil
}
// notify emits `licence.failing` at most once per cooldown (design 39 §3); it carries no secret.
func (m *Manager) notify(l *Licence, why string, when bool) {
if !when {
return
}
now := m.Now().UnixMilli()
if l.NotifiedAt != 0 && now-l.NotifiedAt < m.Settings.Cooldown.Milliseconds() {
return
}
l.NotifiedAt = now
_ = m.Emit("licence.failing", map[string]any{"licence": l.Name, "why": why})
}
// RotateDue refreshes every licence that is due.
func (m *Manager) RotateDue(ctx context.Context) ([]map[string]any, error) {
all, err := m.Store.Licences(ctx)
if err != nil {
return nil, err
}
var out []map[string]any
for _, l := range all {
if Due(l, m.Now(), m.Settings) {
r, err := m.Rotate(ctx, l.Name, false)
if err != nil {
return out, err
}
out = append(out, r)
}
}
return out, nil
}
// ReadUsage reads and records each subscription's usage (ADR 0054); the event names the licence and numbers.
func (m *Manager) ReadUsage(ctx context.Context) error {
all, err := m.Store.Licences(ctx)
if err != nil {
return err
}
for _, l := range all {
if l.Kind != "subscription" || l.Sealed == "" {
continue
}
plain, err := m.Crypt.Open(l.Sealed)
if err != nil {
return err
}
var g FullGrant
_ = json.Unmarshal([]byte(plain), &g)
raw, err := m.Vendor.Usage(ctx, g.AccessToken)
if err != nil || raw == nil {
continue
}
reading := FlattenUsage(raw)
if err := m.Store.RecordUsage(ctx, l.Name, m.Now().UnixMilli(), reading, raw); err != nil {
return err
}
_ = m.Emit("usage.read", map[string]any{"licence": l.Name, "sessionPct": reading.SessionPct,
"weeklyPct": reading.WeeklyPct, "sonnetPct": reading.SonnetPct})
}
return nil
}
// ---- the seat's verbs --------------------------------------------------------------------------------
// Current is a consumer's token sealed to the key it sent (ADR 0206 §6): for a subscription the access
// token and its expiries only — never the refresh token, which no node holds. Nil when it is bound to
// nothing.
func (m *Manager) Current(ctx context.Context, consumer, publicKey string) (map[string]any, error) {
if !strings.Contains(publicKey, "PUBLIC KEY") {
return nil, errors.New("current seals to the consumer's public key, and none was given")
}
b, err := m.Store.Binding(ctx, consumer)
if err != nil || b == nil {
return nil, err
}
l, err := m.Store.Licence(ctx, b.Licence)
if err != nil || l == nil || l.Sealed == "" {
return nil, err
}
plain, err := m.Crypt.Open(l.Sealed)
if err != nil {
return nil, err
}
handed := plain
if l.Kind == "subscription" {
var g FullGrant
if err := json.Unmarshal([]byte(plain), &g); err != nil {
return nil, err
}
access, _ := json.Marshal(map[string]any{"accessToken": g.AccessToken, "expiresAt": g.ExpiresAt,
"refreshTokenExpiresAt": g.RefreshTokenExpiresAt, "scopes": g.Scopes,
"subscriptionType": g.SubscriptionType, "rateLimitTier": g.RateLimitTier})
handed = string(access)
}
box, err := Seal(handed, publicKey)
if err != nil {
return nil, err
}
out := map[string]any{"licence": l.Name, "kind": l.Kind, "generation": b.Generation, "sealed": box}
if l.AccountUUID != "" {
out["identity"] = Identity{AccountUUID: l.AccountUUID, EmailAddress: l.Email, OrganizationUUID: l.OrganizationUUID}
}
return out, nil
}
// Bind binds or switches a consumer: a person's act (ADR 0183), told to the consumer as a new generation.
func (m *Manager) Bind(ctx context.Context, consumer, licence, by string) (map[string]any, error) {
l, err := m.Store.Licence(ctx, licence)
if err != nil {
return nil, err
}
if l == nil {
return nil, fmt.Errorf("there is no licence %s; `licences` lists them", licence)
}
before, err := m.Store.Binding(ctx, consumer)
if err != nil {
return nil, err
}
b, err := m.Store.Bind(ctx, consumer, licence)
if err != nil {
return nil, err
}
from, what := "", "bound"
if before != nil {
from, what = before.Licence, "switched"
}
_ = m.Store.Audit(ctx, what, map[string]any{"consumer": consumer, "licence": licence, "from": from, "by": by})
if err := m.PutBinding(ctx, consumer, BindingState{Licence: licence, Kind: l.Kind, Generation: b.Generation}); err != nil {
return nil, err
}
return map[string]any{"consumer": consumer, "licence": licence, "generation": b.Generation, "from": from}, nil
}
// Release unbinds a consumer; its node keeps its last token, which expires within hours.
func (m *Manager) Release(ctx context.Context, consumer, by string) (map[string]any, error) {
was, err := m.Store.Binding(ctx, consumer)
if err != nil {
return nil, err
}
if ok, err := m.Store.Unbind(ctx, consumer); err != nil || !ok {
return map[string]any{"consumer": consumer, "released": false, "reason": "it was bound to nothing"}, err
}
if err := m.DeleteBinding(ctx, consumer); err != nil {
return nil, err
}
_ = m.Store.Audit(ctx, "released", map[string]any{"consumer": consumer, "licence": was.Licence, "by": by})
return map[string]any{"consumer": consumer, "released": true, "was": was.Licence}, nil
}
var licenceName = regexp.MustCompile(`^[A-Za-z0-9@._-]+$`)
// AdoptKey adopts an API key from a file on this node — never an argument (design 39 §6).
func (m *Manager) AdoptKey(ctx context.Context, name, file string) (map[string]any, error) {
if !licenceName.MatchString(name) {
return nil, fmt.Errorf("%q is not a licence name: letters, digits and @._-", name)
}
raw, err := os.ReadFile(file)
if err != nil {
return nil, err
}
key := strings.TrimSpace(string(raw))
if key == "" {
return nil, fmt.Errorf("%s is empty", file)
}
held, err := m.Store.Licence(ctx, name)
if err != nil {
return nil, err
}
if held != nil && held.Kind != "api-key" {
return nil, fmt.Errorf("%s is a subscription; an API key needs a name of its own", name)
}
l := Licence{Name: name, Kind: "api-key", Sealed: m.Crypt.Seal(key), AdoptedAt: m.Now().UnixMilli()}
if err := m.Store.SaveLicence(ctx, l); err != nil {
return nil, err
}
_ = m.Store.Audit(ctx, "adopted", map[string]any{"licence": name, "kind": "api-key", "from": "a file"})
if err := m.publishAdvance(ctx, l); err != nil {
return nil, err
}
_ = m.Emit("licence.adopted", map[string]any{"licence": name, "from": "a file", "replaced": held != nil})
return map[string]any{"licence": name, "kind": "api-key", "adopted": true, "fingerprint": Fingerprint(key)}, nil
}
// Licences is each licence as a person reads it: health and who is bound, never a token.
func (m *Manager) Licences(ctx context.Context) ([]map[string]any, error) {
all, err := m.Store.Licences(ctx)
if err != nil {
return nil, err
}
bindings, err := m.Store.Bindings(ctx)
if err != nil {
return nil, err
}
stamp := func(ms int64) any {
if ms == 0 {
return nil
}
return time.UnixMilli(ms).UTC().Format(time.RFC3339)
}
out := []map[string]any{}
for _, l := range all {
bound := []string{}
for _, b := range bindings {
if b.Licence == l.Name {
bound = append(bound, b.Consumer)
}
}
account := l.Email
if account == "" {
account = l.AccountUUID
}
out = append(out, map[string]any{"name": l.Name, "kind": l.Kind, "account": account,
"accessExpiresAt": stamp(l.AccessExpiresAt), "refreshExpiresAt": stamp(l.RefreshExpiresAt),
"rotatedAt": stamp(l.RotatedAt), "failures": l.Failures, "bound": bound})
}
return out, nil
}
@@ -0,0 +1,338 @@
package main
import (
"context"
"encoding/json"
"fmt"
"os"
"path/filepath"
"strings"
"sync"
"testing"
"time"
)
var t0 = time.Date(2026, 10, 4, 12, 0, 0, 0, time.UTC)
// stubVendor rotates like the real one is presumed to: each refresh token exchanges once.
type stubVendor struct {
mu sync.Mutex
live map[string]bool
exchanged []string
issued int
account string
now func() time.Time
}
func (v *stubVendor) Refresh(_ context.Context, g FullGrant) Refreshed {
v.mu.Lock()
defer v.mu.Unlock()
v.exchanged = append(v.exchanged, g.RefreshToken)
if !v.live[g.RefreshToken] {
return Refreshed{Status: 400, Reason: `{"error":"invalid_grant"}`}
}
delete(v.live, g.RefreshToken)
v.issued++
next := fmt.Sprintf("rt-%d", v.issued)
v.live[next] = true
g.AccessToken = fmt.Sprintf("at-%d", v.issued)
g.RefreshToken = next
g.ExpiresAt = v.now().Add(8 * time.Hour).UnixMilli()
exp := v.now().Add(30 * 24 * time.Hour).UnixMilli()
g.RefreshTokenExpiresAt = &exp
return Refreshed{OK: true, Grant: g, Account: v.account}
}
func (v *stubVendor) Usage(context.Context, string) (map[string]any, error) {
return map[string]any{"five_hour": map[string]any{"utilization": 12.0}}, nil
}
type miniMesh struct {
m *Manager
store *MemoryStore
vendor *stubVendor
logins map[string]FullGrant // node → what its credentials file holds
state map[string]BindingState
events []string
now time.Time
keys KeyPair
askDown bool
}
func newMesh(t *testing.T) *miniMesh {
t.Helper()
mm := &miniMesh{logins: map[string]FullGrant{}, state: map[string]BindingState{}, now: t0}
now := func() time.Time { return mm.now }
mm.store = NewMemoryStore(now)
mm.vendor = &stubVendor{live: map[string]bool{}, now: now}
crypt, _ := NewCrypt("a key the vault made")
mm.keys, _ = GenerateKeyPair()
mm.m = &Manager{
Store: mm.store, Vendor: mm.vendor, Crypt: crypt, Keys: mm.keys,
AskGrant: func(_ context.Context, node, publicKey string) (GrantAnswer, error) {
if mm.askDown {
return GrantAnswer{}, fmt.Errorf("503 no responders")
}
g, ok := mm.logins[node]
if !ok {
return GrantAnswer{}, nil
}
raw, _ := json.Marshal(g)
box, err := Seal(string(raw), publicKey)
return GrantAnswer{Sealed: &box, Fingerprint: Fingerprint(g.RefreshToken)}, err
},
PutBinding: func(_ context.Context, c string, b BindingState) error { mm.state[c] = b; return nil },
DeleteBinding: func(_ context.Context, c string) error { delete(mm.state, c); return nil },
Emit: func(event string, body map[string]any) error {
raw, _ := json.Marshal(body)
mm.events = append(mm.events, event+" "+string(raw))
return nil
},
Now: now, Log: func(string, ...any) {}, Holder: "test", Settings: Defaults,
}
return mm
}
func (mm *miniMesh) report(node, rt string, at time.Time, account string) Holdings {
h := Holdings{Node: node, Identity: &Identity{AccountUUID: account, EmailAddress: account + "@example.org"},
Kind: "subscription", ChangedAt: at.Format(time.RFC3339Nano)}
if rt != "" {
h.Refresh.Present, h.Refresh.Fingerprint = true, Fingerprint(rt)
}
return h
}
func (mm *miniMesh) login(node, rt string, valid bool, at time.Time) Holdings {
mm.logins[node] = FullGrant{AccessToken: "local-" + node, RefreshToken: rt, ExpiresAt: mm.now.Add(time.Hour).UnixMilli()}
if valid {
mm.vendor.live[rt] = true
}
return mm.report(node, rt, at, "acct-1")
}
const licence1 = "acct-1@example.org"
func TestAManagerWithNoLicenceAdoptsTheNewestLoginThatRefreshesAndNeverExchangesTheRest(t *testing.T) {
mm := newMesh(t)
ctx := context.Background()
older := mm.login("server", "rt-server", true, t0.Add(-time.Hour))
newest := mm.login("laptop", "rt-laptop", true, t0.Add(-time.Minute))
adopted, err := mm.m.Consider(ctx, []Holdings{older, newest})
if err != nil || len(adopted) != 1 || adopted[0] != licence1 {
t.Fatalf("adopted %v, %v", adopted, err)
}
if strings.Join(mm.vendor.exchanged, ",") != "rt-laptop" {
t.Fatalf("exchanged %v: an older login was exchanged although a newer one refreshed", mm.vendor.exchanged)
}
if o, _ := mm.store.Outcome(ctx, Fingerprint("rt-server")); o != Skipped {
t.Fatalf("the older login is %q, not skipped", o)
}
// A first binding follows the login: both nodes reported this account and were bound to nothing.
bs, _ := mm.store.Bindings(ctx)
if len(bs) != 2 || mm.state["laptop"].Licence != licence1 || mm.state["server"].Licence != licence1 {
t.Fatalf("bindings %v, state %v", bs, mm.state)
}
if !strings.HasPrefix(strings.Join(mm.events, "|"), "licence.adopted") {
t.Fatalf("events %v", mm.events)
}
}
func TestALoginThatDoesNotRefreshAdoptsNothingAndIsNotTriedAgain(t *testing.T) {
mm := newMesh(t)
ctx := context.Background()
dead := mm.login("laptop", "rt-dead", false, t0)
if adopted, _ := mm.m.Consider(ctx, []Holdings{dead}); len(adopted) != 0 {
t.Fatalf("adopted %v", adopted)
}
if o, _ := mm.store.Outcome(ctx, Fingerprint("rt-dead")); o != Dead {
t.Fatalf("outcome %q", o)
}
if ls, _ := mm.store.Licences(ctx); len(ls) != 0 {
t.Fatalf("licences %v", ls)
}
if !strings.Contains(strings.Join(mm.events, "|"), "licence.refused") {
t.Fatalf("events %v", mm.events)
}
_, _ = mm.m.Consider(ctx, []Holdings{dead})
if len(mm.vendor.exchanged) != 1 {
t.Fatalf("a dead refresh token was exchanged %d times", len(mm.vendor.exchanged))
}
}
func TestANewerLoginReplacesTheGrantHeldAndAnOlderOneDoesNot(t *testing.T) {
mm := newMesh(t)
ctx := context.Background()
_, _ = mm.m.Consider(ctx, []Holdings{mm.login("laptop", "rt-a", true, t0.Add(-time.Minute))})
before, _ := mm.store.Licence(ctx, licence1)
_, _ = mm.m.Consider(ctx, []Holdings{mm.login("server", "rt-old", true, t0.Add(-24*time.Hour))})
if strings.Join(mm.vendor.exchanged, ",") != "rt-a" {
t.Fatalf("an older login was exchanged: %v", mm.vendor.exchanged)
}
mm.now = t0.Add(10 * time.Minute)
_, _ = mm.m.Consider(ctx, []Holdings{mm.login("desktop", "rt-new", true, t0.Add(10*time.Minute))})
after, _ := mm.store.Licence(ctx, licence1)
if after.RefreshFingerprint == before.RefreshFingerprint || mm.vendor.exchanged[len(mm.vendor.exchanged)-1] != "rt-new" {
t.Fatalf("a newer login did not win: %v", mm.vendor.exchanged)
}
if ls, _ := mm.store.Licences(ctx); len(ls) != 1 {
t.Fatalf("one account became %d licences", len(ls))
}
}
func TestAReportNamingAnotherAccountThanTheVendorAnsweredForIsRefused(t *testing.T) {
mm := newMesh(t)
ctx := context.Background()
mm.vendor.account = "someone-else"
if adopted, _ := mm.m.Consider(ctx, []Holdings{mm.login("laptop", "rt-a", true, t0)}); len(adopted) != 0 {
t.Fatalf("adopted %v", adopted)
}
if o, _ := mm.store.Outcome(ctx, Fingerprint("rt-a")); o != Refused {
t.Fatalf("outcome %q", o)
}
}
func TestTwoRefreshRunsStartedTogetherRotateAGrantOnce(t *testing.T) {
mm := newMesh(t)
ctx := context.Background()
_, _ = mm.m.Consider(ctx, []Holdings{mm.login("laptop", "rt-a", true, t0)})
mm.now = t0.Add(5 * time.Hour)
second := *mm.m
second.Holder = "another run"
var wg sync.WaitGroup
results := make([]map[string]any, 2)
for i, m := range []*Manager{mm.m, &second} {
wg.Add(1)
go func() { defer wg.Done(); results[i], _ = m.Rotate(ctx, licence1, false) }()
}
wg.Wait()
n := 0
for _, r := range results {
if r["refreshed"] == true {
n++
}
}
if n != 1 {
t.Fatalf("rotated %d times: %v", n, results)
}
if r, _ := second.Rotate(ctx, licence1, false); r["reason"] != "not due" {
t.Fatalf("a run just after was %v", r)
}
}
func TestARotationAndASwitchEachGiveANewerGeneration(t *testing.T) {
mm := newMesh(t)
ctx := context.Background()
_, _ = mm.m.Consider(ctx, []Holdings{mm.login("laptop", "rt-a", true, t0)})
g0 := mm.state["laptop"].Generation
_, _ = mm.m.Rotate(ctx, licence1, true)
g1 := mm.state["laptop"].Generation
if g1 <= g0 {
t.Fatalf("a rotation went from %d to %d", g0, g1)
}
file := filepath.Join(t.TempDir(), "key")
_ = os.WriteFile(file, []byte("sk-ant-api-key\n"), 0o600)
if _, err := mm.m.AdoptKey(ctx, "api", file); err != nil {
t.Fatal(err)
}
if _, err := mm.m.Bind(ctx, "laptop", "api", "test"); err != nil {
t.Fatal(err)
}
if mm.state["laptop"].Licence != "api" || mm.state["laptop"].Generation <= g1 {
t.Fatalf("a switch to a licence rotated less often went backwards: %v", mm.state["laptop"])
}
if _, err := mm.m.Release(ctx, "laptop", "test"); err != nil {
t.Fatal(err)
}
if _, ok := mm.state["laptop"]; ok {
t.Fatal("a released consumer still has a binding in the state")
}
}
func TestCurrentHandsAnAccessTokenOnlySealedToTheConsumersKey(t *testing.T) {
mm := newMesh(t)
ctx := context.Background()
_, _ = mm.m.Consider(ctx, []Holdings{mm.login("laptop", "rt-a", true, t0)})
node, _ := GenerateKeyPair()
answer, err := mm.m.Current(ctx, "laptop", node.PublicKey)
if err != nil || answer["kind"] != "subscription" {
t.Fatalf("%v %v", answer, err)
}
box := answer["sealed"].(SealedBox)
plain, err := Open(box, node.PrivateKey)
if err != nil {
t.Fatal(err)
}
var handed map[string]any
_ = json.Unmarshal([]byte(plain), &handed)
if !strings.HasPrefix(handed["accessToken"].(string), "at-") || handed["refreshToken"] != nil {
t.Fatalf("handed %v", handed)
}
other, _ := GenerateKeyPair()
if _, err := Open(box, other.PrivateKey); err == nil {
t.Fatal("the hand-over opened with another key")
}
if a, _ := mm.m.Current(ctx, "nobody", node.PublicKey); a != nil {
t.Fatalf("a consumer bound to nothing was answered %v", a)
}
}
func TestNothingTheManagerPublishesKeepsOrListsCarriesAToken(t *testing.T) {
mm := newMesh(t)
ctx := context.Background()
_, _ = mm.m.Consider(ctx, []Holdings{mm.login("laptop", "rt-secret-1", true, t0)})
_, _ = mm.m.Rotate(ctx, licence1, true)
_, _ = mm.m.Consider(ctx, []Holdings{mm.login("server", "rt-secret-dead", false, t0.Add(time.Hour))})
_ = mm.m.ReadUsage(ctx)
ls, _ := mm.m.Licences(ctx)
bs, _ := mm.store.Bindings(ctx)
everything, _ := json.Marshal([]any{mm.events, mm.state, ls, bs})
for _, token := range []string{"rt-secret", "rt-1", "rt-2", "at-1", "at-2", "local-"} {
if strings.Contains(string(everything), token) {
t.Fatalf("%s was published: %s", token, everything)
}
}
row, _ := mm.store.Licence(ctx, licence1)
if strings.Contains(row.Sealed, "rt-") {
t.Fatal("the grant is stored in the clear")
}
}
func TestANodeThatDoesNotAnswerIsAskedAgainAndOneWithNoLoginIsSettled(t *testing.T) {
mm := newMesh(t)
ctx := context.Background()
r := mm.login("laptop", "rt-a", true, t0)
mm.askDown = true
if adopted, _ := mm.m.Consider(ctx, []Holdings{r}); len(adopted) != 0 {
t.Fatalf("adopted %v from a node that did not answer", adopted)
}
if o, _ := mm.store.Outcome(ctx, Fingerprint("rt-a")); o != "" {
t.Fatalf("a node that was down was settled %q", o)
}
mm.askDown = false
if adopted, _ := mm.m.Consider(ctx, []Holdings{r}); len(adopted) != 1 {
t.Fatalf("adopted %v on the next pass", adopted)
}
gone := mm.report("server", "rt-gone", t0.Add(time.Second), "acct-2")
_, _ = mm.m.Consider(ctx, []Holdings{gone})
if o, _ := mm.store.Outcome(ctx, Fingerprint("rt-gone")); o != Gone {
t.Fatalf("outcome %q", o)
}
}
func TestAReportIsReadAsTheAgentModuleWritesIt(t *testing.T) {
// The agent module's own report shape (claude-code's holdingsOf), parsed here.
raw := `{"node":"laptop","identity":{"accountUuid":"u-1","emailAddress":"a@example.org"},"kind":"subscription",
"refresh":{"present":true,"fingerprint":"sha256:0123456789abcdef","expiresAt":null},
"access":{"fingerprint":"sha256:fedcba9876543210","expiresAt":1},"licence":null,"generation":0,
"changedAt":"2026-10-04T11:00:00.000Z"}`
var h Holdings
if err := json.Unmarshal([]byte(raw), &h); err != nil {
t.Fatal(err)
}
if h.Node != "laptop" || h.Identity.AccountUUID != "u-1" || !h.Refresh.Present || h.Refresh.Fingerprint != "sha256:0123456789abcdef" {
t.Fatalf("%+v", h)
}
if _, err := time.Parse(time.RFC3339Nano, h.ChangedAt); err != nil {
t.Fatalf("the report's time does not parse: %v", err)
}
}
@@ -0,0 +1,281 @@
package main
// The store on the mesh's postgres (novox/hq design 39 §1), the database the mesh provisioned for this
// module. The schema is brought to this version's shape by the preparation step (ADR 0135), each
// statement idempotent.
import (
"context"
"encoding/json"
"errors"
"fmt"
"os"
"strings"
"time"
"github.com/jackc/pgx/v5"
"github.com/jackc/pgx/v5/pgxpool"
)
// Schema is what this version needs.
var Schema = []string{
`create table if not exists licence (
name text primary key,
kind text not null check (kind in ('subscription', 'api-key')),
account_uuid text unique,
email text,
organization_uuid text,
sealed text,
refresh_fingerprint text,
access_expires_at bigint,
refresh_expires_at bigint,
failures integer not null default 0,
notified_at bigint,
adopted_at bigint not null,
rotated_at bigint)`,
`create sequence if not exists binding_generation`,
`create table if not exists binding (
consumer text primary key,
licence text not null references licence(name),
generation bigint not null)`,
`create table if not exists lease (
key text primary key,
holder text not null,
until timestamptz not null)`,
`create table if not exists offered (
fingerprint text primary key,
node text not null,
account_uuid text,
outcome text not null,
why text not null,
at timestamptz not null default now())`,
`create table if not exists usage (
licence text not null,
at bigint not null,
reading jsonb not null,
raw jsonb not null)`,
`create index if not exists usage_by_licence on usage (licence, at desc)`,
`create table if not exists audit (
at timestamptz not null default now(),
what text not null,
detail jsonb not null)`,
}
// PgStore is the store on postgres.
type PgStore struct{ pool *pgxpool.Pool }
// PgStoreFromEnv opens the store the mesh provisioned, its URL in the file DATABASE_URL_FILE names.
func PgStoreFromEnv(ctx context.Context) (*PgStore, error) {
file := os.Getenv("DATABASE_URL_FILE")
if file == "" {
return nil, errors.New("DATABASE_URL_FILE is not set: the manager's database is a requirement the mesh resolves")
}
raw, err := os.ReadFile(file)
if err != nil {
return nil, err
}
return OpenPgStore(ctx, strings.TrimSpace(string(raw)))
}
// OpenPgStore opens a store at a URL.
func OpenPgStore(ctx context.Context, url string) (*PgStore, error) {
pool, err := pgxpool.New(ctx, url)
if err != nil {
return nil, err
}
return &PgStore{pool: pool}, nil
}
// Migrate brings the schema to this version's shape.
func (s *PgStore) Migrate(ctx context.Context) error {
for _, q := range Schema {
if _, err := s.pool.Exec(ctx, q); err != nil {
return fmt.Errorf("%s: %w", strings.Fields(q)[0:6], err)
}
}
return nil
}
const licenceColumns = `name, kind, coalesce(account_uuid,''), coalesce(email,''), coalesce(organization_uuid,''),
coalesce(sealed,''), coalesce(refresh_fingerprint,''), coalesce(access_expires_at,0), coalesce(refresh_expires_at,0),
failures, coalesce(notified_at,0), adopted_at, coalesce(rotated_at,0)`
func scanLicence(row pgx.Row) (*Licence, error) {
var l Licence
err := row.Scan(&l.Name, &l.Kind, &l.AccountUUID, &l.Email, &l.OrganizationUUID, &l.Sealed, &l.RefreshFingerprint,
&l.AccessExpiresAt, &l.RefreshExpiresAt, &l.Failures, &l.NotifiedAt, &l.AdoptedAt, &l.RotatedAt)
if errors.Is(err, pgx.ErrNoRows) {
return nil, nil
}
return &l, err
}
func (s *PgStore) Licences(ctx context.Context) ([]Licence, error) {
rows, err := s.pool.Query(ctx, `select `+licenceColumns+` from licence order by name`)
if err != nil {
return nil, err
}
defer rows.Close()
var out []Licence
for rows.Next() {
l, err := scanLicence(rows)
if err != nil {
return nil, err
}
out = append(out, *l)
}
return out, rows.Err()
}
func (s *PgStore) Licence(ctx context.Context, name string) (*Licence, error) {
return scanLicence(s.pool.QueryRow(ctx, `select `+licenceColumns+` from licence where name = $1`, name))
}
func (s *PgStore) LicenceForAccount(ctx context.Context, account string) (*Licence, error) {
return scanLicence(s.pool.QueryRow(ctx, `select `+licenceColumns+` from licence where account_uuid = $1`, account))
}
func nullable(s string) any {
if s == "" {
return nil
}
return s
}
func nullableInt(v int64) any {
if v == 0 {
return nil
}
return v
}
func (s *PgStore) SaveLicence(ctx context.Context, l Licence) error {
_, err := s.pool.Exec(ctx, `insert into licence (name, kind, account_uuid, email, organization_uuid, sealed, refresh_fingerprint,
access_expires_at, refresh_expires_at, failures, notified_at, adopted_at, rotated_at)
values ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12,$13)
on conflict (name) do update set kind = excluded.kind, account_uuid = excluded.account_uuid, email = excluded.email,
organization_uuid = excluded.organization_uuid, sealed = excluded.sealed, refresh_fingerprint = excluded.refresh_fingerprint,
access_expires_at = excluded.access_expires_at, refresh_expires_at = excluded.refresh_expires_at,
failures = excluded.failures, notified_at = excluded.notified_at, adopted_at = excluded.adopted_at,
rotated_at = excluded.rotated_at`,
l.Name, l.Kind, nullable(l.AccountUUID), nullable(l.Email), nullable(l.OrganizationUUID), nullable(l.Sealed),
nullable(l.RefreshFingerprint), nullableInt(l.AccessExpiresAt), nullableInt(l.RefreshExpiresAt), l.Failures,
nullableInt(l.NotifiedAt), l.AdoptedAt, nullableInt(l.RotatedAt))
return err
}
// Lease is taken in the store before a row is read (design 39 §3): a second run started together finds
// it live and does nothing.
func (s *PgStore) Lease(ctx context.Context, key, holder string, d time.Duration) (bool, error) {
tag, err := s.pool.Exec(ctx, `insert into lease (key, holder, until) values ($1, $2, now() + make_interval(secs => $3))
on conflict (key) do update set holder = excluded.holder, until = excluded.until
where lease.until < now() or lease.holder = excluded.holder`, key, holder, d.Seconds())
if err != nil {
return false, err
}
return tag.RowsAffected() == 1, nil
}
func (s *PgStore) Unlease(ctx context.Context, key, holder string) error {
_, err := s.pool.Exec(ctx, `delete from lease where key = $1 and holder = $2`, key, holder)
return err
}
func (s *PgStore) bindingsWhere(ctx context.Context, q string, args ...any) ([]Binding, error) {
rows, err := s.pool.Query(ctx, q, args...)
if err != nil {
return nil, err
}
defer rows.Close()
var out []Binding
for rows.Next() {
var b Binding
if err := rows.Scan(&b.Consumer, &b.Licence, &b.Generation); err != nil {
return nil, err
}
out = append(out, b)
}
return out, rows.Err()
}
func (s *PgStore) Bindings(ctx context.Context) ([]Binding, error) {
return s.bindingsWhere(ctx, `select consumer, licence, generation from binding order by consumer`)
}
func (s *PgStore) Binding(ctx context.Context, consumer string) (*Binding, error) {
out, err := s.bindingsWhere(ctx, `select consumer, licence, generation from binding where consumer = $1`, consumer)
if err != nil || len(out) == 0 {
return nil, err
}
return &out[0], nil
}
func (s *PgStore) Bind(ctx context.Context, consumer, licence string) (Binding, error) {
out, err := s.bindingsWhere(ctx, `insert into binding (consumer, licence, generation) values ($1, $2, nextval('binding_generation'))
on conflict (consumer) do update set licence = excluded.licence, generation = excluded.generation
returning consumer, licence, generation`, consumer, licence)
if err != nil {
return Binding{}, err
}
return out[0], nil
}
func (s *PgStore) Unbind(ctx context.Context, consumer string) (bool, error) {
tag, err := s.pool.Exec(ctx, `delete from binding where consumer = $1`, consumer)
return err == nil && tag.RowsAffected() == 1, err
}
func (s *PgStore) Advance(ctx context.Context, licence string) ([]Binding, error) {
return s.bindingsWhere(ctx, `update binding set generation = nextval('binding_generation') where licence = $1
returning consumer, licence, generation`, licence)
}
func (s *PgStore) Outcome(ctx context.Context, fp string) (Outcome, error) {
var o string
err := s.pool.QueryRow(ctx, `select outcome from offered where fingerprint = $1`, fp).Scan(&o)
if errors.Is(err, pgx.ErrNoRows) {
return "", nil
}
return Outcome(o), err
}
func (s *PgStore) RecordOutcome(ctx context.Context, fp, node, account string, o Outcome, why string) error {
_, err := s.pool.Exec(ctx, `insert into offered (fingerprint, node, account_uuid, outcome, why) values ($1,$2,$3,$4,$5)
on conflict (fingerprint) do update set outcome = excluded.outcome, why = excluded.why, at = now()`,
fp, node, nullable(account), string(o), why)
return err
}
func (s *PgStore) RecordUsage(ctx context.Context, licence string, at int64, r UsageReading, raw map[string]any) error {
reading, _ := json.Marshal(r)
rawJSON, _ := json.Marshal(raw)
_, err := s.pool.Exec(ctx, `insert into usage (licence, at, reading, raw) values ($1,$2,$3,$4)`, licence, at, reading, rawJSON)
return err
}
func (s *PgStore) Usage(ctx context.Context, licence string, limit int) ([]UsageRow, error) {
rows, err := s.pool.Query(ctx, `select licence, at, reading from usage where ($1 = '' or licence = $1) order by at desc limit $2`, licence, limit)
if err != nil {
return nil, err
}
defer rows.Close()
var out []UsageRow
for rows.Next() {
var u UsageRow
var reading []byte
if err := rows.Scan(&u.Licence, &u.At, &reading); err != nil {
return nil, err
}
_ = json.Unmarshal(reading, &u.Reading)
out = append(out, u)
}
return out, rows.Err()
}
func (s *PgStore) Audit(ctx context.Context, what string, detail map[string]any) error {
raw, _ := json.Marshal(detail)
_, err := s.pool.Exec(ctx, `insert into audit (what, detail) values ($1, $2)`, what, raw)
return err
}
func (s *PgStore) Close() { s.pool.Close() }
@@ -0,0 +1,189 @@
package main
// Sealing to one recipient (novox/hq ADR 0183, ADR 0206): the manager seals what it hands a consumer to
// the key that consumer sent, and a node seals a waiting login to the key the manager gives. The same box
// the agent module's TypeScript makes and opens, byte for byte — X25519 for the agreement, HKDF-SHA256 for
// the key, AES-256-GCM for the box — so `testdata/sealed-by-typescript.json` is opened here, and a test
// reopens what this seals with the same derivation.
//
// A box is `{ v: 1, eph, iv, tag, ct }`, every field base64; `eph` is the one-time public key as SPKI DER,
// and the key is bound to it and to the recipient's raw public key, so a box cannot be re-addressed.
import (
"crypto/aes"
"crypto/cipher"
"crypto/ecdh"
"crypto/hkdf"
"crypto/rand"
"crypto/sha256"
"crypto/x509"
"encoding/base64"
"encoding/pem"
"errors"
"fmt"
)
// SealedBox is a value sealed to one recipient.
type SealedBox struct {
V int `json:"v"`
Eph string `json:"eph"`
IV string `json:"iv"`
Tag string `json:"tag"`
Ct string `json:"ct"`
}
// KeyPair is a recipient's keypair as the two PEM strings it is kept and sent as.
type KeyPair struct {
PublicKey string `json:"publicKey"`
PrivateKey string `json:"privateKey"`
}
const sealInfo = "novox-mesh sealed box v1"
// GenerateKeyPair makes an X25519 keypair, PEM-encoded as the agent module's are.
func GenerateKeyPair() (KeyPair, error) {
priv, err := ecdh.X25519().GenerateKey(rand.Reader)
if err != nil {
return KeyPair{}, err
}
pubDER, err := x509.MarshalPKIXPublicKey(priv.PublicKey())
if err != nil {
return KeyPair{}, err
}
privDER, err := x509.MarshalPKCS8PrivateKey(priv)
if err != nil {
return KeyPair{}, err
}
return KeyPair{
PublicKey: string(pem.EncodeToMemory(&pem.Block{Type: "PUBLIC KEY", Bytes: pubDER})),
PrivateKey: string(pem.EncodeToMemory(&pem.Block{Type: "PRIVATE KEY", Bytes: privDER})),
}, nil
}
func publicFromPEM(p string) (*ecdh.PublicKey, error) {
block, _ := pem.Decode([]byte(p))
if block == nil {
return nil, errors.New("not a PEM public key")
}
k, err := x509.ParsePKIXPublicKey(block.Bytes)
if err != nil {
return nil, err
}
pub, ok := k.(*ecdh.PublicKey)
if !ok || pub.Curve() != ecdh.X25519() {
return nil, errors.New("not an X25519 public key")
}
return pub, nil
}
func privateFromPEM(p string) (*ecdh.PrivateKey, error) {
block, _ := pem.Decode([]byte(p))
if block == nil {
return nil, errors.New("not a PEM private key")
}
k, err := x509.ParsePKCS8PrivateKey(block.Bytes)
if err != nil {
return nil, err
}
priv, ok := k.(*ecdh.PrivateKey)
if !ok || priv.Curve() != ecdh.X25519() {
return nil, errors.New("not an X25519 private key")
}
return priv, nil
}
func boxKey(secret, ephDER, recipientRaw []byte) ([]byte, error) {
salt := append(append([]byte{}, ephDER...), recipientRaw...)
return hkdf.Key(sha256.New, secret, salt, sealInfo, 32)
}
// Seal seals plaintext to the recipient's public key.
func Seal(plaintext, recipientPEM string) (SealedBox, error) {
recipient, err := publicFromPEM(recipientPEM)
if err != nil {
return SealedBox{}, err
}
eph, err := ecdh.X25519().GenerateKey(rand.Reader)
if err != nil {
return SealedBox{}, err
}
secret, err := eph.ECDH(recipient)
if err != nil {
return SealedBox{}, err
}
ephDER, err := x509.MarshalPKIXPublicKey(eph.PublicKey())
if err != nil {
return SealedBox{}, err
}
key, err := boxKey(secret, ephDER, recipient.Bytes())
if err != nil {
return SealedBox{}, err
}
gcm, err := newGCM(key)
if err != nil {
return SealedBox{}, err
}
iv := make([]byte, 12)
if _, err := rand.Read(iv); err != nil {
return SealedBox{}, err
}
out := gcm.Seal(nil, iv, []byte(plaintext), nil)
ct, tag := out[:len(out)-gcm.Overhead()], out[len(out)-gcm.Overhead():]
b64 := base64.StdEncoding.EncodeToString
return SealedBox{V: 1, Eph: b64(ephDER), IV: b64(iv), Tag: b64(tag), Ct: b64(ct)}, nil
}
// Open opens a box with the recipient's private key; it fails for a box to another key or one tampered with.
func Open(box SealedBox, privatePEM string) (string, error) {
if box.V != 1 {
return "", errors.New("not a sealed box this module can open")
}
priv, err := privateFromPEM(privatePEM)
if err != nil {
return "", err
}
d := base64.StdEncoding.DecodeString
ephDER, err := d(box.Eph)
if err != nil {
return "", fmt.Errorf("the box's eph: %w", err)
}
ephKey, err := x509.ParsePKIXPublicKey(ephDER)
if err != nil {
return "", err
}
eph, ok := ephKey.(*ecdh.PublicKey)
if !ok {
return "", errors.New("the box's eph is not an X25519 key")
}
secret, err := priv.ECDH(eph)
if err != nil {
return "", err
}
key, err := boxKey(secret, ephDER, priv.PublicKey().Bytes())
if err != nil {
return "", err
}
iv, err1 := d(box.IV)
tag, err2 := d(box.Tag)
ct, err3 := d(box.Ct)
if err := errors.Join(err1, err2, err3); err != nil {
return "", err
}
gcm, err := newGCM(key)
if err != nil {
return "", err
}
plain, err := gcm.Open(nil, iv, append(ct, tag...), nil)
if err != nil {
return "", errors.New("the box does not open with this key")
}
return string(plain), nil
}
func newGCM(key []byte) (cipher.AEAD, error) {
block, err := aes.NewCipher(key)
if err != nil {
return nil, err
}
return cipher.NewGCM(block)
}
@@ -0,0 +1,43 @@
package main
import (
"encoding/json"
"os"
"testing"
)
// A box the agent module's TypeScript sealed opens here: the two implementations are one format.
func TestABoxSealedInTypeScriptOpensInGo(t *testing.T) {
raw, err := os.ReadFile("testdata/sealed-by-typescript.json")
if err != nil {
t.Fatal(err)
}
var f struct {
PrivateKey string `json:"privateKey"`
Box SealedBox `json:"box"`
Plaintext string `json:"plaintext"`
}
if err := json.Unmarshal(raw, &f); err != nil {
t.Fatal(err)
}
got, err := Open(f.Box, f.PrivateKey)
if err != nil || got != f.Plaintext {
t.Fatalf("opened %q, %v", got, err)
}
}
// What Go seals opens with its own key and no other.
func TestABoxOpensOnlyForItsRecipient(t *testing.T) {
a, _ := GenerateKeyPair()
b, _ := GenerateKeyPair()
box, err := Seal("a token", a.PublicKey)
if err != nil {
t.Fatal(err)
}
if got, err := Open(box, a.PrivateKey); err != nil || got != "a token" {
t.Fatalf("opened %q, %v", got, err)
}
if _, err := Open(box, b.PrivateKey); err == nil {
t.Fatal("a box opened for another key")
}
}
@@ -0,0 +1,263 @@
package main
// The manager's store (novox/hq ADR 0183, design 39 §1): licences with their grants encrypted, bindings
// with a generation, what became of each login it was offered, usage readings, and the audit. One
// interface, two implementations — postgres for the mesh (pgstore.go), memory for the tests — so every
// rule is tested without a database, and the database is asked only to keep rows.
import (
"context"
"sort"
"sync"
"time"
)
// Licence is one licence: an account, or an API key.
type Licence struct {
Name string `json:"name"`
Kind string `json:"kind"` // subscription | api-key
AccountUUID string `json:"accountUuid,omitempty"`
Email string `json:"email,omitempty"`
OrganizationUUID string `json:"organizationUuid,omitempty"`
Sealed string `json:"-"` // the grant or the key, encrypted at rest
RefreshFingerprint string `json:"-"`
AccessExpiresAt int64 `json:"accessExpiresAt,omitempty"`
RefreshExpiresAt int64 `json:"refreshExpiresAt,omitempty"`
Failures int `json:"failures"`
NotifiedAt int64 `json:"-"`
AdoptedAt int64 `json:"adoptedAt"`
RotatedAt int64 `json:"rotatedAt,omitempty"`
}
// Binding is one consumer's binding and the generation it was last given (ADR 0206).
type Binding struct {
Consumer string `json:"consumer"`
Licence string `json:"licence"`
Generation int64 `json:"generation"`
}
// Outcome is what became of a login the manager was offered, by its refresh token's fingerprint.
type Outcome string
const (
Adopted Outcome = "adopted"
Dead Outcome = "dead"
Skipped Outcome = "skipped"
Refused Outcome = "refused"
Gone Outcome = "gone"
)
// UsageRow is one usage reading.
type UsageRow struct {
Licence string `json:"licence"`
At int64 `json:"at"`
Reading UsageReading `json:"reading"`
}
// Store is what the manager keeps.
type Store interface {
Licences(ctx context.Context) ([]Licence, error)
Licence(ctx context.Context, name string) (*Licence, error)
LicenceForAccount(ctx context.Context, account string) (*Licence, error)
SaveLicence(ctx context.Context, l Licence) error
// Lease takes a lease for d, or answers false while another holder's is live.
Lease(ctx context.Context, key, holder string, d time.Duration) (bool, error)
Unlease(ctx context.Context, key, holder string) error
Bindings(ctx context.Context) ([]Binding, error)
Binding(ctx context.Context, consumer string) (*Binding, error)
// Bind binds (or switches) a consumer at the next generation.
Bind(ctx context.Context, consumer, licence string) (Binding, error)
Unbind(ctx context.Context, consumer string) (bool, error)
// Advance gives every consumer of a licence a new generation: what a rotation is to them.
Advance(ctx context.Context, licence string) ([]Binding, error)
Outcome(ctx context.Context, fingerprint string) (Outcome, error)
RecordOutcome(ctx context.Context, fingerprint, node, account string, o Outcome, why string) error
RecordUsage(ctx context.Context, licence string, at int64, r UsageReading, raw map[string]any) error
Usage(ctx context.Context, licence string, limit int) ([]UsageRow, error)
Audit(ctx context.Context, what string, detail map[string]any) error
Close()
}
// MemoryStore is the tests' store.
type MemoryStore struct {
mu sync.Mutex
now func() time.Time
rows map[string]Licence
binds map[string]Binding
leases map[string]struct {
holder string
until time.Time
}
outcomes map[string]Outcome
usage []UsageRow
Audits []map[string]any
generation int64
}
// NewMemoryStore is an empty store whose leases age by now.
func NewMemoryStore(now func() time.Time) *MemoryStore {
return &MemoryStore{now: now, rows: map[string]Licence{}, binds: map[string]Binding{},
leases: map[string]struct {
holder string
until time.Time
}{}, outcomes: map[string]Outcome{}}
}
func (m *MemoryStore) Licences(context.Context) ([]Licence, error) {
m.mu.Lock()
defer m.mu.Unlock()
out := make([]Licence, 0, len(m.rows))
for _, l := range m.rows {
out = append(out, l)
}
sort.Slice(out, func(i, j int) bool { return out[i].Name < out[j].Name })
return out, nil
}
func (m *MemoryStore) Licence(_ context.Context, name string) (*Licence, error) {
m.mu.Lock()
defer m.mu.Unlock()
if l, ok := m.rows[name]; ok {
return &l, nil
}
return nil, nil
}
func (m *MemoryStore) LicenceForAccount(_ context.Context, account string) (*Licence, error) {
m.mu.Lock()
defer m.mu.Unlock()
for _, l := range m.rows {
if l.AccountUUID == account {
return &l, nil
}
}
return nil, nil
}
func (m *MemoryStore) SaveLicence(_ context.Context, l Licence) error {
m.mu.Lock()
defer m.mu.Unlock()
m.rows[l.Name] = l
return nil
}
func (m *MemoryStore) Lease(_ context.Context, key, holder string, d time.Duration) (bool, error) {
m.mu.Lock()
defer m.mu.Unlock()
if held, ok := m.leases[key]; ok && held.until.After(m.now()) && held.holder != holder {
return false, nil
}
m.leases[key] = struct {
holder string
until time.Time
}{holder, m.now().Add(d)}
return true, nil
}
func (m *MemoryStore) Unlease(_ context.Context, key, holder string) error {
m.mu.Lock()
defer m.mu.Unlock()
if m.leases[key].holder == holder {
delete(m.leases, key)
}
return nil
}
func (m *MemoryStore) Bindings(context.Context) ([]Binding, error) {
m.mu.Lock()
defer m.mu.Unlock()
out := make([]Binding, 0, len(m.binds))
for _, b := range m.binds {
out = append(out, b)
}
sort.Slice(out, func(i, j int) bool { return out[i].Consumer < out[j].Consumer })
return out, nil
}
func (m *MemoryStore) Binding(_ context.Context, consumer string) (*Binding, error) {
m.mu.Lock()
defer m.mu.Unlock()
if b, ok := m.binds[consumer]; ok {
return &b, nil
}
return nil, nil
}
func (m *MemoryStore) Bind(_ context.Context, consumer, licence string) (Binding, error) {
m.mu.Lock()
defer m.mu.Unlock()
m.generation++
b := Binding{Consumer: consumer, Licence: licence, Generation: m.generation}
m.binds[consumer] = b
return b, nil
}
func (m *MemoryStore) Unbind(_ context.Context, consumer string) (bool, error) {
m.mu.Lock()
defer m.mu.Unlock()
_, ok := m.binds[consumer]
delete(m.binds, consumer)
return ok, nil
}
func (m *MemoryStore) Advance(_ context.Context, licence string) ([]Binding, error) {
m.mu.Lock()
defer m.mu.Unlock()
var out []Binding
for c, b := range m.binds {
if b.Licence != licence {
continue
}
m.generation++
b.Generation = m.generation
m.binds[c] = b
out = append(out, b)
}
sort.Slice(out, func(i, j int) bool { return out[i].Consumer < out[j].Consumer })
return out, nil
}
func (m *MemoryStore) Outcome(_ context.Context, fp string) (Outcome, error) {
m.mu.Lock()
defer m.mu.Unlock()
return m.outcomes[fp], nil
}
func (m *MemoryStore) RecordOutcome(_ context.Context, fp, _, _ string, o Outcome, _ string) error {
m.mu.Lock()
defer m.mu.Unlock()
m.outcomes[fp] = o
return nil
}
func (m *MemoryStore) RecordUsage(_ context.Context, licence string, at int64, r UsageReading, _ map[string]any) error {
m.mu.Lock()
defer m.mu.Unlock()
m.usage = append(m.usage, UsageRow{Licence: licence, At: at, Reading: r})
return nil
}
func (m *MemoryStore) Usage(_ context.Context, licence string, limit int) ([]UsageRow, error) {
m.mu.Lock()
defer m.mu.Unlock()
var out []UsageRow
for i := len(m.usage) - 1; i >= 0 && len(out) < limit; i-- {
if licence == "" || m.usage[i].Licence == licence {
out = append(out, m.usage[i])
}
}
return out, nil
}
func (m *MemoryStore) Audit(_ context.Context, what string, detail map[string]any) error {
m.mu.Lock()
defer m.mu.Unlock()
d := map[string]any{"what": what}
for k, v := range detail {
d[k] = v
}
m.Audits = append(m.Audits, d)
return nil
}
func (m *MemoryStore) Close() {}
@@ -0,0 +1,125 @@
package main
import (
"context"
"os"
"strings"
"testing"
"time"
)
// Against a real postgres, because the questions are the database's: does the schema apply twice, does a
// lease refuse a second holder, does a generation only grow. Skipped unless one is named:
//
// docker run -d --rm --name licmgr-pg -e POSTGRES_PASSWORD=t -p 15498:5432 postgres:16-alpine
// MESH_TEST_POSTGRES=postgres://postgres:t@127.0.0.1:15498/postgres go test ./...
func TestTheStoreOnPostgres(t *testing.T) {
url := os.Getenv("MESH_TEST_POSTGRES")
if url == "" {
t.Skip("MESH_TEST_POSTGRES unset")
}
ctx := context.Background()
s, err := OpenPgStore(ctx, url)
if err != nil {
t.Fatal(err)
}
defer s.Close()
for _, table := range []string{"binding", "licence", "lease", "offered", "usage", "audit"} {
_, _ = s.pool.Exec(ctx, "drop table if exists "+table+" cascade")
}
_, _ = s.pool.Exec(ctx, "drop sequence if exists binding_generation")
for i := 0; i < 2; i++ {
if err := s.Migrate(ctx); err != nil {
t.Fatalf("migration %d: %v", i+1, err)
}
}
l := Licence{Name: "a@example.org", Kind: "subscription", AccountUUID: "u-1", Email: "a@example.org", Sealed: "v1.x.y",
RefreshFingerprint: "sha256:1", AccessExpiresAt: 1, RefreshExpiresAt: 2, AdoptedAt: 3}
if err := s.SaveLicence(ctx, l); err != nil {
t.Fatal(err)
}
l.Failures = 2
_ = s.SaveLicence(ctx, l)
if got, _ := s.LicenceForAccount(ctx, "u-1"); got == nil || got.Failures != 2 || got.OrganizationUUID != "" {
t.Fatalf("%+v", got)
}
if none, err := s.Licence(ctx, "nobody"); none != nil || err != nil {
t.Fatalf("%v %v", none, err)
}
lease := func(holder string) bool {
ok, err := s.Lease(ctx, "licence:a", holder, time.Minute)
if err != nil {
t.Fatal(err)
}
return ok
}
if !lease("one") || lease("two") || !lease("one") {
t.Fatal("a lease did not refuse a second holder, or its own holder could not renew it")
}
_ = s.Unlease(ctx, "licence:a", "one")
if !lease("two") {
t.Fatal("a released lease was not taken")
}
b1, _ := s.Bind(ctx, "laptop", l.Name)
adv, _ := s.Advance(ctx, l.Name)
b3, _ := s.Bind(ctx, "laptop", l.Name)
if len(adv) != 1 || !(b1.Generation < adv[0].Generation && adv[0].Generation < b3.Generation) {
t.Fatalf("generations %d %v %d", b1.Generation, adv, b3.Generation)
}
_ = s.RecordOutcome(ctx, "sha256:x", "laptop", "u-1", Dead, "400")
if o, _ := s.Outcome(ctx, "sha256:x"); o != Dead {
t.Fatalf("outcome %q", o)
}
if o, _ := s.Outcome(ctx, "sha256:none"); o != "" {
t.Fatalf("an unknown login is %q", o)
}
pct := 1.0
_ = s.RecordUsage(ctx, l.Name, 5, UsageReading{SessionPct: &pct}, map[string]any{})
if u, _ := s.Usage(ctx, "", 5); len(u) != 1 || *u[0].Reading.SessionPct != 1 {
t.Fatalf("usage %v", u)
}
if err := s.Audit(ctx, "bound", map[string]any{"consumer": "laptop"}); err != nil {
t.Fatal(err)
}
if ok, _ := s.Unbind(ctx, "laptop"); !ok {
t.Fatal("unbind")
}
all, _ := s.Licences(ctx)
if len(all) != 1 || !strings.HasPrefix(all[0].Sealed, "v1.") {
t.Fatalf("%v", all)
}
}
func TestARefreshThatDoesNotRotateKeepsTheRefreshToken(t *testing.T) {
prev := FullGrant{AccessToken: "a", RefreshToken: "r", ExpiresAt: 1}
in := int64(60)
kept := NextGrant(prev, tokenResponse{AccessToken: "a2", ExpiresIn: &in}, 1000)
if !kept.OK || kept.Grant.RefreshToken != "r" || kept.Grant.ExpiresAt != 61_000 {
t.Fatalf("%+v", kept)
}
rotated := NextGrant(prev, tokenResponse{AccessToken: "a3", RefreshToken: "r2"}, 0)
if rotated.Grant.RefreshToken != "r2" {
t.Fatalf("%+v", rotated)
}
if NextGrant(prev, tokenResponse{}, 0).OK {
t.Fatal("an answer with no access token was a grant")
}
}
func TestAGrantAtRestOpensOnlyWithItsKey(t *testing.T) {
a, _ := NewCrypt("one key")
b, _ := NewCrypt("another key")
sealed := a.Seal(`{"refreshToken":"rt"}`)
if strings.Contains(sealed, "rt") {
t.Fatal("stored in the clear")
}
if got, err := a.Open(sealed); err != nil || got != `{"refreshToken":"rt"}` {
t.Fatalf("%q %v", got, err)
}
if _, err := b.Open(sealed); err == nil {
t.Fatal("opened with another key")
}
if _, err := NewCrypt(" "); err == nil {
t.Fatal("an empty key was accepted")
}
}
@@ -0,0 +1,12 @@
{
"privateKey": "-----BEGIN PRIVATE KEY-----\nMC4CAQAwBQYDK2VuBCIEIAi2NK/bN+p7cqYUwv/kz72TgLdmJUfOHCDZTrMpQzpS\n-----END PRIVATE KEY-----\n",
"publicKey": "-----BEGIN PUBLIC KEY-----\nMCowBQYDK2VuAyEARYvD/w+9ah0KWS9T9pd6Ea6CBymUE48vEVk983QPKyY=\n-----END PUBLIC KEY-----\n",
"box": {
"v": 1,
"eph": "MCowBQYDK2VuAyEAIYlSGrJvF8qSjR1aTFWbEQM/NFbZXrarglN0aRLZZ0E=",
"iv": "ABqT/hMukn6+xpjh",
"tag": "POzmsWTPHSn9xLMMJJ9Akg==",
"ct": "m2u0kuQ0PwrsGelM99Z5aBw6FCd6lFISUbwiK5TzzDw4ZrGtsHEvxytoIeFC"
},
"plaintext": "a grant sealed by the TypeScript agent module"
}
@@ -0,0 +1,187 @@
package main
// The only file that talks to Anthropic (novox/hq ADR 0183): the token endpoint, which this module alone
// calls — one rotation source — and the usage endpoint. Ported from the predecessor's manager, whose
// client id and error handling were each earned by an incident.
import (
"context"
"encoding/json"
"fmt"
"io"
"net/http"
"net/url"
"os"
"strings"
"time"
)
// The public Claude Code client's id: not a secret, and a hard-won constant — a metadata URL in its place
// answers 400, which the predecessor once misdiagnosed as a dead grant.
const clientID = "9d1c250a-e61b-44d9-88ed-5944d1962f5e"
func tokenEndpoint() string {
if v := os.Getenv("MESH_ANTHROPIC_TOKEN_ENDPOINT"); v != "" {
return v
}
return "https://platform.claude.com/v1/oauth/token"
}
func usageEndpoint() string {
if v := os.Getenv("MESH_ANTHROPIC_USAGE_ENDPOINT"); v != "" {
return v
}
return "https://api.anthropic.com/api/oauth/usage"
}
// FullGrant is a subscription's grant as this module keeps it: what the agent's credentials file calls
// `claudeAiOauth`.
type FullGrant struct {
AccessToken string `json:"accessToken"`
RefreshToken string `json:"refreshToken"`
ExpiresAt int64 `json:"expiresAt"`
RefreshTokenExpiresAt *int64 `json:"refreshTokenExpiresAt,omitempty"`
Scopes []string `json:"scopes,omitempty"`
SubscriptionType string `json:"subscriptionType,omitempty"`
RateLimitTier string `json:"rateLimitTier,omitempty"`
}
// Refreshed is what a refresh came to: the next grant and the account the vendor answered for, or why not.
type Refreshed struct {
OK bool
Grant FullGrant
Account string // empty when the vendor named none
Status int
Reason string
}
// Vendor is the vendor as the manager reaches it; a test stubs it.
type Vendor interface {
Refresh(ctx context.Context, g FullGrant) Refreshed
Usage(ctx context.Context, accessToken string) (map[string]any, error)
}
type tokenResponse struct {
AccessToken string `json:"access_token"`
RefreshToken string `json:"refresh_token"`
ExpiresIn *int64 `json:"expires_in"`
RefreshTokenExpiresIn *int64 `json:"refresh_token_expires_in"`
Scope string `json:"scope"`
Scopes []string `json:"scopes"`
SubscriptionType string `json:"subscription_type"`
Account *struct {
UUID string `json:"uuid"`
} `json:"account"`
}
// NextGrant is the grant a refresh answered, laid over the one refreshed: a refresh token the vendor did
// not rotate is kept, so a rotating vendor and one that does not are both handled.
func NextGrant(prev FullGrant, r tokenResponse, nowMs int64) Refreshed {
if r.AccessToken == "" {
return Refreshed{Status: 200, Reason: "the vendor answered without an access token"}
}
g := prev
g.AccessToken = r.AccessToken
if r.RefreshToken != "" {
g.RefreshToken = r.RefreshToken
}
if r.ExpiresIn != nil {
g.ExpiresAt = nowMs + *r.ExpiresIn*1000
}
if r.RefreshTokenExpiresIn != nil {
v := nowMs + *r.RefreshTokenExpiresIn*1000
g.RefreshTokenExpiresAt = &v
}
if len(r.Scopes) > 0 {
g.Scopes = r.Scopes
} else if r.Scope != "" {
g.Scopes = strings.Fields(r.Scope)
}
if r.SubscriptionType != "" {
g.SubscriptionType = r.SubscriptionType
}
out := Refreshed{OK: true, Grant: g}
if r.Account != nil {
out.Account = r.Account.UUID
}
return out
}
type liveVendor struct{ client *http.Client }
// LiveVendor is the vendor over the network.
func LiveVendor() Vendor { return liveVendor{client: &http.Client{Timeout: 30 * time.Second}} }
func (v liveVendor) Refresh(ctx context.Context, g FullGrant) Refreshed {
form := url.Values{"grant_type": {"refresh_token"}, "refresh_token": {g.RefreshToken}, "client_id": {clientID}}
req, err := http.NewRequestWithContext(ctx, http.MethodPost, tokenEndpoint(), strings.NewReader(form.Encode()))
if err != nil {
return Refreshed{Reason: err.Error()}
}
req.Header.Set("content-type", "application/x-www-form-urlencoded")
resp, err := v.client.Do(req)
if err != nil {
return Refreshed{Reason: "the token endpoint did not answer: " + err.Error()}
}
defer resp.Body.Close()
body, _ := io.ReadAll(io.LimitReader(resp.Body, 1<<20))
if resp.StatusCode/100 != 2 {
// The body, never only the status: a malformed request and a revoked grant both answer 400.
reason := string(body)
if len(reason) > 400 {
reason = reason[:400]
}
return Refreshed{Status: resp.StatusCode, Reason: reason}
}
var r tokenResponse
if err := json.Unmarshal(body, &r); err != nil {
return Refreshed{Status: resp.StatusCode, Reason: "the vendor's answer is not JSON"}
}
return NextGrant(g, r, time.Now().UnixMilli())
}
func (v liveVendor) Usage(ctx context.Context, accessToken string) (map[string]any, error) {
req, err := http.NewRequestWithContext(ctx, http.MethodGet, usageEndpoint(), nil)
if err != nil {
return nil, err
}
req.Header.Set("authorization", "Bearer "+accessToken)
resp, err := v.client.Do(req)
if err != nil {
return nil, err
}
defer resp.Body.Close()
if resp.StatusCode/100 != 2 {
return nil, fmt.Errorf("the usage endpoint answered %d", resp.StatusCode)
}
var out map[string]any
return out, json.NewDecoder(resp.Body).Decode(&out)
}
// UsageReading is the licence-grain reading (ADR 0054), flattened from the vendor's windows.
type UsageReading struct {
SessionPct *float64 `json:"sessionPct"`
SessionResetsAt string `json:"sessionResetsAt,omitempty"`
WeeklyPct *float64 `json:"weeklyPct"`
SonnetPct *float64 `json:"sonnetPct"`
}
// FlattenUsage reads the windows the predecessor read.
func FlattenUsage(u map[string]any) UsageReading {
window := func(k string) (*float64, string) {
w, ok := u[k].(map[string]any)
if !ok {
return nil, ""
}
pct, ok := w["utilization"].(float64)
resets, _ := w["resets_at"].(string)
if !ok {
return nil, resets
}
return &pct, resets
}
s, resets := window("five_hour")
w, _ := window("seven_day")
so, _ := window("seven_day_sonnet")
return UsageReading{SessionPct: s, SessionResetsAt: resets, WeeklyPct: w, SonnetPct: so}
}
+16
View File
@@ -0,0 +1,16 @@
module claude-licence-manager
go 1.25.0
require (
git.novox.be/novox/mesh-sdk/go v0.1.7
github.com/jackc/pgx/v5 v5.11.0
)
require (
github.com/jackc/pgpassfile v1.0.0 // indirect
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect
github.com/jackc/puddle/v2 v2.2.2 // indirect
golang.org/x/sync v0.17.0 // indirect
golang.org/x/text v0.29.0 // indirect
)
+28
View File
@@ -0,0 +1,28 @@
git.novox.be/novox/mesh-sdk/go v0.1.7 h1:C0sTQmtTiyYH7bnqZb7PusXnqA37gKuT7Nqjn9gG47w=
git.novox.be/novox/mesh-sdk/go v0.1.7/go.mod h1:GFuZUElBZ9A++mxgIKo97aXXo+kV0uJ/UkbhQPPIbrY=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM=
github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg=
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo=
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM=
github.com/jackc/pgx/v5 v5.11.0 h1:IzBBtyK9AHqf98cctWFifYSci2hgQR/cd56wB4p+ogg=
github.com/jackc/pgx/v5 v5.11.0/go.mod h1:mal1tBGAFfLHvZzaYh77YS/eC6IX9OWbRV1QIIM0Jn4=
github.com/jackc/puddle/v2 v2.2.2 h1:PR8nw+E/1w0GLuRFSmiioY6UooMp6KJv0/61nB7icHo=
github.com/jackc/puddle/v2 v2.2.2/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
golang.org/x/sync v0.17.0 h1:l60nONMj9l5drqw6jlhIELNv9I0A4OFgRsG9k2oT9Ug=
golang.org/x/sync v0.17.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
golang.org/x/text v0.29.0 h1:1neNs90w9YzJ9BocxfsQNHKuAT4pkghyXc4nhZ6sJvk=
golang.org/x/text v0.29.0/go.mod h1:7MhJOA9CD2qZyOKYazxdYMF85OwPdEr9jTtBpO7ydH4=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
+130
View File
@@ -0,0 +1,130 @@
{
"module": "claude-licence-manager",
"version": "1",
"slug": "licmgr",
"requires": [
"postgres-database",
"secret"
],
"contributes": {
"postgres-database": {
"name": "claude_licences"
}
},
"binds": {
"postgres-database": "${dir:state}/database.json"
},
"secrets": {
"postgres-database": "${dir:state}/database.secret",
"secret": {
"grant-key": "${dir:state}/grant.key"
}
},
"seats": [
{
"name": "anthropic-licence-manager",
"scope": "mesh",
"serves": [
"licences",
"bindings",
"bind",
"switch",
"release",
"refresh",
"usage",
"adopt",
"current"
]
}
],
"claims": [
{
"name": "anthropic-licence-manager",
"scope": "mesh",
"serves": [
"licences",
"bindings",
"bind",
"switch",
"release",
"refresh",
"usage",
"adopt",
"current"
]
}
],
"emits": [
"licence.adopted",
"licence.refused",
"licence.failing",
"usage.read"
],
"state": [
"bindings"
],
"reads": [
"claude-code.holdings"
],
"resources": [
{
"id": "state",
"type": "directory",
"mode": "0700",
"place": "."
},
{
"id": "database-url",
"type": "file",
"path": "${dir:state}/database.url",
"mode": "0600",
"content": "postgresql://${bound:postgres-database:as}:${secret:postgres-database}@${bound:postgres-database:at}:${bound:postgres-database:port}/${bound:postgres-database:as}\n"
},
{
"id": "settings",
"type": "file",
"path": "${dir:state}/settings.json",
"mode": "0600",
"merge": "json",
"content": "{\n \"cadence_minutes\": 240,\n \"floor_minutes\": 60,\n \"failures_to_notify\": 3,\n \"cooldown_hours\": 24,\n \"refresh_warn_days\": 3\n}\n"
},
{
"id": "prepare",
"type": "process",
"name": "claude-licence-manager-prepare",
"artifact": "code",
"run": [
"./claude-licence-manager",
"prepare"
],
"run-once": true,
"env": {
"DATABASE_URL_FILE": "${dir:state}/database.url"
},
"restart-on": [
"database-url"
]
}
],
"build": {
"artifacts": [
{
"name": "code",
"kind": "bundle",
"language": "go",
"system": "arch",
"from": "cmd/claude-licence-manager",
"binary": "claude-licence-manager",
"loads": [
"claude-licence-manager"
],
"env": {
"DATABASE_URL_FILE": "${dir:state}/database.url",
"MESH_LICENCE_STATE": "${dir:state}",
"MESH_LICENCE_KEY_FILE": "${dir:state}/grant.key",
"MESH_LICENCE_SETTINGS": "${dir:state}/settings.json"
}
}
]
}
}
+1 -1
View File
@@ -58,7 +58,7 @@
"type": "file",
"path": "${dir:state}/server.env",
"mode": "0600",
"content": "NODE_ENV=production\nPORT=9000\nMONGO_URL=mongodb://${bound:mongodb-database:as}:${secret:mongodb-database}@${bound:mongodb-database:at}:${bound:mongodb-database:port}/${bound:mongodb-database:as}?authSource=admin\nMONGO_DB=${bound:mongodb-database:as}\nMINIO_ENDPOINT=${bound:s3-bucket:at}\nMINIO_PORT=${bound:s3-bucket:port}\nMINIO_BUCKET=${bound:s3-bucket:bucket}\nMINIO_ACCESSKEY=${bound:s3-bucket:as}\nMINIO_SECRET=${secret:s3-bucket}\nMINIO_USE_SSL=false\n"
"content": "NODE_ENV=production\nPORT=9000\nMONGO_URL=mongodb://${bound:mongodb-database:as}:${secret:mongodb-database}@${bound:mongodb-database:at}:${bound:mongodb-database:port}/${bound:mongodb-database:as}?authSource=${bound:mongodb-database:as}\nMONGO_DB=${bound:mongodb-database:as}\nMINIO_ENDPOINT=${bound:s3-bucket:at}\nMINIO_PORT=${bound:s3-bucket:port}\nMINIO_BUCKET=${bound:s3-bucket:bucket}\nMINIO_ACCESSKEY=${bound:s3-bucket:as}\nMINIO_SECRET=${secret:s3-bucket}\nMINIO_USE_SSL=false\n"
},
{
"id": "net",