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.
| 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.
src/loomworks/stele/ — 10 modules:
__init__ actor base credentials models person_totp recovery registry session webauthn
APIRouter and no FastAPI defined anywhere in stele/ (grep: zero matches).stele/. The verb-pattern sweep matched only false positives, each read and dismissed: credentials.py:185 await db.delete(row) (a SQLAlchemy session delete), and webauthn.py:197/220/223 response.get(...) / credential.get(...) / a docstring mention of navigator.credentials.get() (dict accessors).→ 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.
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 |
persons.signup / persons.login → SteleThe 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_orphaned→onboard (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 |
| 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 |
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.
Because the auth logic lives in persons/signup.py + persons/login.py (the routers are thin), the "mountable router" deliverable forks:
begin_signup/complete_passkey/complete_totp_verify/begin_login/complete_login/verify_ into Stele; Stele ships full /auth/signup/ + /auth/login/ routes. Cost: these functions call onboard (host), so Stele would call back into a host-supplied onboarding hook — a new Stele→host callback seam to design. Benefit: the marquee sign-up / sign-in flows are in* the package.me_security / auth_dev shape — direct primitive calls). signup/login stay host compositions over Stele primitives; the reference app wires them. Benefit: smaller, cleaner Stele surface, no callback seam. Cost: the headline flows aren't in the package — the clone-and-run bar leans on the reference app to demonstrate them.This is the scoping note's central decision; Step 0 only frames it.
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.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.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.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.
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