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
+11 -3
View File
@@ -29,7 +29,7 @@ from fastapi import APIRouter, HTTPException, Request
from fastapi.responses import StreamingResponse
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
@@ -576,7 +576,11 @@ def make_router(
fetched = await gitea.read_file(owner, repo, path, ref=branch)
body_text = _extract_body(rfc, fetched[0], branch) if fetched else ""
provider = providers[reask_model]
# §6.7: pick the funder-credentialed instance when a consenting
# funder is in effect on this RFC; otherwise the operator's.
provider = funder.provider_for_rfc(slug, reask_model, providers)
if provider is None:
raise HTTPException(503, "No AI providers configured")
system = chat_layer.build_system_prompt(title=rfc["title"], body=body_text)
history = chat_layer.build_history(thread_id)
reask_prompt = (
@@ -907,7 +911,11 @@ def make_router(
if not resolved:
raise HTTPException(503, "No AI providers configured")
model_key = body.model if body.model in resolved else resolved[0]
provider = providers[model_key]
# §6.7: pick the funder-credentialed instance when a consenting
# funder is in effect on this RFC; otherwise the operator's.
provider = funder.provider_for_rfc(slug, model_key, providers)
if provider is None:
raise HTTPException(503, "No AI providers configured")
# Fetch the live branch body so the prompt is anchored to
# what's in Gitea right now, not the cache. For super-draft,