Version. 0.3
Date. 2026-06-19
Author. Marvin Percival (DUNIN7 Operator) with Claude.ai (CR drafting) on DUNIN7-M4; CC executing on DUNIN7-M4.
Status. Change request, as-built / landed. The final Phase 7 slice — the clone-and-run-and-use bar — is built, verified, and (pending the records push) closed. Phase 7 is closed. Five deliverables shipped; one sub-step (§1.6 cookie-constant neutralization) deferred to a named parked thread on a Step-1 finding. This version records what shipped and carries the §9 asymmetry-dissolution forward-note (the canonical home for it, per the settled decision). Markdown primary (technical consumer); the §5 docs deliverable shipped HTML primary + Markdown source.
Governing scoping note. loomworks-stele-phase-7-p7-3-scoping-note-v0_1.md.
Governing handoff. loomworks-stele-phase-7-p7-3-cr-drafting-handoff-v0_2.md.
Grounding inspection. loomworks-stele-phase-7-p7-3-step-0-inspection-report-v0_1.md (record 45ac2f1).
Step 0 grounding note. CC, read-only, loomworks-record at 74e16e6 (premise held, no drift, no HALT).
Completion record. phases/stele-extraction-phase-7/loomworks-stele-phase-7-p7-3-completion-record-v0_1.md.
v0.3 change. Records the as-built outcome. §1.6 (cookie neutralization) deferred — a Step-1 grep-first finding inverted the CR's "two-site touch" premise: the engine consumes stele.session.COOKIE_NAME as its live production cookie (10 sites, 4 files), so an in-place rename mass-logs-out every session; removing the leak needs a sequenced engine-decouple sub-slice (out of P7-3 scope). The lift shipped without it. Adds the §9 asymmetry-dissolution forward-note as canonical (the completion record + manifest forward-reference this version for it). Records the seed-alignment inspection (discoverable-credential / "remember ID" non-feature) folded into the §5 docs. Names two parked threads (§1.6 cookie decouple; the login.py:5 stale comment). Supersedes v0.2.
CR number. CR-2026-116, confirmed. v0.2 filed at ed0b1d8; this v0.3 supersedes it.
End-state HEADs (landed). DUNIN7/stele 8246729; loomworks-engine 32c1a68; loomworks-record (this CR v0.3 + completion record + manifest v0.68 + §5 docs, pushed together).
What this CR does. Takes Stele from "a stranger can mount the router" (P7-2) to "a stranger can clone a reference host, run it, and use Stele's identity primitives end-to-end — including registering a real passkey in a browser" (P7-3). This closes Phase 7.
Five deliverables, sequenced so the lift lands first (everything downstream shrinks once it does):
add_passkey_begin / add_passkey_complete block out of loomworks-engine/persons/signup.py and into stele.webauthn, where it belongs (it already calls Stele's WebAuthn primitives). This dissolves the P7-2 asymmetry — Stele now owns the passkey-enrollment logic, not just the routes. The injection slots for the ceremony disappear; the engine re-points its import. The inspection proved this is a clean extraction, not a messy split — two trivial one-directional shared symbols, no entangled control flow, signup flow left fully intact.stele.router, runs Stele's 3-table migration baseline against a real Postgres, wires the post-lift slot set, and demonstrates both delivery shapes (cookie for browser, bearer for agent) plus enrollment and login.navigator.credentials.create() / .get() ceremony against the mounted router. This is the "clone, run, and use it with a real authenticator" proof the engine's synthetic-payload mount-verify could not show..env template: secret pre-generated, RP-ID / origin / RP-name / database URL to fill. No code generation; can't drift.What changes for the Operator. After this CR, Phase 7 is closed. Stele is a self-contained, mountable identity plugin with a canonical reference host a stranger can clone and run. The published-package path and the CR-2026-102 SCOPE_TOTP thread remain parked, walkable — not Phase 7.
The asymmetry-dissolution recording. CR-2026-115 §4 named the add_passkey injection-slot asymmetry as a correction-in-waiting. This CR's lift dissolves it. Per the Operator's settled decision (2026-06-19), the dissolution is recorded as a forward-note in this CR (§9) pointing back to CR-2026-115 §4 — not a 115 v0.3. CR-115 stays true to its own close; the dissolution belongs to P7-3's record, trajectory walkable.
The cookie brand-leak: found deeper than scoped, deferred (correction named). v0.2 scoped a small §1.6 step — neutralize stele/session.py's COOKIE_NAME = "loomworks_session" (an engine-branded constant lifted into Stele at P7-1), estimated as a "two-site touch" (the constant + the P7-2 mount test). The Step-1 grep-first instruction — written to make a missed reader break loudly — did exactly that. It exposed that the engine consumes stele.session.COOKIE_NAME as its live production session-cookie name: 4 set-sites (login.py, signup.py, me_reactivate.py) + 6 read-sites (deps.py), across 4 files. Renaming in place would change the engine's production cookie on deploy → every live browser still sends loomworks_session, the engine reads stele_session, no token resolves → mass logout. Not a two-site touch — a ~10-site engine production-behavior change. So §1.6 is deferred (Operator decision, option A): the lift shipped without it. The brand-leak persists but is harmless to clone-and-run adopters — the reference app names its own cookie (stele_ref_session), so an adopter never inherits Stele's default. Removing the leak properly needs a sequenced engine-decouple sub-slice (engine defines its own COOKIE_NAME and re-points its ~10 sites first, keeping the value loomworks_session; then Stele renames) — a named parked thread (§6), not P7-3.
74e16e6; Operator-confirmed 2026-06-19)The Step 0 grounding pass ran read-only (no edits, commits, migrations, or build). The lift's clean-extraction premise held at live — no drift, no HALT. All six surfaced decisions are resolved and Operator-confirmed. This section records them as settled; the deliverables below are written against these resolutions. Build re-confirms HEADs and the suite floor at start, but does not re-open these.
§0.1 — HEADs. DUNIN7/stele 1077803 ✓; loomworks-engine 08e89c5 ✓; loomworks-record 74e16e6 (ahead of cited 45ac2f1 by two docs-only commits — the scoping note + handoff v0.2; confirmed ancestor, no code-fact drift).
§0.2 — CR number. Highest filed cr-2026-NNN = 115. This CR is 116 — no renumber, no rename. Draft staged in ~/Downloads/; files to change-requests/ at build-go.
§0.3 — Lift clean-extraction facts: HELD at live. Against persons/signup.py (450 lines): the add-passkey block is still co-located at the file bottom (_PendingAddPasskey L350, AddPasskeyBeginResult L359, PendingAddPasskeyStore L364, add_passkey_begin L388, add_passkey_complete L422); the two shared symbols are still one-directional (PENDING_TTL def L46, PendingRegistrationNotFound def L54 — both flow signup-flow → ceremony; the signup-flow half references zero add-passkey symbols, every add-passkey hit being at L≥350); the wrappers carry no host concepts (the file's host concepts — onboard import L35, host_account INSERT L284–319 — are all in the signup-flow half's complete_totp_verify, none in the ceremony); callers are still only me_security.py (import L49–50; calls L178, L230). The lift rests on a true premise.
§0.4 — Store-slot fate: STELE-INTERNAL (confirmed). PendingAddPasskeyStore is a plain in-process dict[str, _PendingAddPasskey] — no host config, no host lifecycle, no host-owned state. _PendingAddPasskey carries only person_id (= principal id) + challenge/user_handle/options_json/expires_at — identity-only, Stele-shaped. stele.api already defines the AddPasskeyStore Protocol (L65). No host binding → the store becomes Stele-internal; provide_add_passkey_store disappears with the two ceremony slots. Post-lift mandatory slot set = provide_db_session + provide_webauthn_config (two).
§0.5 — Login-TOTP path: HOST-COMPOSITION (confirmed). stele.person_totp exposes only begin_totp_rotation (L48) + confirm_totp_rotation (L73) — no standalone TOTP-verify primitive anywhere in stele/. The reference app composes login-TOTP itself (kek_decrypt(totp_secret) → pyotp.TOTP(secret).verify(code)), consistent with B-first; Stele's primitive surface does not grow in this CR. (The alternative — a verify_totp_code Stele primitive — remains a walkable later slice if the Operator ever wants it; it is not in P7-3.)
§0.6 — Shared-symbol handling: confirmed. stele.api carries PasskeyEnrollmentNotFound (L84) + PasskeyEnrollmentError (L88) and the AddPasskeyPending / AddPasskeyStore / AddPasskeyBeginResult Protocols (L58/65/71). The lift raises Stele's PasskeyEnrollmentNotFound in place of PendingRegistrationNotFound and brings a Stele-native TTL constant. The signup flow keeps its own PendingRegistrationNotFound (a SignupError subclass) — unaffected.
§0.7 — WebauthnConfig field set + env floor: confirmed exact. WebauthnConfig (frozen) = exactly rp_id, rp_name, rp_origin (three). The generator's env floor: STELE_SECRET_KEY (Fernet, fresh), STELE_RP_ID, STELE_RP_NAME, STELE_RP_ORIGIN, STELE_DATABASE_URL (postgresql+asyncpg://…), optional STELE_SECRET_KEYS_PREVIOUS. Field names match the live EnvKeyEncryptionKeyProvider / WebauthnConfig reads.
§0.8 — Reference-app home: stele/examples/ (confirmed; Operator filing call made). stele top level is alembic.ini, migrations/, pyproject.toml, README.md, src/, tests/; examples/ does not yet exist (the build creates it). stele/examples/ is structurally clean (no collision, sits beside src/) and travels with the package a stranger clones. The reference app (§2), browser front (§3), and config generator (§4) all land under stele/examples/.
§0.9 — Cookie disposition: NEUTRALIZE scoped at Step 0, then DEFERRED at Step 1 on a grep finding (the full correction trajectory). This decision moved twice; both moves are preserved.
loomworks_session is not Stele's to rename."stele/session.py ships COOKIE_NAME = "loomworks_session", an engine-branded constant lifted into Stele at P7-1 and exported. A standalone identity package shouldn't ship a host's brand as a default; rename loomworks_session → stele_session, estimated a two-site touch (constant + the P7-2 mount test that imports it). v0.2 folded this into the lift slice as §1.6, with a grep-first / rename-to-find guard ("the engine has its own cookie mechanics and does not read Stele's constant, but confirm by grep, not assumption").stele.session.COOKIE_NAME as its live production session-cookie name — 4 set-sites (login.py:203,222, signup.py:250, me_reactivate.py:155) + 6 read-sites (deps.py:187,483,572,647,903,1116), 4 files importing it. An in-place rename changes the engine's production cookie → mass logout. The "two-site touch" estimate was wrong by ~10 sites and a production-behavior change.loomworks_session appears only in comments (UserMenu.tsx, signout.ts); the actual clear is server-side Set-Cookie, so the decouple's blast radius is engine-only.Suite floor (to confirm at build start, not re-decided): P7-2's documented floor was 2930/0/46. CC confirms the live floor at §1's start before cutting.
Goal. add_passkey_begin / add_passkey_complete (+ their private helpers + the _PendingAddPasskey / AddPasskeyBeginResult / PendingAddPasskeyStore types) move from loomworks-engine/persons/signup.py into stele.webauthn (or the natural stele home Step 0 confirms). The injection slots for the ceremony disappear; the engine re-points its import. The asymmetry dissolves.
§1.1 — extract into stele.webauthn.
begin_registration / verify_registration (its siblings — the inspection confirmed the natural landing site).person_id is the principal id) — no host concepts. The stele.api AddPasskeyStore / AddPasskeyBeginResult Protocols become concrete in stele.webauthn.PasskeyEnrollmentNotFound in place of PendingRegistrationNotFound; bring/read a Stele-native TTL constant.stele first (P7-1/P7-2 two-repo discipline). The ceremony resolves standalone against Stele's own primitives.§1.2 — the injection slots disappear (all three).
stele.router's provide_add_passkey_begin / provide_add_passkey_complete slots are removed; the router calls the now-internal stele.webauthn ceremony directly.provide_add_passkey_store slot is also removed — the store becomes Stele-internal (§0.4 confirmed: plain in-process dict, person_id-only, no host binding, Protocol already in stele.api). The ceremony owns its pending store.provide_db_session + provide_webauthn_config. Update the router's slot inventory and the mount-contract docstring (stele/__init__.py) to match — and the SDK's public surface if it re-exported the now-removed slots.§1.3 — the engine re-points.
me_security.py (the only caller, §0.3-confirmed) imports the ceremony from stele.webauthn instead of persons.signup, and drops the now-removed injected callables. The engine does not mount stele.router in production (held-out scope), so me_security keeps calling the ceremony — now imported from Stele, not host-local.
§1.4 — persons/signup.py shrinks.
begin_signup / complete_passkey / complete_totp_verify + their store/types).playground_dev, not the suite alone.§1.6 — brand-leaked cookie constant: DEFERRED on a Step-1 grep finding (not executed).
stele/session.py COOKIE_NAME = "loomworks_session" → "stele_session", estimated a two-site touch, with a grep-first / rename-to-find guard before cutting.stele.session.COOKIE_NAME as its live production cookie — 10 sites across 4 files (set: login.py:203,222, signup.py:250, me_reactivate.py:155; read: deps.py:187,483,572,647,903,1116). An in-place rename → engine reads stele_session while live browsers send loomworks_session → mass logout. The guard caught the false premise before any cut.COOKIE_NAME stays "loomworks_session". The lift shipped without it. Named as a parked thread (§6): the proper removal is an engine-decouple sub-slice (engine owns its COOKIE_NAME first, value unchanged; then Stele renames), out of P7-3 scope. Harmless to adopters — the reference app names its own cookie (stele_ref_session, §2.4).§1.7 — verification.
stele suite green (the lifted ceremony resolves standalone).tests/test_stele_router_mount.py) — confirm it still passes with the ceremony now Stele-internal; update it if it wired the now-removed ceremony slots (it did, per the P7-2 record §5 — it injected persons.signup.add_passkey_* into the ceremony slots). The updated test mounts the router and confirms the now-internal ceremony resolves end-to-end without the host supplying it. This is a real regression-guard update, not a deletion.playground_dev (a real passkey-begin round-trips through the Stele-internal ceremony).COOKIE_NAME stays "loomworks_session".)Per-step commits; suite green at each; HALT before push; explicit-path staging.
A minimal standalone FastAPI app that mounts stele.router and demonstrates real end-to-end use. Lives at stele/examples/ (§0.8).
§2.1 — framework. A small FastAPI app — not the engine, not Next.js. stele.router is a plain APIRouter; the app include_routers it under a host-chosen prefix (e.g. /me/security).
§2.2 — DB: real Postgres, via the Stele baseline. SQLite is not viable — the Stele models use JSONB, LargeBinary, PG_UUID. The app:
STELE_DATABASE_URL = postgresql+asyncpg://…).0001_baseline (the 3-table P7-1 migration) to stand up its DB.docker-compose (or equivalent) for the throwaway Postgres so the stranger's "clone and run" is one command.
§2.3 — wire the post-lift slot set. After §1, the mandatory slots are exactly two — provide_db_session + provide_webauthn_config (the store + both ceremony slots are gone, §1.2). The app supplies minimal real providers:
provide_db_session — a request-scoped AsyncSession yielder over an async engine (a few lines).provide_webauthn_config — return WebauthnConfig(rp_id, rp_name, rp_origin) from env (one line).resolve_current_principal, extract_token, provide_secret_key, provide_person_email) except where a demonstration overrides them (§2.4, §2.5).
§2.4 — demonstrate both delivery shapes. Cookie (browser) and bearer (agent) over the same Stele-minted token, by overriding extract_token:
extract_token reads request.cookies[...]) for the browser front, and the bearer path for an agent caller.issue_session is mint-only — the app writes the cookie from the returned token (cookie shape) or returns the token (bearer shape). The cookie name is the app's choice (per §0.9).§2.5 — demonstrate enrollment AND login. Not just enrollment:
stele.webauthn.begin_authentication + verify_authentication, stele.credentials.get_credential_by_credential_id + update_sign_count;stele.recovery.verify_and_consume_recovery_code, and TOTP per §0.5 (host-composition default: kek_decrypt(totp_secret) → pyotp.TOTP(secret).verify(code));stele.session.issue_session, with the partial-session shape (assertion-then-2FA) a host concern.§2.6 — seed-alignment (mandatory; as-built confirmed by a dedicated inspection). The login composition honors the seed's authentication framework:
stele.webauthn.begin_authentication(allow_credentials=None) defaults to the discoverable / resident-key flow (the browser shows every passkey bound to the RP; the user picks; no identifier typed before the assertion — a credential list is an optional narrowing, never required); the engine's login.py is explicitly discoverable-only and cites seed v0.12 ("identity is resolved at /complete by the selected credential, never by email"); and the reference app as built has zero identifier field — /auth/login/begin takes no arguments, app.js sends no body, and the only <input>s are a signup display-name and second-factor code/recovery fields (none of which key lookup). Consequence for the §5 docs: an email/username field or a "remember ID" / "remember me" control at sign-in is a non-feature — the authenticator holds identity (discoverable flow), so an adopter must not reintroduce identity-by-attribute by habit. This is folded into the §5 docs' "No email as identity" section.§2.7 — verification. The app runs; a request round-trips through the mounted router against the real Postgres; enrollment and login both work end-to-end (the browser front in §3 drives the real ceremony).
A minimal HTML/JS front that runs the real navigator.credentials ceremony against the mounted router.
§3.1 — what it does.
fetch /passkeys/begin → navigator.credentials.create(options) → fetch /passkeys/complete.fetch login-begin → navigator.credentials.get(options) → fetch login-verify (the reference app's composed login routes from §2.5).
§3.2 — the RP binding. rp_id / rp_origin (from the webauthn config) must match the serving origin or the browser refuses the ceremony. The reference app's config and the served origin must agree — document this in §5 (it's the first thing a stranger gets wrong).
§3.3 — the proof. A user opens the reference app in a browser, registers a real passkey with a real authenticator, and it round-trips through stele.router end-to-end. This is the proof the engine's synthetic-payload mount-verify (P7-2) could not show — "clone and run and use it."
§3.4 — minimum, not a product. Smallest static HTML/JS that demonstrates the ceremony. Not a styled UI; a working proof. No framework.
A small helper that emits a .env template. No code generation — env-scaffolding-only suffices because the post-lift mount is two short providers + ~5 env values (Fork 2's resolution).
§4.1 — what it emits. A .env template with:
STELE_SECRET_KEY — freshly generated (a real Fernet key, so the stranger isn't tempted to ship a placeholder).STELE_RP_ID, STELE_RP_NAME, STELE_RP_ORIGIN — RP fields to fill (with inline comments naming what each is and the origin-match requirement from §3.2).STELE_DATABASE_URL — postgresql+asyncpg://… template to fill.STELE_SECRET_KEYS_PREVIOUS — optional, commented, for rotation.WebauthnConfig read.§4.2 — can't drift. It emits no code, so it can't fall out of sync with the package surface. The adopter copies the file, fills the site-specific fields, runs.
§4.3 — form. A small script (generate_env.py) under stele/examples/. The fresh-secret generation is the one piece of real logic (call the same Fernet-key generation Stele uses internally — confirm the exact call against the live stele KEK module at build, so the generated key is valid for STELE_SECRET_KEY).
The adopter-facing documentation — HTML primary + Markdown source (operator/adopter-facing, per convention). The reference-app code + its README stay Markdown/technical.
§5.1 — content.
resolve_current_principal. State it plainly; it's the conceptual spine.extract_token defaults to bearer; how a host writes the cookie override; issue_session is mint-only..env, start the app, open the browser front, register a passkey. The walkthrough is the reference app embodied; the docs narrate what the code does.
§5.2 — plain-English standard. What each slot does, what the host writes, in plain terms. Examples where an example makes the wiring concrete (the provide_db_session yielder; the extract_token cookie override). The walkthrough is copy-runnable.
§5.3 — home. loomworks-record (the docs are canonical adopter documentation), with the reference-app code at stele/examples/.
../stele editable dep stays; retiring it for a version pin is a parked thread. (Note: the lift adds an engine→Stele import-path coupling, but the dep mechanism is unchanged — still the editable path dep.)stele.router — the engine keeps its hand-rolled routers; P7-3 does not mount stele.router in the engine's production surface. me_security keeps calling the ceremony (now imported from Stele).verify_totp_code primitive remains a walkable later slice the Operator may take — not P7-3.stele.session.COOKIE_NAME brand-leak removal — parked thread (was scoped as §1.6 neutralize; deferred on the Step-1 finding that the engine consumes the constant as its live cookie, 10 sites/4 files). Proper removal is a sequenced engine-decouple sub-slice: the engine defines its own COOKIE_NAME (value unchanged at loomworks_session) and re-points its ~10 sites first, so the engine cookie name never changes (no logout); then Stele renames its constant to a brand-neutral default (or drops it — a standalone substrate arguably shouldn't ship a cookie name at all, since delivery is the host's). Out of P7-3 scope. Harmless meanwhile — adopters name their own cookie.login.py:5 stale comment — parked thread (engine doc-hygiene). The module-level comment reads "allowCredentials (email hint)", describing behavior that is not implemented — the code is discoverable-only and passes no email. A stale comment that implies identity-by-attribute is exactly the kind a future reader could act on; flagged for correction, out of P7-3 scope.74e16e6, all six decisions confirmed). Build re-confirms HEADs and the suite floor at start; it does not re-open the decisions.stele/examples/). The app wires the post-lift slot set; the front drives the real ceremony; the generator scaffolds the env.
Each slice: inspect-first on every seam; per-step commits, suite green at each; HALT before push, CC confirms staged state; explicit-path staging (git add [explicit path], never git add -A); shell commands on separate lines, no inline comments.
Seven commits across two repos, plus the records.
DUNIN7/stele (1077803 → 8246729, 5 commits):
| Commit | Step | What |
|---|---|---|
| ef69b88 | §1.1 | Lift the add-passkey ceremony (+ store, types, enrollment exceptions, Stele-native TTL) into stele.webauthn |
| cfb779f | §1.2 | Router calls the internal ceremony; the 3 ceremony/store slots + their Protocols removed; SDK surface + mount docstring updated |
| df0e4c5 | §2 | Reference host under examples/ — mounts stele.router, composes signup + login host-side |
| 402f8b0 | §3 | Browser WebAuthn ceremony front (index.html + app.js) |
| 8246729 | §4 | Config generator (generate_env.py, env-scaffolding only) |
loomworks-engine (08e89c5 → 32c1a68, 2 commits):
| Commit | Step | What |
|---|---|---|
| 01d4aa6 | §1.3 + §1.7 | Re-point me_security + deps + app onto stele.webauthn; catch Stele's PasskeyEnrollment*; update the P7-2 mount test |
| 32c1a68 | §1.4 | Delete the now-dead 109-line add-passkey block from persons/signup.py |
§1.6 (cookie neutralize): not executed — deferred (the §0.9 / §1.6 / §6 finding).
Verification (as-built).
stele: import stele stays FastAPI-free; removed slots gone; router still mounts 8 routes; post-lift mandatory slots = exactly 2 (provide_db_session, provide_webauthn_config); internal ceremony resolves standalone.playground_dev: live passkey-begin round-trip through the Stele-internal ceremony; excludeCredentials correctly reflects the principal's existing passkeys; read-only, no writes.app.js passes node --check; index.html + /static/app.js serve 200. The live passkey-with-real-authenticator step is the human-in-browser proof the README walks..env carries the full field set; the fresh STELE_SECRET_KEY is a working Stele KEK (session encode/decode round-trips under it); regenerated each run.login.py, reference app); zero identifier field; "remember ID" is a non-feature (§2.6).
As-built decisions embodied: 2-slot mount (provide_db_session + provide_webauthn_config); ceremony Stele-internal; no email as identity (signup collects a display name only; login keys on credentials); login-TOTP composed host-side (kek_decrypt + pyotp, Stele's surface unchanged); the reference app names its own cookie (stele_ref_session) — making the §1.6 deferral safe by construction.
P7-3 is done. Confirmed:
stele.webauthn; the three ceremony/store injection slots are gone; the engine re-pointed and its suite is green (2930/0/46); persons/signup.py carries only the intact signup flow (109-line block deleted).stele/examples/, run docker-compose for Postgres, run the baseline migration, fill a generated .env, start the app, open it in a browser, and register a real passkey with a real authenticator that round-trips through stele.router — and log in with it (discoverable flow; no identifier typed).loomworks-record (standards/loomworks-stele-mount-contract-v0_1.{md,html}).Scoped-but-deferred: §1.6 cookie-constant neutralization (the engine-dependency finding) — a named parked thread (§6), not a done-bar item. The clone-and-run bar is unaffected: adopters never inherit Stele's cookie name.
This is the settled recording shape (Operator decision, 2026-06-19): a forward-note here, not a CR-2026-115 v0.3.
CR-2026-115 §4 ("The add_passkey ceremony: injection slot, not lift") named a deliberate asymmetry: P7-2's mountable router shipped passkey-enrollment routes whose enrollment logic lived host-side (in persons/signup.py), because lifting the ceremony was feared to reopen a parked persons/signup.py split. CR-115 §4 recorded this as a correction-in-waiting — to be dissolved by a later slice when cheap.
The P7-3 Step 0 inspection (45ac2f1) inverted the fear. The lift is a clean extraction, not a split: the add-passkey block shares exactly two trivial one-directional symbols with the signup flow, is a thin wrapper over Stele's own WebAuthn primitives, carries no host concepts, and has only me_security as a caller. Removing it leaves the signup flow fully intact.
This CR (§1) dissolves the asymmetry. The ceremony is lifted into stele.webauthn; Stele now owns the passkey-enrollment logic as well as the routes; the ceremony injection slots disappear. The asymmetry CR-115 §4 named is gone.
Trajectory preserved. CR-115 §4's position (injection-slot, asymmetry-accepted-to-avoid-a-split) was correct at 115's close, on the information then available (the split was assumed to exist). The inspection corrected the premise; this CR corrects the asymmetry. Both records stay true to their own moments — CR-115 records the asymmetry as it stood; this CR records its dissolution, with this note as the pointer back. The correction is named, not smoothed.
1077803→8246729, engine 08e89c5→32c1a68) in §7.5; §8 done-bar met; Phase 7 closed. §1.6 cookie neutralization deferred — the load-bearing correction of the build phase: v0.2 scoped an in-place rename as a "two-site touch"; the Step-1 grep-first/rename-to-find guard exposed that the engine consumes stele.session.COOKIE_NAME as its live production cookie (10 sites, 4 files), so the rename would mass-logout every session. Deferred (option A) to a named parked thread (§6); the discipline turned a would-be production incident into a grep-time HALT before any cut. Adds the seed-alignment inspection result — discoverable-credential sign-in confirmed at all three layers, "remember ID" / email-username field a non-feature, folded into §5 docs (§2.6). Adds the engine login.py:5 stale-comment parked thread (§6). Carries the §9 asymmetry-dissolution forward-note as canonical (the completion record + manifest v0.68 forward-reference this version). Supersedes v0.2 (ed0b1d8).74e16e6); all six surfaced decisions Operator-confirmed and folded in. Store-slot → Stele-internal (slots collapse to two: db session + webauthn config). Login-TOTP → host-composition (Stele's surface does not grow). Reference-app home → hardcoded stele/examples/. Shared-symbol + env-floor → confirmed exact. Cookie disposition → neutralize (stele.session.COOKIE_NAME loomworks_session → stele_session, §1.6) — a v0.1 correction: v0.1 §0.9 called the cookie carry-forward moot; Step 0 found a real brand-leaked Stele constant; v0.2 dissolves it. [Note: v0.3 records that the dissolution was then deferred at build time on the engine-dependency finding — the trajectory is moot → neutralize → defer, each step preserved.] §0 rewritten from open-decision surface to settled-resolutions record. Ready to build. Supersedes v0.1.DUNIN7 — Done In Seven LLC — Miami, Florida CR-2026-116 — Stele Phase 7 P7-3 — Ceremony Lift + Reference App + Browser Ceremony + Config Generator + Docs — v0.3 — 2026-06-19