The ADR 0050 carve-out, built generic and vendor-neutral. A refreshable-grant
licence records one manager node; that node holds the refresh token encrypted at
rest, access tokens are still sealed per holder, and the refresh token is never in
a holder's delivery. Bounded on the three stated axes: refreshable-grant vendors
only, the refresh token only, the manager node only. Anthropic's actual OAuth
refresh stays a Phase-C plug-in behind a clean seam.
- New at-rest crypto (secrets.SealAtRest/OpenAtRest): envelope encryption distinct
from the per-holder anonymous-box seal. The refresh token is under a symmetric
data key (secretbox); the data key is wrapped to the manager node's public
sealing key. The database alone holds ciphertext and a wrapped key with no
private half to open either — only the manager node reads it back.
- Refreshable-grant adapter dispatch: anthropic is now refreshable-grant,
anthropic-api-key the static-key second case. The adapter implements the
Refresher seam by delegating to an injected VendorRefresher (the Phase-C plug,
none shipped). static-key is untouched. The type assertion to Refresher is what
gates the carve-out to refreshable-grant vendors.
- Refresh lease/rotate/publish flow (Licences.Refresh): a transaction-scoped
advisory lock is the single-refresher lease; the new access token comes from the
vendor refresh, is sealed per holder (secrets.Seal, as Accept does) and delivered
on the next push — doc 13's reseal-and-publish half, all-or-nothing. The refresh
token stays put, re-encrypted at rest only if the vendor rotated it.
- Manager and refresh_grant schema: consolidated into migrations/0001 and carried
by a new incremental 0003 (the dual-write rule).
- 17 new tests, including the four security checks: KeyFor never carries the
refresh token, a static key has no manager and cannot be refreshed, the at-rest
token needs the manager's key, and a refresh delivers a new sealed access token.
Claude-Session: https://claude.ai/code/session_01LrgweAeERJYBg88c5cKDzF