From b8e8be619da785903a549415822cf1677db1a38a Mon Sep 17 00:00:00 2001 From: jochen Date: Thu, 12 Nov 2026 01:02:54 +0100 Subject: [PATCH] M-15 fix: resync hands the vault each access token built from its grant (issue 1409) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Review round 1, finding 4: the vault hands a licence's access token only to a machine a binding names, so licences_resync builds it from the grant (the key itself for an API key) instead of reading it back. Builds: design 39 bus-and-switch §15, §17, §42, B23, B24 Claude-Session: https://claude.ai/code/session_01NcbgfHb9d4T9nq5Q6WqdiJ --- .../cmd/claude-licence-manager/manager_test.go | 6 ++++++ .../cmd/claude-licence-manager/tools.go | 10 +++++++++- .../cmd/claude-licence-manager/tools_test.go | 1 + 3 files changed, 16 insertions(+), 1 deletion(-) diff --git a/modules/claude-licence-manager/cmd/claude-licence-manager/manager_test.go b/modules/claude-licence-manager/cmd/claude-licence-manager/manager_test.go index 09c91fbe7..c94dbc3f1 100644 --- a/modules/claude-licence-manager/cmd/claude-licence-manager/manager_test.go +++ b/modules/claude-licence-manager/cmd/claude-licence-manager/manager_test.go @@ -61,6 +61,9 @@ type memGrants struct { mu sync.Mutex held map[string]heldGrant puts []string + // refuseAccess is the vault refusing a licence's access token to the manager's machine, as it does for a licence no + // binding there names (§17). + refuseAccess bool } type heldGrant struct { @@ -86,6 +89,9 @@ func (g *memGrants) Get(_ context.Context, key, part string) (string, error) { g.mu.Lock() defer g.mu.Unlock() h, ok := g.held[SecretName(key, part)] + if g.refuseAccess && part == PartAccess { + return "", fmt.Errorf("no consumer on this machine is bound to the licence %s", key) + } if !ok { return "", fmt.Errorf("the vault holds no secret named %s", SecretName(key, part)) } diff --git a/modules/claude-licence-manager/cmd/claude-licence-manager/tools.go b/modules/claude-licence-manager/cmd/claude-licence-manager/tools.go index 706400efc..f7bbd4dea 100644 --- a/modules/claude-licence-manager/cmd/claude-licence-manager/tools.go +++ b/modules/claude-licence-manager/cmd/claude-licence-manager/tools.go @@ -338,7 +338,15 @@ func (m *Manager) Resync(ctx context.Context) (map[string]any, error) { continue } key := KeyFor(l.Name) - access, err := m.Grants.Get(ctx, key, PartAccess) + // The access token is built from the grant, which the vault hands the manager; it hands a licence's access + // token only to a machine a binding names (§17), and the manager's machine may have none. + access, err := m.Grants.Get(ctx, key, PartGrant) + if err == nil && l.Kind == "subscription" { + var g FullGrant + if err = json.Unmarshal([]byte(access), &g); err == nil { + access = accessOf(g) + } + } if err == nil { err = m.Grants.Put(ctx, key, PartAccess, l.Generation, access) } diff --git a/modules/claude-licence-manager/cmd/claude-licence-manager/tools_test.go b/modules/claude-licence-manager/cmd/claude-licence-manager/tools_test.go index 01c41af28..b54e8e969 100644 --- a/modules/claude-licence-manager/cmd/claude-licence-manager/tools_test.go +++ b/modules/claude-licence-manager/cmd/claude-licence-manager/tools_test.go @@ -259,6 +259,7 @@ func TestResyncSaysEachOnce(t *testing.T) { mm.m.Settings.WarnAt = 100 _ = mm.m.SayConditions(ctx, false) mm.events, *said = nil, nil + mm.grants.refuseAccess = true // §42: the access token is built from the grant, never read back from the vault got := answer(t, served(t, mm)["licences_resync"], nil).(map[string]any) count := map[string]int{} for _, e := range mm.events {