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:
Ben Stull
2026-05-25 06:08:43 -07:00
parent a255429e57
commit 55a8be051a
12 changed files with 1437 additions and 43 deletions
+10 -3
View File
@@ -23,7 +23,7 @@ from typing import Any
from fastapi import APIRouter, HTTPException, Request
from pydantic import BaseModel, Field
from . import auth, cache, chat as chat_layer, db, entry as entry_mod, models_resolver
from . import auth, cache, chat as chat_layer, db, entry as entry_mod, funder, models_resolver
from .bot import Bot
from .config import Config
from .gitea import Gitea, GiteaError
@@ -92,6 +92,7 @@ def make_router(
chat_messages = _branch_chat_excerpt(slug, branch)
rfc_default_model = models_resolver.default_model_for_rfc(slug, providers)
title, description = _draft_with_provider(
slug=slug,
providers=providers,
default_model=rfc_default_model,
rfc_title=rfc["title"],
@@ -786,6 +787,7 @@ def _branch_chat_excerpt(slug: str, branch: str, limit: int = 40) -> list[dict]:
def _draft_with_provider(
*,
slug: str,
providers: dict[str, BaseProvider],
default_model: str,
rfc_title: str,
@@ -799,11 +801,16 @@ def _draft_with_provider(
When no provider is configured — or per §6.6 the RFC's resolved
list is empty (operator universe empty, frontmatter opt-out, or
intersection empty) — we fall back to a deterministic stub. The
surface still works; the contributor edits the text.
surface still works; the contributor edits the text. Per §6.7,
when a consenting funder is in effect, the provider instance is
built from funder credentials; if the funder cannot serve the
default model, we also fall back to the stub.
"""
if not providers or not default_model:
return _stub_draft(rfc_title=rfc_title, main_body=main_body, branch_body=branch_body)
provider = providers.get(default_model) or next(iter(providers.values()))
provider = funder.provider_for_rfc(slug, default_model, providers)
if provider is None:
return _stub_draft(rfc_title=rfc_title, main_body=main_body, branch_body=branch_body)
system = (
"You are summarizing a contributor's proposed change to an RFC for an arbiter audience. "
"Output exactly two sections in this order: 'TITLE: <one line, structural, spec-voice>' "