M-8: current answers the licence and its health, never the token
The vault hands every token (ADR 0550 §3); current names the licence, its key, kind, generation, health and access expiry, and keeps the account's identity for the agent's account file. bindings takes an optional consumer, answered unpaged. Builds: design 39 bus-and-switch §18, B7, B10, B19 Claude-Session: https://claude.ai/code/session_01NcbgfHb9d4T9nq5Q6WqdiJ
This commit is contained in:
@@ -259,8 +259,12 @@ func tools() []stdio.Tool {
|
||||
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("bindings", "Which licence each consumer (a node's agent, by the node's name) is bound to, and the generation it was last given; with consumer, that consumer's alone.",
|
||||
map[string]any{"consumer": str("a consumer; absent for every one")},
|
||||
func(ctx context.Context, m *Manager, a map[string]any) (any, error) {
|
||||
c, _ := a["consumer"].(string)
|
||||
return m.BindingsOf(ctx, strings.TrimSpace(c))
|
||||
}),
|
||||
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) {
|
||||
@@ -334,14 +338,14 @@ func tools() []stdio.Tool {
|
||||
nil, func(_ context.Context, m *Manager, _ map[string]any) (any, error) {
|
||||
return map[string]any{"public_key": m.Keys.PublicKey}, nil
|
||||
}),
|
||||
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")},
|
||||
verb("current", "For a consumer's agent module: which licence it is bound to, the licence's key, kind and generation, its health (ok, failing or expiring) and when its access token expires; never the token, which the vault hands. Null when it is bound to nothing.",
|
||||
map[string]any{"consumer": consumer},
|
||||
func(ctx context.Context, m *Manager, a map[string]any) (any, error) {
|
||||
c, k, err := two(a, "consumer", "public_key")
|
||||
c, err := text(a, "consumer")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return m.Current(ctx, c, k)
|
||||
return m.Current(ctx, c)
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -636,47 +636,57 @@ func (m *Manager) ReadUsage(ctx context.Context) error {
|
||||
|
||||
// ---- 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
|
||||
// Current is which licence a consumer is bound to and its health, never the token (design 39 bus-and-switch §18, B7,
|
||||
// B19): the licence, its key (the name of its secrets in the vault), kind, generation, health and accessExpiresAt, and
|
||||
// for a subscription the account's identity the agent module points its account file at. 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")
|
||||
}
|
||||
func (m *Manager) Current(ctx context.Context, consumer string) (map[string]any, error) {
|
||||
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 == "" {
|
||||
if err != nil || l == nil {
|
||||
return nil, err
|
||||
}
|
||||
plain, err := m.Crypt.Open(l.Sealed)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
out := map[string]any{"licence": l.Name, "key": KeyFor(l.Name), "kind": l.Kind, "generation": b.Generation,
|
||||
"health": Health(*l, m.Now(), m.Settings)}
|
||||
if l.AccessExpiresAt != 0 {
|
||||
out["accessExpiresAt"] = time.UnixMilli(l.AccessExpiresAt).UTC().Format(time.RFC3339)
|
||||
}
|
||||
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
|
||||
}
|
||||
|
||||
// Health is a licence's health (B19): `failing` once its refresh failed failures_to_notify times in a row, `expiring`
|
||||
// when its refresh token expires within refresh_warn_days, else `ok`.
|
||||
func Health(l Licence, now time.Time, s Settings) string {
|
||||
switch {
|
||||
case l.Failures >= s.FailuresToNotify:
|
||||
return "failing"
|
||||
case l.RefreshExpiresAt != 0 && l.RefreshExpiresAt-now.UnixMilli() < s.RefreshWarn.Milliseconds():
|
||||
return "expiring"
|
||||
}
|
||||
return "ok"
|
||||
}
|
||||
|
||||
// BindingsOf is every binding, or the one of consumer when one is named (B10): a list either way, unpaged.
|
||||
func (m *Manager) BindingsOf(ctx context.Context, consumer string) ([]Binding, error) {
|
||||
all, err := m.Store.Bindings(ctx)
|
||||
if err != nil || consumer == "" {
|
||||
return all, err
|
||||
}
|
||||
out := []Binding{}
|
||||
for _, b := range all {
|
||||
if b.Consumer == consumer {
|
||||
out = append(out, b)
|
||||
}
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
// Bind binds or switches a consumer: a person's act (ADR 0183), told to the consumer as a new generation, and
|
||||
// recorded with the cause ctx carries — the verb and its proven caller (novox/hq issue 625).
|
||||
func (m *Manager) Bind(ctx context.Context, consumer, licence string) (map[string]any, error) {
|
||||
|
||||
@@ -253,32 +253,39 @@ func TestARotationAndASwitchEachGiveANewerGeneration(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestCurrentHandsAnAccessTokenOnlySealedToTheConsumersKey(t *testing.T) {
|
||||
// design 39 bus-and-switch §18, B7, B19: no answer of current holds a token; it names the licence's key and health.
|
||||
func TestCurrentHoldsNoToken(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" {
|
||||
answer, err := mm.m.Current(ctx, "laptop")
|
||||
if err != nil || answer["kind"] != "subscription" || answer["key"] != KeyFor(licence1) || answer["health"] != "ok" ||
|
||||
answer["accessExpiresAt"] == nil || answer["identity"] == nil {
|
||||
t.Fatalf("%v %v", answer, err)
|
||||
}
|
||||
box := answer["sealed"].(SealedBox)
|
||||
plain, err := Open(box, node.PrivateKey)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
raw, _ := json.Marshal(answer)
|
||||
for _, token := range []string{"rt-", "at-", "sealed"} {
|
||||
if strings.Contains(string(raw), token) {
|
||||
t.Fatalf("current answered %s: %s", token, raw)
|
||||
}
|
||||
}
|
||||
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)
|
||||
l, _ := mm.store.Licence(ctx, licence1)
|
||||
for failures, health := range map[int]string{0: "ok", 3: "failing"} {
|
||||
l.Failures = failures
|
||||
if got := Health(*l, mm.now, mm.m.Settings); got != health {
|
||||
t.Errorf("%d failures: %s, want %s", failures, got, health)
|
||||
}
|
||||
}
|
||||
other, _ := GenerateKeyPair()
|
||||
if _, err := Open(box, other.PrivateKey); err == nil {
|
||||
t.Fatal("the hand-over opened with another key")
|
||||
l.Failures, l.RefreshExpiresAt = 0, mm.now.Add(48*time.Hour).UnixMilli()
|
||||
if got := Health(*l, mm.now, mm.m.Settings); got != "expiring" {
|
||||
t.Errorf("a refresh token expiring in two days: %s", got)
|
||||
}
|
||||
if a, _ := mm.m.Current(ctx, "nobody", node.PublicKey); a != nil {
|
||||
if a, _ := mm.m.Current(ctx, "nobody"); a != nil {
|
||||
t.Fatalf("a consumer bound to nothing was answered %v", a)
|
||||
}
|
||||
if bs, _ := mm.m.BindingsOf(ctx, "laptop"); len(bs) != 1 || bs[0].Consumer != "laptop" {
|
||||
t.Fatalf("bindings of laptop: %v", bs)
|
||||
}
|
||||
}
|
||||
|
||||
func TestNothingTheManagerPublishesKeepsOrListsCarriesAToken(t *testing.T) {
|
||||
|
||||
Reference in New Issue
Block a user