DUNIN7 · LOOMWORKS · RECORD
record.dunin7.com
Status Current
Path phases/stele-extraction-phase-7/loomworks-stele-phase-7-p7-3-completion-record-v0_1.md

Loomworks — Stele Phase 7 P7-3 (Ceremony Lift + Reference App + Browser Ceremony + Config Generator + Docs) Completion Record — v0.1

Version. 0.1 Date. 2026-06-19 Status. Completion record. CR-2026-116 (Stele Phase 7 P7-3) is built, verified, and pushed across stele and loomworks-engine; the docs (§5) and this record are staged in loomworks-record. The ceremony is lifted into stele.webauthn; a runnable reference host at stele/examples/ mounts stele.router and composes signup + login; a browser front drives the real WebAuthn ceremony; a config generator scaffolds the env; the adopter docs are written. This closes Phase 7. Markdown primary (technical consumer); the §5 docs are adopter-facing (HTML primary + Markdown source).

Plain-language summary

P7-2 made stele.router mountable. P7-3 takes Stele from "a stranger can mount the router" to "a stranger can clone a reference host, run it, and use Stele end-to-end — registering a real passkey in a browser." Five deliverables, sequenced lift-first: the add-passkey enrollment ceremony moved out of the engine and into stele.webauthn (dissolving the P7-2 asymmetry and collapsing the mount to two required slots); a minimal standalone FastAPI reference host; a browser WebAuthn front; an env-scaffolding config generator; and the adopter's mount-contract guide. One CR step — §1.6, neutralizing a brand-leaked cookie constant — was deferred when the grounding grep exposed that the engine consumes that constant as its live cookie name (renaming it would log every engine user out). It is a named parked thread, not a loss.

1. What was built

  1. The ceremony lift (§1)add_passkey_begin / add_passkey_complete (+ their pending store, types, and enrollment exceptions) moved from loomworks-engine/persons/signup.py into stele.webauthn, beside the begin_registration / verify_registration primitives they already wrapped. The three injection slots (provide_add_passkey_store / provide_add_passkey_begin / provide_add_passkey_complete) disappeared; the post-lift mandatory slot set is exactly two (provide_db_session + provide_webauthn_config). The engine re-pointed its single consumer surface onto stele.webauthn; persons/signup.py shrank to the intact signup flow.
  2. The reference host (§2)stele/examples/reference_app/: a minimal standalone FastAPI app that mounts stele.router, wires the two required slots, and composes signup + login host-side over Stele primitives. Both delivery shapes (cookie + bearer) over one Stele-minted token; enrollment (the mounted routes) and login (passkey assertion → second factor → full session), with login-TOTP composed host-side.
  3. The browser ceremony (§3)static/index.html + static/app.js: the smallest correct front running the real navigator.credentials.create() / .get() ceremony, with the base64url ↔ ArrayBuffer coercions that are the actual trick.
  4. The config generator (§4)examples/generate_env.py: emits a .env template with a freshly generated STELE_SECRET_KEY (a real Fernet key) plus the fields to fill. Env-scaffolding only; emits no code, so it cannot drift.
  5. The docs (§5)standards/loomworks-stele-mount-contract-v0_1.{html,md}: the mount contract written down — the slot set, the authorization boundary, the cookie-vs-bearer choice, the copy-runnable walkthrough, the RP-origin-match gotcha, and the no-email-as-identity note. HTML primary + Markdown source.

2. The build

Lift — stele (2 commits): ef69b88 §1.1 (ceremony + store + exceptions + Stele-native TTL into stele.webauthn); cfb779f §1.2 (router calls the internal ceremony; three slots + Protocols removed; SDK surface + mount docstring updated).

