Phase B — the generic, vendor-neutral refreshable-grant machinery: the ADR-0050 carve-out, built so the control plane never sees a refresh token in the clear.
Manager model: a licence.manager column (node name), set via licence manager <name> <node>; refused for static-key licences (the "manager node only" axis).
Encrypted-at-rest (internal/secrets/atrest.go): envelope encryption — the refresh token is NaCl-secretbox'd under a random 32-byte data key; only the data key is wrapped (anonymous box) to the manager node's existing sealing key. Stored in a separate refresh_grant table. A DB dump alone can't read it (needs the manager's private half, which the control plane never held). No new key material.
Refresh flow (Licences.Refresh): a transaction-scoped advisory lock (the single-refresher lease), open the at-rest grant → call the injected VendorRefresher (the Phase-C seam; the module opens the envelope on the manager node and does the OAuth call — the control plane never decrypts) → seal the returned access token per holder → replace the envelope only if the vendor rotated the refresh token. Reuses doc-13's reseal-and-publish shape; delivery is the existing push path.
Access-token-only delivery: KeyFor returns the sealed access token; the refresh token is never in a holder's delivery. Static-key keeps the full guarantee (no token to hold, carve-out never fires).
17 new tests (crypto/adapter/refresh-flow) + the 4 required security checks; go build/go vet/make check (real Postgres, migrations 0001+0003) all green.
Phase B — the generic, vendor-neutral refreshable-grant machinery: the ADR-0050 carve-out, built so the control plane never sees a refresh token in the clear.
- **Manager model**: a `licence.manager` column (node name), set via `licence manager <name> <node>`; refused for static-key licences (the "manager node only" axis).
- **Encrypted-at-rest** (`internal/secrets/atrest.go`): envelope encryption — the refresh token is NaCl-secretbox'd under a random 32-byte data key; only the data key is wrapped (anonymous box) to the **manager node's existing sealing key**. Stored in a separate `refresh_grant` table. A DB dump alone can't read it (needs the manager's private half, which the control plane never held). No new key material.
- **Refresh flow** (`Licences.Refresh`): a transaction-scoped advisory lock (the single-refresher lease), open the at-rest grant → call the injected `VendorRefresher` (the **Phase-C seam**; the module opens the envelope on the manager node and does the OAuth call — the control plane never decrypts) → seal the returned access token **per holder** → replace the envelope only if the vendor rotated the refresh token. Reuses doc-13's reseal-and-publish shape; delivery is the existing push path.
- **Access-token-only delivery**: `KeyFor` returns the sealed access token; the refresh token is never in a holder's delivery. Static-key keeps the full guarantee (no token to hold, carve-out never fires).
17 new tests (crypto/adapter/refresh-flow) + the 4 required security checks; `go build`/`go vet`/`make check` (real Postgres, migrations 0001+0003) all green.
https://claude.ai/code/session_01LrgweAeERJYBg88c5cKDzF
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
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
Phase B — the generic, vendor-neutral refreshable-grant machinery: the ADR-0050 carve-out, built so the control plane never sees a refresh token in the clear.
licence.managercolumn (node name), set vialicence manager <name> <node>; refused for static-key licences (the "manager node only" axis).internal/secrets/atrest.go): envelope encryption — the refresh token is NaCl-secretbox'd under a random 32-byte data key; only the data key is wrapped (anonymous box) to the manager node's existing sealing key. Stored in a separaterefresh_granttable. A DB dump alone can't read it (needs the manager's private half, which the control plane never held). No new key material.Licences.Refresh): a transaction-scoped advisory lock (the single-refresher lease), open the at-rest grant → call the injectedVendorRefresher(the Phase-C seam; the module opens the envelope on the manager node and does the OAuth call — the control plane never decrypts) → seal the returned access token per holder → replace the envelope only if the vendor rotated the refresh token. Reuses doc-13's reseal-and-publish shape; delivery is the existing push path.KeyForreturns the sealed access token; the refresh token is never in a holder's delivery. Static-key keeps the full guarantee (no token to hold, carve-out never fires).17 new tests (crypto/adapter/refresh-flow) + the 4 required security checks;
go build/go vet/make check(real Postgres, migrations 0001+0003) all green.https://claude.ai/code/session_01LrgweAeERJYBg88c5cKDzF