DUNIN7 · LOOMWORKS · RECORD
record.dunin7.com
Status Current
Path change-requests/cr-2026-101-founder-authority-principal-and-admin-gate-v0_1.md

Change Request — Platform-founder authority principal + require_founder + /admin/* gate — v0.1

CR number. CR-2026-101 Version. 0.1 Date. 2026-06-12 Repo (code). loomworks-engine (+ a verified no-op repoint check in loomworks Operator Layer — Step 4) Repo (this document). loomworks-record, change-requests/ Anchor. origin/main @ 158d4bc (engine HEAD at drafting; §3 facts re-verified here). Predecessor. cr-2026-098-engine-api-boundary-closure-v0_3.md (deferred "A3" — the founder-identity decision — to "a focused follow-up CR." This is that follow-up.) Companion deliverable. standing-notes/loomworks-standing-note-platform-founder-authority-v0_1 (records that a platform-authority principal now exists; cross-referenced in §7). Settled rulings. R1–R4, Operator-confirmed 2026-06-12 (founder-authority CR-drafting handoff v0.1). Built to as settled; not re-decided here.

> CR-number note. The handoff said "next after 098." 099 and 100 are already assigned (CR-2026-099 Companion orientation & discovery; CR-2026-100 first-class personal recall — both landed and tagged), so the next free number is 101. The change-requests/ directory holds only cr-2026-060 and cr-2026-098 as numbered files, which is why a directory-only check misleads; a full repo grep gives 100 as the current max.


1. Plain-language summary

The Loomworks engine has no platform-level founder identity. The three /admin/* credit routes — POST /admin/grants, GET /admin/grants, POST /admin/provisioning — are reachable by any authenticated person, a known, accepted, temporary exposure that CR-2026-098 v0.3 deferred as "A3." This session settled the authority model (rulings R1–R4 below). This CR builds it: it stores the founder identity as a singleton, builds one shared require_founder dependency, and **applies it to the three /admin/* routes** — closing the exposure.

What changes: a system_config row names the founder; a new require_founder dependency 403s any caller who is not the founder; the three admin routes gain that gate. What does not change: the founder's own use of the admin-grants UI (their session passes); the routes' behaviour for the founder; everything outside /admin/*.

Scope out (explicit): operator-mediated account recovery (seed mechanism 3) is not in this CR. It is the next arc, built on the require_founder dependency this CR creates (R4). No audit ledger for routine admin routes (R2). No account_status gating (self-service by design). No PersonRow role column, no multi-founder, no grantable authority (R1 singleton).

This CR rests on the 2026-06-12 rulings R1–R4; it does not reopen them.


2. Inheritance — why now

CR-2026-098 v0.3 removed dead /admin/ surface but deliberately deferred the founder-identity decision, recording: "the founder/platform-authority decision for /admin/grants and /admin/provisioning … Until this lands, grants and provisioning remain reachable by any authenticated person — a known, accepted, temporary exposure, recorded here."* It named the three open sub-questions: who the founder is and where stored; door-vs-ledger; a single shared authorization dependency.

The live Operator Layer admin-grants UI made the exposure real rather than hypothetical (a built browser consumer of POST/GET /admin/grants). The Companion functional spec v0.3 §10.3 then recorded operator-mediated recovery as "committed, deferred … gated on a prerequisite the Engine has deliberately not yet built — a platform-founder authority principal." This CR is the settled resolution of that prerequisite. It answers all three sub-questions (R1: who/where; R2: door; R3: one dependency) for the /admin/* scope.


3. Verified inheritance (re-verified at engine HEAD 158d4bc)

Re-confirmed against current HEAD at drafting (Step-0 was also @ 158d4bc; HEAD unchanged, line numbers current):

Nothing material differs from Step-0. If, at implementation HEAD, any of the above has moved, halt and surface before building (drafting-handoff-encodes-stated-state).


4. The settled model (R1–R4 — built design, not open questions)


5. Change set (per-step; each independently committable; two-pass on existing code; halt before push)

Step 1 — Founder identity: store, read, and bootstrap

A first-class way to store and read the founder, and an explicit way to set the first one.

(Alternative considered and set aside: folding the reader into the system_config package. A named authority/ module was chosen because the standing note frames platform-founder authority as a distinct authority kind above engagement scope; a discoverable home serves the recovery CR that builds on it.)

(Migration alternative considered and set aside: a Fernet-encrypted value is key-dependent, so an Alembic data-migration would have to either hardcode/recompute the ciphertext or perform app-level encryption inside the migration — clumsy and brittle. A management script using the existing set_system_config encrypt path is the clean bootstrap. Recorded so the choice is not silent.)

Step 2 — require_founder dependency (one seam, in deps.py)

In src/loomworks/api/deps.py, mirroring require_operator_designation (:449) in register:


async def require_founder(
    person: Person = Depends(get_current_person),
    db: AsyncSession = Depends(get_db_session),
) -> Person:
    """Refuse with 403 when the authenticated caller is not the platform founder.

    Resolves the current person (so unauthenticated callers are already 401'd
    by get_current_person), reads the stored founder UUID, and 403s on mismatch.
    Returns the person so a gated route can keep using it directly."""
    founder_id = await get_platform_founder_person_id(db)
    if founder_id is None or person.id != founder_id:
        raise HTTPException(
            status_code=status.HTTP_403_FORBIDDEN,
            detail="Founder authority required for this action.",
        )
    return person

One definition. The 403 detail matches the existing guard's plain register ("Operator designation required for this action." → "Founder authority required for this action."). When no founder is set (founder_id is None), the routes fail closed (403) — a deliberate safe default: an un-bootstrapped deployment exposes nothing rather than everything. (This is a behaviour change from today's open state; it is the closure itself.)

Step 3 — Apply the gate to the three /admin/* routes (two-pass; existing code)

In admin_grants.py, change each of the three handlers' person dependency from Depends(get_current_person) to Depends(require_founder) (the dependency returns the Person, so the handler body keeps its person parameter unchanged — minimal diff, no body rewrite):

Update the router docstring (lines ~11–13): remove the "gated by get_current_person only — any authenticated person — pending the founder-authority follow-up" exposure note; replace with the now-built founder gate (e.g. "gated by require_founder: the three /admin/* credit routes are founder-only; non-founders receive 403"). Two-pass: inspect-and-confirm the exact current docstring + dependency lines at implementation HEAD before editing.

Step 4 — Operator Layer repoint (cross-repo loomworks) — verified finding, NOT assumed

Finding (investigated, not assumed): the OL admin-grants UI does not break when the gate lands; no OL code change is required. Evidence at loomworks main c17539a:

Net: for the founder's session the routes pass — no behavioural change. For a non-founder reaching /admin/grants by URL, the engine now 403s → the existing permission-error card renders correctly (the copy "You don't have permission to issue grants." reads correctly for the new meaning). Step 4 is therefore a verification-only step: confirm the founder bootstrap (Step 1) sets the founder to the operator's person so their session passes, and confirm no OL change is needed. One optional, non-blocking polish is noted but not done in this CR: GrantList's reuse of the form-centric errorUnauthorizedTitle is acceptable but could later get list-specific copy — out of scope here.


6. Tests


7. What this CR does NOT do


8. After this CR (recorded, not built here)

Operator-mediated recovery (seed mechanism 3) is the next arc — its own scoping, built on the require_founder dependency this CR creates. It adds: the ledger (account_recovery_granted event following the write_*_event pattern, audit/events.py), the person-level credential-reset mechanics, and the out-of-band verification boundary. It is the highest-stakes consumer of this gate and was deferred to exactly it. Not this CR's work. The companion standing note (below) records the gate as the unblock condition so both threads — CR-2026-098 v0.3 and Companion spec v0.3 §10.3 — close.


Halt note

This CR is paired with standing-notes/loomworks-standing-note-platform-founder-authority-v0_1 (HTML + Markdown), which records that a platform-authority principal — a new authority kind above engagement scope — now exists in the engine. Drafted for Operator review; no code, no commit, no push until the CR text is approved.


DUNIN7 — Done In Seven LLC — Miami, Florida Change Request — Platform-founder authority principal + require_founder + /admin/* gate — CR-2026-101 — v0.1 — 2026-06-12