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:
2026-11-11 23:51:00 +01:00
parent 6eccfc286a
commit 31ce47d1cc
7 changed files with 157 additions and 76 deletions
@@ -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)
}
}