DUNIN7 · LOOMWORKS · RECORD
record.dunin7.com
Status Current
Path phases/stele-extraction-phase-7/loomworks-stele-phase-7-step-0-inspection-report-v0_1.md

Loomworks — Stele Phase 7 Step 0 Inspection Report — v0.1

Version. 0.1 Date. 2026-06-19 By. Claude Code (CC) on DUNIN7-M4 — read-only inspection per loomworks-stele-phase-7-step-0-inspection-brief-v0_1.md. Against. engine main = fe7c3f1 (CR-B merge — verified), alembic head 0093, suite 2928/0/46. Nothing changed — no engine edits, no commits, no migrations. Status. Step 0 grounding pass that precedes the Phase 7 (packaging) scoping note. Markdown primary (technical consumer).

Headline: Stele today is a pure library — 10 .py files, no APIRouter, no FastAPI app, zero route decorators of its own. The identity/auth HTTP surface lives entirely host-side, and it couples to Stele at three distinct depths. The Phase-4 mint↔onboard split already is the Stele↔host cut line (mint_principal = Stele, onboard = host-pure), which is the single most load-bearing fact for packaging. The one decision Phase 7 turns on — surfaced here, not settled — is whether Stele owns the signup/login orchestration or ships only primitive-level routes.


Ground state

| Check | Expected (brief) | Found | |---|---|---| | origin/main / HEAD | fe7c3f1 | fe7c3f1 ✓ (CR-B merge) | | alembic head | 0093 | 0093 ✓ | | suite | 2928/0/46 | inherited from CR-B close (not re-run; read-only pass) |

No drift — orientation is current. Proceeded.


A. The Stele package is a library, not a mountable unit

src/loomworks/stele/10 modules:


__init__  actor  base  credentials  models  person_totp  recovery  registry  session  webauthn

→ Stele exposes no HTTP surface of its own. It is models + primitives + service functions, consumed by the host. The brief's "mountable router" deliverable is therefore a construction, not a relocation of an existing Stele router.

B. The host HTTP surface that calls into Stele — 18 endpoints, three depths

The five auth-shaped routers (api/routers/{signup,login,me_security,claim,auth_dev}.py) carry 18 endpoints. Their coupling to Stele falls into three depths — and the depth, not the file, is the right axis for the extraction cut:

Depth 1 — direct Stele-primitive calls (cleanly Stele-owned): me_security (8) + auth_dev (1).

| Method · Path | Handler | Stele functions called | |---|---|---| | POST /me/security/passkeys/begin | passkey_begin_route | webauthn (config type); credentials.list_credentials_for_person | | POST /me/security/passkeys/complete | passkey_complete_route | Principal; webauthn config type | | GET /me/security/passkeys | passkey_list_route | credentials.list_credentials_for_person | | DELETE /me/security/passkeys/{passkey_id} | passkey_revoke_route | credentials.list_credentials_for_person · .remove_credential · .LastPasskeyError | | POST /me/security/recovery-codes/regenerate | recovery_codes_regenerate_route | recovery.regenerate_recovery_codes | | GET /me/security/recovery-codes | recovery_codes_status_route | recovery.count_unused_recovery_codes · .count_active_recovery_codes | | POST /me/security/totp/rotate/begin | totp_rotate_begin_route | person_totp.begin_totp_rotation | | POST /me/security/totp/rotate/confirm | totp_rotate_confirm_route | person_totp.confirm_totp_rotation · .PersonTotpCodeInvalid | | POST /auth/dev/issue-session | issue_session_dev | registry.get_principal_by_id · session.issue_session (dev-only) |

All 8 me_security handlers take person: Principal = Depends(get_current_principal) — the Stele Principal type (the dependency function is host, api/deps.py).

**Depth 2 — service-mediated (router thin; logic in persons.*): signup (3) + login (5).** The routers touch Stele only via the WebauthnConfig type and the COOKIE_NAME session cookie; the substance is one layer down (see §C).

| Method · Path | Handler | Router-level Stele touch | |---|---|---| | POST /auth/signup/begin | signup_begin_route | WebauthnConfig (type) | | POST /auth/signup/passkey | signup_passkey_route | WebauthnConfig (type) | | POST /auth/signup/totp-verify | signup_totp_verify_route | WebauthnConfig + COOKIE_NAME (set) | | POST /auth/login/begin | login_begin_route | WebauthnConfig (type) | | POST /auth/login/complete | login_complete_route | WebauthnConfig + COOKIE_NAME (set) | | POST /auth/login/totp-verify | login_totp_verify_route | COOKIE_NAME (set) | | POST /auth/login/recovery | login_recovery_route | none direct (delegates to persons.login) | | POST /auth/logout | logout_route | COOKIE_NAME indirectly via local _clear_session_cookie |

Depth 3 — type-only / host-owned: claim (1).

| Method · Path | Handler | Stele touch | |---|---|---| | POST /claim/grant | claim_grant_route | WebauthnConfig (dep type only) — a credit/grant-claim concern, not identity |

