C-3: a node takes its licence token from the vault (issue 1409)
The agent module asks the seat's current which licence it holds and its key, then the vault's get for that licence's access secret sealed to its own key: at its start, on a binding.changed with a newer generation than it applied, and on the vault's secret-changed for the licence it holds. The applied binding keeps the licence's key, so the vault's event is matched by the carried key, never one formed here. Builds: design 39 bus-and-switch §19, §18, B7, B9, B11 Claude-Session: https://claude.ai/code/session_01NcbgfHb9d4T9nq5Q6WqdiJ
This commit is contained in:
@@ -457,13 +457,9 @@ func TestTheGuardIsAskedAboutEveryToolAndRefusesEachForAnotherAccount(t *testing
|
||||
func TestAMoveWritesTheAccountFileBesideTheToken(t *testing.T) {
|
||||
p, w := node(t, "anchor")
|
||||
writeFile(t, p.binding(), `{"licence":"personal","kind":"subscription","generation":4}`)
|
||||
ask := func(address string, args any) (json.RawMessage, error) {
|
||||
key := args.(map[string]any)["public_key"].(string)
|
||||
g, _ := json.Marshal(Grant{AccessToken: "at", ExpiresAt: now + 3_600_000})
|
||||
box, _ := Seal(string(g), key)
|
||||
return json.Marshal(Current{Licence: "personal", Kind: "subscription", Generation: 4, Sealed: &box,
|
||||
Identity: &Identity{AccountUUID: "u-1", EmailAddress: "operator@example.org"}})
|
||||
}
|
||||
var asked []string
|
||||
ask := vaulted(t, Current{Licence: "personal", Key: "personal", Kind: "subscription", Generation: 4,
|
||||
Identity: &Identity{AccountUUID: "u-1", EmailAddress: "operator@example.org"}}, "at", &asked)
|
||||
if _, err := OnBinding(p, &BindingState{Licence: "personal", Kind: "subscription", Generation: 4}, ask, writer(w)); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
@@ -283,31 +283,52 @@ func TestTheGrantAnswersOnlyAWaitingLoginSealedToTheManagersKey(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// seat answers `current` as the manager does: the grant sealed to the key the node sent.
|
||||
// seat answers `current` as the manager does, the licence and its key and never a token, and `get` as the vault does:
|
||||
// the licence's access secret sealed to the key the node sent (design 39 bus-and-switch §18, §19).
|
||||
func seat(t *testing.T, licence, token string, gen int64, asked *[]string) Ask {
|
||||
return vaulted(t, Current{Licence: licence, Key: licence, Kind: "subscription", Generation: gen}, token, asked)
|
||||
}
|
||||
|
||||
// vaulted answers `current` with c and the vault's `get` of c's access secret with token.
|
||||
func vaulted(t *testing.T, c Current, token string, 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)
|
||||
a := args.(map[string]any)
|
||||
switch address {
|
||||
case SeatVerb("current"):
|
||||
if _, sent := a["public_key"]; sent || a["consumer"] == nil {
|
||||
t.Fatalf("current asked with %v", a)
|
||||
}
|
||||
return json.Marshal(c)
|
||||
case VaultGet:
|
||||
if a["name"] != AccessSecret(c.Key) {
|
||||
t.Fatalf("the vault asked for %v", a["name"])
|
||||
}
|
||||
// Each answer a little later, as each rotation hands a later token.
|
||||
g, _ := json.Marshal(Grant{AccessToken: token, ExpiresAt: now + 3_600_000 + int64(len(*asked))*1000})
|
||||
box, err := Seal(string(g), a["public_key"].(string))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
return json.Marshal(map[string]any{"name": a["name"], "generation": 1, "sealed": box})
|
||||
}
|
||||
return json.Marshal(Current{Licence: licence, Kind: "subscription", Generation: gen, Sealed: &box})
|
||||
t.Fatalf("asked %s", address)
|
||||
return nil, nil
|
||||
}
|
||||
}
|
||||
|
||||
func TestANewerGenerationFetchesTheTokenOnceByTheSeatsVerb(t *testing.T) {
|
||||
// design 39 bus-and-switch §19: a newer generation asks the seat which licence, and the vault for its token.
|
||||
func TestANewerGenerationFetchesTheTokenOnceFromTheVault(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" {
|
||||
if len(asked) != 2 || asked[0] != SeatVerb("current") || asked[1] != VaultGet || 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 {
|
||||
if done, _ := OnBinding(p, &BindingState{Licence: "personal", Kind: "subscription", Generation: 3}, ask, writer(w)); done != "" || len(asked) != 2 {
|
||||
t.Fatal("an equal generation asked again")
|
||||
}
|
||||
if HoldingsOf(p).Generation != 3 || w["managed-mcp.json"] == "" {
|
||||
@@ -498,31 +519,31 @@ func TestAnAPIKeyIsHandedOverSealedAndItsFileRemoved(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// A manager whose store was rebuilt counts generations from one again (novox/hq issue 243): a binding with a
|
||||
// lower generation than the one applied is still a binding to apply, and only the one applied is skipped.
|
||||
func TestALowerGenerationAfterTheManagerWasRebuiltIsStillApplied(t *testing.T) {
|
||||
// design 39 bus-and-switch §19, B11: only a newer generation than the one applied is fetched; a lower one is not, and
|
||||
// the vault's secret-changed for the licence held asks its token again, for another secret nothing.
|
||||
func TestTheTokenIsAskedOnANewerGenerationAndOnTheVaultsChange(t *testing.T) {
|
||||
p, w := node(t, "laptop")
|
||||
var asked []string
|
||||
if _, err := OnBinding(p, &BindingState{Licence: "personal", Kind: "subscription", Generation: 16},
|
||||
seat(t, "personal", "at-old", 16, &asked), writer(w)); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
// The rotation that came with it: a later token, as a refresh hands one over.
|
||||
later := func(address string, args any) (json.RawMessage, error) {
|
||||
asked = append(asked, address)
|
||||
g, _ := json.Marshal(Grant{AccessToken: "at-new", ExpiresAt: now + 7_200_000})
|
||||
box, err := Seal(string(g), args.(map[string]any)["public_key"].(string))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
return json.Marshal(Current{Licence: "personal", Kind: "subscription", Generation: 3, Sealed: &box})
|
||||
if done, err := OnBinding(p, &BindingState{Licence: "personal", Kind: "subscription", Generation: 3},
|
||||
seat(t, "personal", "at-low", 3, &asked), writer(w)); err != nil || done != "" || len(asked) != 2 {
|
||||
t.Fatalf("a lower generation was fetched: %q %v, asked %v", done, err, asked)
|
||||
}
|
||||
if _, err := OnBinding(p, &BindingState{Licence: "personal", Kind: "subscription", Generation: 3},
|
||||
later, writer(w)); err != nil {
|
||||
rotated := seat(t, "personal", "at-new", 16, &asked)
|
||||
for _, other := range []string{`{"name":"licence.personal.grant","generation":5}`, `{"name":"licence.other.access","generation":5}`} {
|
||||
if done, err := OnSecretChanged(p, []byte(other), rotated, writer(w)); err != nil || done != "" || len(asked) != 2 {
|
||||
t.Fatalf("%s asked again: %q %v, asked %v", other, done, err, asked)
|
||||
}
|
||||
}
|
||||
if _, err := OnSecretChanged(p, []byte(`{"name":"licence.personal.access","generation":5,"by":"x","at":"2026-11-11T00:00:00Z"}`),
|
||||
rotated, writer(w)); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if len(asked) != 2 || creds(t, p)["accessToken"] != "at-new" || HoldingsOf(p).Generation != 3 {
|
||||
t.Fatalf("asked %v, credentials %v: a lower generation was ignored", asked, creds(t, p))
|
||||
if len(asked) != 4 || asked[3] != VaultGet || creds(t, p)["accessToken"] != "at-new" || HoldingsOf(p).Generation != 16 {
|
||||
t.Fatalf("asked %v, credentials %v: the vault's change was not applied", asked, creds(t, p))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -540,7 +561,7 @@ func TestAnEventForAnotherConsumerIsIgnored(t *testing.T) {
|
||||
t.Fatalf("an event for laptop was acted on by server: %q %v, asked %v", done, err, asked)
|
||||
}
|
||||
p, w = node(t, "laptop")
|
||||
if done, err := OnBindingChanged(p, said, ask, writer(w)); err != nil || !strings.Contains(done, `"generation":3`) || len(asked) != 1 {
|
||||
if done, err := OnBindingChanged(p, said, ask, writer(w)); err != nil || !strings.Contains(done, `"generation":3`) || len(asked) != 2 {
|
||||
t.Fatalf("laptop's own event: %q %v, asked %v", done, err, asked)
|
||||
}
|
||||
if done, _ := OnBindingChanged(p, []byte(`{"consumer":"laptop","licence":"","generation":3,"from":"api"}`), ask, writer(w)); !strings.Contains(done, "released") {
|
||||
@@ -553,7 +574,7 @@ func TestAnEventForAnotherConsumerIsIgnored(t *testing.T) {
|
||||
func TestTheFirstViewOfTheBindingIsApplied(t *testing.T) {
|
||||
p, w := node(t, "laptop")
|
||||
var asked []string
|
||||
current := seat(t, "api", "at-9", 9, &asked)
|
||||
current := vaulted(t, Current{Licence: "api", Key: "api", Kind: "subscription", Generation: 9}, "at-9", &asked)
|
||||
ask := func(address string, args any) (json.RawMessage, error) {
|
||||
if address == SeatVerb("bindings") {
|
||||
asked = append(asked, address)
|
||||
@@ -567,16 +588,21 @@ func TestTheFirstViewOfTheBindingIsApplied(t *testing.T) {
|
||||
if done, err := FirstBinding(p, ask, writer(w)); err != nil || !strings.Contains(done, `"generation":9`) {
|
||||
t.Fatalf("the first view: %q %v, asked %v", done, err, asked)
|
||||
}
|
||||
if len(asked) != 2 || asked[0] != SeatVerb("bindings") || creds(t, p)["accessToken"] != "at-9" {
|
||||
if len(asked) != 3 || asked[0] != SeatVerb("bindings") || creds(t, p)["accessToken"] != "at-9" {
|
||||
t.Fatalf("asked %v, credentials %v", asked, creds(t, p))
|
||||
}
|
||||
// §19: at every start the vault is asked, though the binding is the one applied.
|
||||
if _, err := FirstBinding(p, ask, writer(w)); err != nil || len(asked) != 6 || asked[5] != VaultGet {
|
||||
t.Fatalf("a start asked %v", asked)
|
||||
}
|
||||
raw, _ := os.ReadFile("../../module.json")
|
||||
var m struct {
|
||||
Consumes, Invokes []string
|
||||
Reads []string
|
||||
}
|
||||
_ = json.Unmarshal(raw, &m)
|
||||
if !slices.Contains(m.Consumes, BindingChanged) || !slices.Contains(m.Invokes, SeatVerb("bindings")) || len(m.Reads) != 0 {
|
||||
if !slices.Contains(m.Consumes, BindingChanged) || !slices.Contains(m.Consumes, SecretChanged) ||
|
||||
!slices.Contains(m.Invokes, SeatVerb("bindings")) || !slices.Contains(m.Invokes, VaultGet) || len(m.Reads) != 0 {
|
||||
t.Fatalf("the manifest consumes %v, invokes %v, reads %v", m.Consumes, m.Invokes, m.Reads)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -277,10 +277,10 @@ func TestTheManifestListsEveryToolServed(t *testing.T) {
|
||||
_ = json.Unmarshal(raw, &m)
|
||||
// The hand-act seat's warranted and hand-act, the seat mesh-catalog's seat list (ADR 0635 §1), and the owners' read tools of the three followed states; no verb of
|
||||
// the controller, and no read of the indexer's or the assigner's state (ADR 0615 §1): their views are consumed. The
|
||||
// licence binding is the seat's bindings and the manager's binding.changed (design 39 bus-and-switch §22, B9).
|
||||
if !slices.Equal(m.Invokes, []string{"seat:mesh-hand-acts.warranted", "seat:mesh-hand-acts.hand-act", "seat:mesh-catalog.seat-definitions", ModulesTool, ManifestsTool, AssignmentsTool, SeatVerb("bindings")}) ||
|
||||
// licence binding is the seat's bindings and the manager's binding.changed, its token the vault's get and secret-changed (design 39 bus-and-switch §19, §22, B9).
|
||||
if !slices.Equal(m.Invokes, []string{"seat:mesh-hand-acts.warranted", "seat:mesh-hand-acts.hand-act", "seat:mesh-catalog.seat-definitions", ModulesTool, ManifestsTool, AssignmentsTool, SeatVerb("bindings"), SeatVerb("current"), VaultGet}) ||
|
||||
len(m.Reads) != 0 ||
|
||||
!slices.Equal(m.Consumes, []string{"mesh-delivery-indexer.views.modules", "mesh-delivery-indexer.views.manifests", "mesh-delivery-assigner.views.assignments", "mesh-catalog.seat-definition-changed", "mesh-catalog.seat-definition-removed", HoldingsAsked, BindingChanged}) {
|
||||
!slices.Equal(m.Consumes, []string{"mesh-delivery-indexer.views.modules", "mesh-delivery-indexer.views.manifests", "mesh-delivery-assigner.views.assignments", "mesh-catalog.seat-definition-changed", "mesh-catalog.seat-definition-removed", HoldingsAsked, BindingChanged, SecretChanged}) {
|
||||
t.Errorf("invokes = %v, reads = %v, consumes = %v", m.Invokes, m.Reads, m.Consumes)
|
||||
}
|
||||
if src, _ := os.ReadFile("proposals.go"); !strings.Contains(string(src), `"seat:mesh-hand-acts.warranted"`) ||
|
||||
|
||||
@@ -346,7 +346,7 @@ func tools(p Paths, servers ServerState, view *ServerView, config ConfigState, c
|
||||
return GuardStatus(p, last), nil
|
||||
}},
|
||||
{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.",
|
||||
Description: "Ask the licence manager which licence this node holds and the vault for its access token now, and apply it, rather than waiting for its binding or the token 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.",
|
||||
@@ -613,9 +613,9 @@ func run(p Paths, view *ServerView, configView *ConfigView) {
|
||||
})
|
||||
}, func(n int) { say("hearing %s%s", HoldingsAsked, refusals(n)) })
|
||||
|
||||
// What this node should hold (design 39 bus-and-switch §22): the manager's binding.changed naming this node's
|
||||
// What this node should hold (design 39 bus-and-switch §19, §22): the manager's binding.changed naming this node's
|
||||
// consumer, and its first view, the seat's bindings for this consumer, asked once each time the events are heard
|
||||
// anew (the start, or a reconnect); a newer generation is fetched.
|
||||
// anew (the start, or a reconnect); the token is asked from the vault at that first view and on a newer generation.
|
||||
applied := func(done string, err error) {
|
||||
if err != nil {
|
||||
say("fetching this node's token failed: %v", err)
|
||||
@@ -640,6 +640,13 @@ func run(p Paths, view *ServerView, configView *ConfigView) {
|
||||
}
|
||||
return err
|
||||
}, func(n int) { say("hearing this node's licence binding%s", refusals(n)) })
|
||||
// The token of the licence this node holds, asked again when the vault says its access secret changed (§19).
|
||||
go persist("hearing the vault's changes of this node's licence token", func() error {
|
||||
return stdio.Subscribe(SecretChanged, func(e stdio.Envelope) error {
|
||||
applied(OnSecretChanged(p, e.Body, ask, writeManaged))
|
||||
return nil
|
||||
})
|
||||
}, func(n int) { say("hearing %s%s", SecretChanged, refusals(n)) })
|
||||
|
||||
report()
|
||||
var last string
|
||||
|
||||
@@ -10,8 +10,10 @@ package main
|
||||
// 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.
|
||||
// - What this node should hold is the manager's binding, heard as `binding.changed` and asked at start; the
|
||||
// seat's `current` says which licence, and its `access` secret is asked from the vault, sealed to this
|
||||
// module's key, at start, on a newer generation and on the vault's `secret-changed` for it, and written
|
||||
// access-token-only (design 39 bus-and-switch §19).
|
||||
// - 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.
|
||||
|
||||
@@ -30,12 +32,23 @@ import (
|
||||
"time"
|
||||
)
|
||||
|
||||
// Seat is the licence manager's role, and Manager the module whose `bindings` state this one reads.
|
||||
// Seat is the licence manager's role, and Manager the module whose events this one consumes.
|
||||
const (
|
||||
Seat = "anthropic-licence-manager"
|
||||
Manager = "claude-licence-manager"
|
||||
)
|
||||
|
||||
// VaultGet is the vault's verb a licence's access secret is asked by, and SecretChanged the vault's event that it
|
||||
// changed (design 39 bus-and-switch §19, B9).
|
||||
const (
|
||||
VaultGet = "seat:mesh-vault.get"
|
||||
SecretChanged = "mesh-vault.secret-changed"
|
||||
)
|
||||
|
||||
// AccessSecret is the name of a licence's access secret in the vault, by the licence's key as the manager says it
|
||||
// (§15, B7: the key is carried, never formed here).
|
||||
func AccessSecret(key string) string { return "licence." + key + ".access" }
|
||||
|
||||
// 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 }
|
||||
|
||||
@@ -309,13 +322,14 @@ type BindingState struct {
|
||||
By string `json:"by,omitempty"`
|
||||
}
|
||||
|
||||
// Current is what the seat answers to `current`: the licence this node is bound to and its token, sealed.
|
||||
// Current is what the seat answers to `current`: the licence this node is bound to, its key and its account, never
|
||||
// its token (§18, B7).
|
||||
type Current struct {
|
||||
Licence string `json:"licence"`
|
||||
Kind string `json:"kind"`
|
||||
Generation int64 `json:"generation"`
|
||||
Sealed *SealedBox `json:"sealed"`
|
||||
Identity *Identity `json:"identity"`
|
||||
Licence string `json:"licence"`
|
||||
Key string `json:"key"`
|
||||
Kind string `json:"kind"`
|
||||
Generation int64 `json:"generation"`
|
||||
Identity *Identity `json:"identity"`
|
||||
}
|
||||
|
||||
// Holdings is what this node holds, as the `holdings` state carries it (ADR 0206): enough for the manager
|
||||
@@ -404,40 +418,50 @@ func GrantFor(p Paths, managerPublicKey string) (GrantAnswer, error) {
|
||||
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.
|
||||
// Pull asks the seat which licence this node holds, then the vault for that licence's access secret, sealed to this
|
||||
// module's key, and applies it (§19).
|
||||
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})
|
||||
raw, err := ask(SeatVerb("current"), map[string]any{"consumer": p.Node})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var c Current
|
||||
if err := json.Unmarshal(raw, &c); err != nil || c.Sealed == nil {
|
||||
if err := json.Unmarshal(raw, &c); err != nil || c.Licence == "" || c.Key == "" {
|
||||
return map[string]any{"applied": false, "reason": "the seat holds no licence for this node"}, nil
|
||||
}
|
||||
return Apply(p, c, write)
|
||||
raw, err = ask(VaultGet, map[string]any{"name": AccessSecret(c.Key), "public_key": keys.PublicKey})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var got struct {
|
||||
Sealed *SealedBox `json:"sealed"`
|
||||
}
|
||||
if err := json.Unmarshal(raw, &got); err != nil || got.Sealed == nil {
|
||||
return nil, fmt.Errorf("the vault answered no sealed %s", AccessSecret(c.Key))
|
||||
}
|
||||
return Apply(p, c, *got.Sealed, write)
|
||||
}
|
||||
|
||||
// OnBinding takes a change to this node's key in the manager's `bindings` state (ADR 0206): the token is
|
||||
// fetched when the binding differs from the one applied. A released binding keeps the last token, which
|
||||
// lives hours, and says so.
|
||||
//
|
||||
// **Differs, not "is newer"** (novox/hq issue 243). The state keeps only the latest value per node, so
|
||||
// nothing older can arrive. A manager whose store was rebuilt counts generations from one again, and
|
||||
// a node that waited for a number above its own ignored every binding it was sent, its login and the
|
||||
// licence's rotations included, until the count caught up. Only the binding already applied is skipped.
|
||||
// OnBinding takes this node's binding as the manager said it: the token is fetched when its generation is newer than
|
||||
// the one applied (§19). A home that holds no token of the applied kind has nothing applied, so it is fetched too. 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 && applied.Licence == b.Licence &&
|
||||
holdsApplied(p, applied.Kind) {
|
||||
if readJSON(p.binding(), &applied) && b.Generation <= applied.Generation && holdsApplied(p, applied.Kind) {
|
||||
return "", nil
|
||||
}
|
||||
return fetched(p, b, ask, write)
|
||||
}
|
||||
|
||||
// fetched pulls this node's token for binding b and says the line naming it.
|
||||
func fetched(p Paths, b *BindingState, ask Ask, write WriteManaged) (string, error) {
|
||||
out, err := Pull(p, ask, write)
|
||||
if err != nil {
|
||||
return "", err
|
||||
@@ -497,21 +521,44 @@ func FirstBinding(p Paths, ask Ask, write WriteManaged) (string, error) {
|
||||
return "", fmt.Errorf("the seat's bindings answered what is not a list of bindings: %w", err)
|
||||
}
|
||||
for _, b := range listed {
|
||||
if b.Consumer == p.Node {
|
||||
return OnBinding(p, b.state(), ask, write)
|
||||
if b.Consumer == p.Node && b.state() != nil {
|
||||
// At its start the module asks the vault whatever it applied before (§19).
|
||||
return fetched(p, b.state(), ask, write)
|
||||
}
|
||||
}
|
||||
return OnBinding(p, nil, ask, write)
|
||||
}
|
||||
|
||||
// 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) {
|
||||
// secretChanged is the vault's `secret-changed` as it says it: the secret's name and generation, never its value.
|
||||
type secretChanged struct {
|
||||
Name string `json:"name"`
|
||||
Generation int64 `json:"generation"`
|
||||
}
|
||||
|
||||
// OnSecretChanged takes one `secret-changed`: acted on only when it names the access secret of the licence this node
|
||||
// holds, which is then asked again (§19).
|
||||
func OnSecretChanged(p Paths, body []byte, ask Ask, write WriteManaged) (string, error) {
|
||||
var e secretChanged
|
||||
if err := json.Unmarshal(body, &e); err != nil {
|
||||
return "", fmt.Errorf("%s is not a secret's change: %w", SecretChanged, err)
|
||||
}
|
||||
var applied Binding
|
||||
if !readJSON(p.binding(), &applied) || applied.Key == "" || e.Name != AccessSecret(applied.Key) {
|
||||
return "", nil
|
||||
}
|
||||
return fetched(p, &BindingState{Licence: applied.Licence, Kind: applied.Kind, Generation: applied.Generation,
|
||||
Why: "the vault's " + e.Name + " changed"}, ask, write)
|
||||
}
|
||||
|
||||
// Apply applies the access secret the vault handed over for the licence the seat named. 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, sealed SealedBox, write WriteManaged) (map[string]any, error) {
|
||||
keys, err := Keypair(p)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
plain, err := Open(*c.Sealed, keys.PrivateKey)
|
||||
plain, err := Open(sealed, keys.PrivateKey)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -571,7 +618,7 @@ func Apply(p Paths, c Current, write WriteManaged) (map[string]any, error) {
|
||||
if gen == 0 {
|
||||
gen = previous.Generation
|
||||
}
|
||||
raw, _ := json.Marshal(Binding{Licence: c.Licence, Kind: c.Kind, Generation: gen})
|
||||
raw, _ := json.Marshal(Binding{Licence: c.Licence, Key: c.Key, Kind: c.Kind, Generation: gen})
|
||||
if err := fsys.WriteFile(p.binding(), append(raw, '\n'), 0o600); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
@@ -50,7 +50,9 @@ type Settings struct {
|
||||
|
||||
// Binding is the licence this node holds, as it was last applied.
|
||||
type Binding struct {
|
||||
Licence string `json:"licence"`
|
||||
Licence string `json:"licence"`
|
||||
// Key is the licence's key as the manager says it, which names its access secret in the vault (§19, B7).
|
||||
Key string `json:"key,omitempty"`
|
||||
Kind string `json:"kind"` // subscription | api-key
|
||||
Generation int64 `json:"generation,omitempty"`
|
||||
}
|
||||
|
||||
@@ -29,7 +29,9 @@
|
||||
"mesh-delivery-indexer.indexer_read_modules",
|
||||
"mesh-delivery-indexer.indexer_read_manifests",
|
||||
"mesh-delivery-assigner.assigner_read_assignments",
|
||||
"seat:anthropic-licence-manager.bindings"
|
||||
"seat:anthropic-licence-manager.bindings",
|
||||
"seat:anthropic-licence-manager.current",
|
||||
"seat:mesh-vault.get"
|
||||
],
|
||||
"consumes": [
|
||||
"mesh-delivery-indexer.views.modules",
|
||||
@@ -38,7 +40,8 @@
|
||||
"mesh-catalog.seat-definition-changed",
|
||||
"mesh-catalog.seat-definition-removed",
|
||||
"claude-licence-manager.holdings.asked",
|
||||
"claude-licence-manager.binding.changed"
|
||||
"claude-licence-manager.binding.changed",
|
||||
"mesh-vault.secret-changed"
|
||||
],
|
||||
"state": [
|
||||
"servers",
|
||||
|
||||
Reference in New Issue
Block a user