Post-v1: per-RFC credential delegation (funder role) folded into §6.7
Second §19.2 settlement after v1. New §6.7 alongside §6.6: optional `funder:` frontmatter field names a single gitea_login; a `funder_consents` app-db row records funder-side opt-in; both halves required for the binding to activate (two-key rule). Funder universe replaces — does not augment — the operator universe per-RFC for attribution-clean resolution. Funder role grants zero §6.1/§6.3 authority. Three revocation paths each restore the operator-credentials status quo. §19.2's credential-delegation entry is split: lighter half marked settled with a pointer to §6.7; operational-realities half (mid-call failure, rotation, billing, rate-limit attribution) lives on as its own entry. Test suite is 125/125 green (106 prior + 19 new). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
+123
@@ -44,6 +44,129 @@ rare and surgical and live in the appropriate numbered section per
|
||||
|
||||
## State of the codebase
|
||||
|
||||
### Post-v1: §6.7 per-RFC credential delegation (funder role + grant shape)
|
||||
|
||||
The second §19.2 candidate settled after v1 shipped. The original
|
||||
per-RFC credential-delegation topic explicitly subdivided into "funder
|
||||
role + delegation grant" (schema/permissions-shaped) and "operational
|
||||
realities" (mid-call failure, rotation, billing, rate-limit
|
||||
attribution); this session folded in the lighter half and left the
|
||||
heavier one as its own §19.2 entry. The natural follow-on to §6.6 —
|
||||
§6.6 names *which* models are permitted, §6.7 names *whose credentials
|
||||
pay*.
|
||||
|
||||
The settlement: a new optional `funder:` frontmatter field on the
|
||||
meta-repo entry, naming a single gitea_login (parallel in shape to
|
||||
`owners:` / `arbiters:` / `models:`). The frontmatter is RFC-side
|
||||
approval (edited via the meta-repo PR flow). A `funder_consents`
|
||||
app-db row is the funder-side approval (the user opts in per-slug
|
||||
from `/settings/funder`). Both halves are required for the binding to
|
||||
be operationally active — the spec calls this the "frontmatter +
|
||||
consent hybrid" two-key rule. Either side can veto by withdrawing
|
||||
their half.
|
||||
|
||||
When a funder is in effect, the funder universe **replaces** the
|
||||
operator universe for the RFC's picker — it does not augment.
|
||||
Resolution is attribution-clean: a call on a funder-active RFC is
|
||||
paid entirely from funder credentials, never blended per-call. If
|
||||
the funder cannot serve the resolved default model, the §10.2
|
||||
draft falls back to its deterministic stub (same shape as §6.6's
|
||||
opt-out); the §8.12 chat refuses with 503. Three revocation paths
|
||||
each restore the operator-credentials status quo: funder withdraws
|
||||
consent (instant), frontmatter edit removes/changes the field
|
||||
(PR flow), funder's account disabled (the §6.1 future affordance —
|
||||
when it lands, the two-key rule extends to a three-key check).
|
||||
|
||||
§6.7 carries the structural rule. §2.1's frontmatter example shows
|
||||
the field. §6.6's last paragraph now points at §6.7 for the
|
||||
credentials half. §17 grows five funder endpoints (self-read,
|
||||
credential register/delete, consent add/withdraw). §18 reframes the
|
||||
operator-credentials default as one of two cases (the other being a
|
||||
named funder per §6.7). §19.2's credential-delegation entry is split
|
||||
— the lighter half is marked *settled* with a pointer to §6.7; the
|
||||
operational-realities half lives on as its own entry.
|
||||
|
||||
Code changes:
|
||||
|
||||
- [`backend/migrations/010_funder.sql`](../backend/migrations/010_funder.sql)
|
||||
adds `cached_rfcs.funder_login` (nullable, NULL meaning absent),
|
||||
the `user_funder_credentials` table (per-user, per-provider-family
|
||||
API key, primary key `(user_id, provider)`), and the
|
||||
`funder_consents` table (per-user, per-slug opt-in, primary key
|
||||
`(user_id, rfc_slug)`).
|
||||
- [`backend/app/entry.py`](../backend/app/entry.py) grows an optional
|
||||
`funder: str | None` field on the `Entry` dataclass. The parser
|
||||
treats empty strings and `None` as absent (one set of semantics —
|
||||
unlike `models:`, there is no second "explicit opt-out" meaning).
|
||||
The serializer emits `funder:` only when truthy.
|
||||
- [`backend/app/cache.py`](../backend/app/cache.py)'s
|
||||
`_upsert_cached_rfc` writes `funder_login` from frontmatter on the
|
||||
round-trip; the existing `refresh_meta_repo` and reconciler sweep
|
||||
carry the column through transparently.
|
||||
- [`backend/app/funder.py`](../backend/app/funder.py) is the new
|
||||
module that holds the §6.7 runtime — three responsibilities:
|
||||
the consenting-funder lookup (`consenting_funder_user_id(slug)`
|
||||
enforces the two-key rule); the universe + per-(slug,
|
||||
picker_key) provider resolver (`resolve_funder_universe`,
|
||||
`provider_for_rfc`); the credential and consent CRUD that the
|
||||
§17 endpoints in `api.py` call into. The module deliberately
|
||||
does not handle per-call fallback, instance caching, or
|
||||
rotation — those are the operational-realities §19.2 candidate.
|
||||
- [`backend/app/providers.py`](../backend/app/providers.py) grows
|
||||
three §6.7 helpers: `FUNDER_PROVIDER_FAMILIES` enumerates the
|
||||
family names (`anthropic` / `google` / `openai`),
|
||||
`provider_family_for_picker_key` maps a picker key to its family,
|
||||
and `construct_for_funder` instantiates a fresh provider from a
|
||||
funder-supplied API key (mirrors the variant table
|
||||
`load_providers` uses, without the env contract).
|
||||
- [`backend/app/models_resolver.py`](../backend/app/models_resolver.py)'s
|
||||
resolver consults `funder.resolve_funder_universe` first; when a
|
||||
consenting funder is in effect, the funder universe replaces the
|
||||
operator universe as the base set the §6.6 frontmatter intersects
|
||||
against. The replace-not-augment rule lives here.
|
||||
- [`backend/app/api_branches.py`](../backend/app/api_branches.py)'s
|
||||
chat-stream and reask paths route their provider lookup through
|
||||
`funder.provider_for_rfc(slug, model_key, providers)` instead of
|
||||
the direct `providers[model_key]` access. Same shape as before
|
||||
when no funder is named; constructs fresh on each call when one is.
|
||||
- [`backend/app/api_prs.py`](../backend/app/api_prs.py)'s
|
||||
`_draft_with_provider` takes the slug and routes through
|
||||
`funder.provider_for_rfc`. The §6.6 stub-fallback path now also
|
||||
catches the case where the funder cannot serve the default model.
|
||||
- [`backend/app/api.py`](../backend/app/api.py) adds the five
|
||||
§17 endpoints: `GET /api/users/me/funder` (lists registered
|
||||
provider families without exposing the API keys themselves plus
|
||||
the consented slugs), `POST /api/users/me/funder/credentials`
|
||||
(refused if the provider family is not in the operator's enabled
|
||||
set — the §6.7 cannot-expand-operator rule),
|
||||
`DELETE /api/users/me/funder/credentials/<provider>`,
|
||||
`POST /api/rfcs/<slug>/funder/consent` (refused if the user has
|
||||
no registered credentials — a consent without a universe would
|
||||
be inert), and `DELETE /api/rfcs/<slug>/funder/consent`.
|
||||
|
||||
Settlement ships covered by
|
||||
[`test_funder_vertical.py`](../backend/tests/test_funder_vertical.py)
|
||||
— nineteen integration tests across the two-key rule's four
|
||||
combinations (frontmatter-only inert, consent-only inert, both
|
||||
present active, neither present operator), the universe-replaces-
|
||||
not-augments rule, the §6.6 + §6.7 composition with an empty
|
||||
intersection falling into the opt-out shape, the funder-with-no-
|
||||
matching-family case, the consent-withdrawal and frontmatter-removal
|
||||
revocation paths, the API surface's read-without-API-key contract,
|
||||
the cannot-expand-operator gate, the consent-needs-credentials gate,
|
||||
the full opt-in/opt-out cycle, the delete-credential-leaves-consents-
|
||||
intact rule, the §10.2 PR-draft routing through funder credentials
|
||||
(with the API key the funder registered), the §10.2 fallback to
|
||||
stub when the funder cannot serve the default, the §8.12 chat
|
||||
routing through funder credentials, the cache round-trip from
|
||||
meta-repo frontmatter through `refresh_meta_repo`, and the entry
|
||||
parser/serializer round-trip preserving the field.
|
||||
|
||||
The full test suite is 125/125 green (106 prior + 19 new). No
|
||||
behavioral change for RFCs without `funder:` in frontmatter — the
|
||||
operator-credentials path is preserved as the default, so existing
|
||||
deployments see no surface change until a funder field lands.
|
||||
|
||||
### Post-v1: §6.6 per-RFC model availability (UX half)
|
||||
|
||||
The first §19.2 candidate settled after v1 shipped. The heavier
|
||||
|
||||
Reference in New Issue
Block a user