Lift — loomworks-engine (2 commits): 01d4aa6 §1.3+§1.7 (re-point me_security + deps + app.py onto stele.webauthn; the passkey_complete route catches Stele's PasskeyEnrollmentNotFound/PasskeyEnrollmentError; update the P7-2 mount test to the post-lift shape); 32c1a68 §1.4 (delete the now-dead ~109-line add-passkey block from persons/signup.py).

Reference app — stele (3 commits): df0e4c5 §2 (reference host + docker-compose + README); 402f8b0 §3 (browser front); 8246729 §4 (config generator).

Per-step discipline: suite green at each step; explicit-path staging throughout; HALT-before-push at each repo boundary, with the staged state reported for authorization before every push.

3. The mount, as built

The post-lift injection contract — what an adopter wires:

| Slot | Required? | Supplied | |---|---|---| | provide_db_session | Required | A request-scoped AsyncSession (raises until supplied). | | provide_webauthn_config | Required | A WebauthnConfig(rp_id, rp_name, rp_origin) (raises until supplied). | | resolve_current_principal | Defaulted | Default gates session + 2FA; override for host authorization. | | extract_token | Defaulted | Default bearer; override for cookie delivery. | | provide_secret_key | Defaulted | Default reads STELE_SECRET_KEY. | | provide_person_email | Defaulted | Default None — no email as identity. |

The passkey-enrollment ceremony is Stele's own; there is no ceremony slot. import stele stays FastAPI-free; stele.router loads FastAPI lazily.

4. As-built decisions (preserved, not smoothed)

  1. §1.6 deferred — the cookie constant is load-bearing for the engine, not a cosmetic leak. The CR assumed stele.session.COOKIE_NAME = "loomworks_session" was an unread brand-leak ("two-site touch"). The §1.6 grep-first instruction — built to catch exactly this — found the engine sets and reads its live production session cookie through that constant at ~10 sites across 4 files (login.py, signup.py, me_reactivate.py, deps.py). Renaming it would change the engine's cookie name on deploy and invalidate every live session (mass logout). CC halted and surfaced; the Operator deferred §1.6 (option A). Parked thread (see §7): a properly-sequenced engine-decouple-then-neutralize sub-slice (the engine defines its own cookie-name constant first, then Stele neutralizes its own). The brand-leak is harmless to clone-and-run adopters — the reference host names its own cookie, so an adopter never inherits Stele's constant.
  2. The pending store became Stele-internal (§0.4 confirmed at live). PendingAddPasskeyStore carried no host binding (a plain in-process dict, person_id-only), so the store slot was removed with the two ceremony slots. The router reaches a module-level pending_add_passkey_store singleton directly; the ceremony functions accept an optional store kwarg (defaulting to the singleton) so the engine kept passing its own store across the re-point.
  3. Login-TOTP composed host-side (§0.5). Stele exposes TOTP rotation but no standalone TOTP-verify primitive; the reference host composes login-TOTP itself (kek_decrypt the secret, then pyotp verify), consistent with B-first. Stele's surface did not grow.
  4. Inspect-first found a third store importer the §0.3 caller-check had not. The caller-check covered add_passkey_ (only me_security); the store* (PendingAddPasskeyStore) was also imported by deps.py and app.py (app-state seeding). The pre-cut grep surfaced app.py before the lift broke it — the lens-defines-the-blind-spot lesson, caught.
  5. The reference host names its own cookie. config.py sets stele_ref_session — §1.6 made concrete: delivery, including the cookie name, is the host's (issue_session is mint-only).

5. Verification

6. End-state

| Repo | origin/main | State | |---|---|---| | DUNIN7/stele (private) | 8246729 | §1 lift + §2–§4 examples — pushed | | loomworks-engine | 32c1a68 | re-pointed onto stele.webauthn; signup shrank; mount test updated — pushed | | loomworks-record | (this record + §5 docs + manifest v0.68 + CR v0.3) | staged; HALT before push |

7. Asymmetry dissolution + carry-forwards

The CR-2026-115 §4 asymmetry is dissolved. P7-2 §4 named a deliberate asymmetry — Stele shipped passkey-enrollment routes whose enrollment logic lived host-side — recorded as a correction-in-waiting against a feared persons/signup.py split. The P7-3 Step 0 inspection inverted the fear (a clean extraction, not a split: two trivial one-directional shared symbols, a thin wrapper over Stele's own primitives, only me_security calling it). This CR's §1 lift dissolved the asymmetry; Stele now owns the enrollment logic as well as the routes. The canonical record of this is the CR-2026-116 §9 forward-note (pointing back to CR-115 §4) — per the Operator's settled decision (2026-06-19), a forward-note in this CR, not a CR-115 v0.3.

Parked threads (found during P7-3, named so they are walkable, not dropped):

  1. §1.6 cookie-constant neutralization — deferred. The brand-leaked stele.session.COOKIE_NAME "loomworks_session" is consumed by the engine as its live production session cookie at ~10 sites across 4 files (login.py / signup.py / me_reactivate.py / deps.py). An in-place rename would change the engine's cookie on deploy and mass-logout every live session. Removing the leak needs a sequenced engine-decouple sub-slice (option B: the engine defines its own cookie-name constant + re-points its sites first, then Stele renames its constant). Out of P7-3 scope. Harmless to adopters — the reference host names its own cookie, so a clone-and-run adopter never inherits Stele's constant.
  2. Engine login.py:5 stale comment. The module-level comment reads "POST /auth/login/begin — challenge + allowCredentials (email hint)", describing behavior the code does not implement: the login is discoverable-only and passes no email (begin_login receives none; the docstring at login.py:114–117 is the accurate account, citing seed v0_12). Engine doc-hygiene only; surfaced by the P7-3 seed-alignment inspection. Out of P7-3 scope.

Other carry-forwards:

  1. The published-package path — replace the engine's ../stele editable path dep with a version pin against a published stele. (The lift added an engine→Stele import-path coupling; the dep mechanism is unchanged.)
  2. CR-2026-102 SCOPE_TOTP landed-state thread — orthogonal, walkable.
  3. A Stele TOTP-verify primitive — held out (§0.5 resolved to host-composition); a verify_totp_code primitive remains a walkable later slice if ever wanted.

8. Methodology lessons banked (for the next consolidation)

  1. A grep-first guard written into the CR earned its keep. §1.6 said "confirm by grep, not assumption" precisely because a missed cookie reader should break loudly. The grep found the CR's own premise false (the engine consumes the constant as its live cookie name) before any code changed — the guard caught a mass-logout regression at the design seam, not in production. When a CR names a "trivial two-site touch," verify the site count before believing it.
  2. The deciding fact of a fork can invert the fear. The whole lift-vs-wire fork turned on how tangled persons/signup.py was; the inspection found a clean extraction where the CR feared a split. Grounding the fork on real code (not the remembered shape) is what made the lift cheap.
  3. A caller-check is narrower than an importer-check. The §0.3 caller-check covered the ceremony functions (only me_security); the store had two more importers (deps.py, app.py). Inspect-first on the actual symbols to be moved — not just the headline functions — caught app.py before the delete broke it.
  4. Verify the composition end-to-end, even when you can't drive the real device. Patching only verify_registration/verify_authentication (the two library-boundary calls the reference app cannot fake) and running everything else real — against a real Postgres — proved the whole signup→login→enrollment composition, including the totp_verified gate, without a physical authenticator. The narrowest possible mock keeps the most of the system real.
  5. A deferred step is a recorded thread, not a silent gap. §1.6 was deferred with its cause, its blast radius, and the safe sequence to do it later — so the brand-leak is parked walkable, not dropped.

DUNIN7 — Done In Seven LLC — Miami, Florida Loomworks — Stele Phase 7 P7-3 (Ceremony Lift + Reference App + Browser Ceremony + Config Generator + Docs) Completion Record — v0.1 — 2026-06-19