C. Depth-2 traced one level down: persons.signup / persons.login → Stele

The depth-2 routers delegate to two service modules that hold the real orchestration and the heavy Stele binding:

| Router endpoint | persons.* service fn | Stele functions it calls | |---|---|---| | /auth/signup/begin | signup.begin_signup | webauthn.begin_registration | | /auth/signup/passkey | signup.complete_passkey | webauthn.verify_registration | | /auth/signup/totp-verify | signup.complete_totp_verify | registry.mint_principal · session.issue_session · → onboard (host-pure) | | /auth/login/begin | login.begin_login | webauthn.begin_authentication | | /auth/login/complete | login.complete_login | credentials.get_credential_by_credential_id · webauthn.verify_authentication · credentials.update_sign_count · → _heal_if_orphanedonboard (host) | | /auth/login/totp-verify | login.verify_totp_for_partial_session | session.issue_session | | /auth/login/recovery | login.verify_recovery_for_partial_session | recovery.verify_and_consume_recovery_code · session.issue_session |

The Stele binding surface the service layer pulls (the relocation target)

| Stele module | Functions bound by persons.signup / persons.login | Notes | |---|---|---| | stele/webauthn | begin_registration, verify_registration, begin_authentication, verify_authentication | heaviest — the ceremony engine; used by both flows | | stele/registry | mint_principal (signup); get_principal_by_id (auth_dev); Principal type (me_security) | the principal mint + lookup | | stele/session | issue_session; COOKIE_NAME | session-token mint + cookie name | | stele/credentials | get_credential_by_credential_id, update_sign_count, add_credential, list_credentials_for_person, remove_credential, LastPasskeyError | credential store + last-passkey guard | | stele/recovery | verify_and_consume_recovery_code, regenerate_recovery_codes, count_unused/count_active | recovery codes | | stele/person_totp | begin_totp_rotation, confirm_totp_rotation, PersonTotpCodeInvalid | TOTP rotation | | stele/models | PrincipalRow (type) | everywhere |

D. The mint↔onboard split already IS the Stele↔host cut line

signup.complete_totp_verify calls mint_principal (Stele) and then onboard — and onboard is host-pure: persons/onboard.py imports exactly one Stele symbol, the PrincipalRow type (the only other mint_principal mention is in its docstring). It makes zero Stele function calls; its body builds the host rows (host_account, the credit grant, membership). The same onboard is reached from login._heal_if_orphaned (the Phase-4 heal path).

→ The Phase-4 two-phase split maps exactly onto the package boundary: mint_principal (+ the ceremony / session / credential / recovery / TOTP primitives) goes into Stele; onboard (host account, credit, membership) stays host-side, with no untangling required. The boundary is already drawn in the code. This is the gift Phase 7 inherits.

E. The decision Phase 7 turns on (surfaced, not settled)

Because the auth logic lives in persons/signup.py + persons/login.py (the routers are thin), the "mountable router" deliverable forks:

This is the scoping note's central decision; Step 0 only frames it.

F. Migration / packaging-shape signals (reported, not designed)

  1. No schema work implied by the router question. CR-B left principals as the sole identity table (alembic 0093); Stele's MetaData is already self-contained (its own SteleBase, zero outward FK — confirmed across CR-A/C). Packaging is a code-and-surface concern, not a migration.
  2. persons/signup.py is doubly-bound. It also holds add_passkey_begin / add_passkey_complete, which me_security's passkey-enrollment endpoints call — so one module serves both the signup flow and the me_security enrollment surface. Splitting it touches both; the scoping note should treat it as a shared module, not a signup-only one.
  3. stele/webauthn is the load-bearing dependency — 4 distinct ceremony functions across both flows. Whatever the fork resolution, the WebAuthn ceremony engine is the core binding the service layer cannot do without; the reference-UI WebAuthn-ceremony gap (named in the body-of-work brief as closing at Phase 7) attaches here.
  4. The session/cookie seam. COOKIE_NAME (Stele) is set by signup/login handlers and cleared by logout via the host _clear_session_cookie helper; api/deps.py resolves sessions via stele.session. The host owns the cookie mechanics (set/clear on the FastAPI Response) over a Stele-owned token. A standalone Stele must decide whether it ships the cookie set/clear helpers or leaves them to the host.

Inventory-lens caution (carried from CR-B, validated again here)

CR-B's banked lesson — an inventory's lenses define its blind spots — reproduces exactly in this surface. "Endpoints that call into Stele" is not grep-able as @router in stele/ (zero hits). The real surface is host routers whose handlers reach into Stele, at three depths, plus a service layer (persons.*) one level further down that holds the heaviest binding. A flat "move the auth routers" framing would conflate the direct-primitive me_security surface, the service-mediated signup/login surface, and the host-owned claim touch — three different jobs. The scoping note should cut along depth (§B 1→2→3) and resolve the §E fork, not partition by file.


DUNIN7 — Done In Seven LLC — Miami, Florida Loomworks — Stele Phase 7 Step 0 Inspection Report — v0.1 — 2026-06-19