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

Loomworks — Stele Phase 7 P7-2 (Mountable Router + SDK Surface) Completion Record — v0.1

Version. 0.1 Date. 2026-06-19 Author. Marvin Percival (DUNIN7 Operator) with Claude.ai (CR drafting) on DUNIN7-M4; CC executing on DUNIN7-M4. Status. Completion record. CR-2026-115 (Stele Phase 7 P7-2 — Mountable Router + SDK Surface) is built, verified, and pushed across both repos. stele carries the mountable router, the SDK surface, and the env-var rename; loomworks-engine carries the committed mount-verify test. Markdown primary (technical consumer).

Governing CR. change-requests/cr-2026-115-stele-phase-7-p7-2-mountable-router-sdk-v0_1.md (record e6c757f); amended to v0.2 alongside this record (the five as-built resolutions).

Grounding line at execution. stele main = 0d5e265, engine main = ff0c4cf (suite 2928/0/46) at Step 0 — both confirmed at start-state, no drift.


Plain-language summary

What P7-2 did. Gave the standalone stele package an HTTP surface of its own — a mountable FastAPI APIRouter carrying the eight credential-backed auth-primitive routes (passkey enrollment, recovery codes, TOTP rotation), plus the SDK surface a host imports to mount it. A downstream app can now include_router(stele.router) and supply a small named set of dependencies via FastAPI's app.dependency_overrides to get the identity/credential/recovery/TOTP primitives without re-implementing the engine's hand-rolled wiring. The env-var the package's KEK fallback reads was renamed from LOOMWORKS_SECRET_KEY to STELE_SECRET_KEY — strictly inside the package, with zero engine deployment change.

What it did NOT do (held to scope): no reference app, docs, or config generator (P7-3); no WebAuthn-ceremony-gap close (P7-3); no loomworks_session cookie rename (P7-3); no add_passkey ceremony lift (it stayed an injection slot); no persons/signup.py split; no published-package path (the ../stele editable dep stays); no schema change; no production mount of stele.router in the engine; no auth-surface migration (the engine keeps its own hand-rolled routers).

The verification done-bar held. A passkey-enrollment route resolves end-to-end through the engine's real dependency providers wired into the router's slots — proving the contract is satisfiable by a real host, not just by stubs. The proof is a committed engine test, so it stands as a regression guard.


1. What was built

DUNIN7/stele (private; main = 1077803, pushed) — three slices on 0d5e265:

loomworks-engine (main = 08e89c5, pushed) — one slice on ff0c4cf:

2. The build

stele (3 commits, DUNIN7/stele main 1077803):

| # | Commit | Slice | What | |---|---|---|---| | 1 | 22b9fc3 | §1 router | src/stele/api.py — 8-route mountable APIRouter + injection slots + default resolver; fastapi==0.135.3 pinned | | 2 | 005e11e | §3 SDK | src/stele/__init__.py — 65 primitives eager (FastAPI-free) + lazy stele.router/slots + mount-contract docstring + stated authorization boundary | | 3 | 1077803 | §2 rename | src/stele/kek.py — no-arg fallback LOOMWORKS_SECRET_KEYSTELE_SECRET_KEY; injectable path untouched |

engine (1 commit, loomworks-engine main 08e89c5, pushed):

| # | Commit | What | |---|---|---| | 1 | 08e89c5 | tests/test_stele_router_mount.py — mount-verify test; production app + auth routers untouched |

Suite green at every step: 2928/0/46 after the engine consumed the modified stele (the three slices don't disturb the engine), then 2930/0/46 with the two new mount tests.

3. The injection contract, as built

The mountable-router contract is the set of FastAPI dependency slots the host fills via app.dependency_overrides. Override keys are the exact slot objects (identity-verified standalone and again with the engine's real providers).

| Slot | Default | Who supplies | |---|---|---| | resolve_current_principal | Stele's resolver over neutral resolve_session | host overrides for its policy gate (totp_verified) | | extract_token | neutral Authorization: Bearer | host overrides for cookie delivery | | provide_secret_key | Stele's KEK env (STELE_SECRET_KEY) | host injects its own secret | | provide_person_email | None (optional) | host overrides (passkeys/begin only) | | provide_db_session | none — raises until overridden | host must supply | | provide_webauthn_config | none — raises | host must supply | | provide_add_passkey_store | none — raises | host must supply | | provide_add_passkey_begin / _complete | none — raises | host supplies the §4 ceremony |

Contract difficulty (the honest "stranger mounts it" estimate). Three slots ship working defaults a host can leave alone (resolve_current_principal, extract_token, provide_person_email) and one ships a Stele-env default (provide_secret_key). Five slots a real host must supply: the DB session, the WebAuthn config, the pending-passkey store, and the two ceremony functions. The mandatory slots fail loud (503/500) when unwired, never silent — so a partial mount is caught at first request, not in production. P7-3's reference app demonstrates the full wiring; this record establishes the contract is satisfiable by the engine's real providers (§5).

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

4a — Mount shape: module-level stele.router + dependency_overrides, not a get_router(...) factory. The CR left the mount shape as a build-time choice ("whichever fits FastAPI dependency-override cleanly"). Module-level won — it is the idiomatic FastAPI override path, and slot-key identity holds both standalone and with the engine's real providers.

4b — Narrow person_email injection taken; the CurrentUser/host_account fallback was not needed. The CR flagged this as a §1 finding: inject just person_email (which add_passkey_begin needs) rather than the whole CurrentUser/host_account structure. It threaded cleanly, so the narrow path was taken — Stele depends on no host HostAccount view shape. Only passkeys/begin uses it; the other seven routes are Principal-only. This is cleaner than the CR's stated fallback and is the as-built contract.

4c — /auth/dev/issue-session omitted from the mountable router (Operator decision). Step 0 confirmed the route is dev/test-guarded host-side (api/app.py mounts auth_dev only when loomworks_env ∈ {development, test}). Rather than drag the host's env-guard semantics into a mountable package, the route was omitted. Session issuance stays an SDK-exposed primitive (stele.session.issue_session); the dev session-mint route is a reference/testing affordance for P7-3 to demonstrate, not package-shipped. The mountable router is the eight credential-backed routes (down from the CR's candidate nine).

4d — extract_token defaults to Authorization: Bearer, host-overridable for cookie; issue_session stays mint-only (the router sets no cookie). This is the session-delivery resolution made concrete, and the default direction is deliberate: bearer is the agent-neutral default (an agent host mounts with zero delivery config and works), with cookie as the browser host's override. Defaulting to cookie would have been the inherited-assumption trap the session-delivery queued direction warned against. The build picked the neutral default.

4e — Lazy router import (a beyond-CR improvement worth recording). FastAPI is a runtime dependency of stele but is pulled only on stele.router access. So import stele is FastAPI-free, and library/ORM/migration consumers — and the engine's existing primitive imports — never load FastAPI. This is what keeps §3.4 (engine import paths intact, identity-equal) true without re-pointing a single engine import.

4f — STELE_SECRET_KEYS_PREVIOUS rotation (a beyond-CR-text addition, named here). The §2 rename build also added a key-rotation read-order affordance: a standalone caller can set STELE_SECRET_KEYS_PREVIOUS so the no-arg provider reads a previous key after the current one. This is beyond the CR's §2 text (which scoped only the single-key fallback rename) and adjacent to the §2.4 transition-fallback that was declined for the engine. It is engine-irrelevant (the engine never fires Stele's fallback), standalone-only, and sound. Recorded as an as-built addition with rationale, not a silent extra — it is a useful standalone affordance the reference app and bare deployments can use. CR v0.2 names it.

5. Verification — the contract is satisfiable by a real host

6. End-state

| Repo | origin/main | State | |---|---|---| | DUNIN7/stele (private) | 1077803 | §1 router + §3 SDK + §2 rename — pushed | | loomworks-engine | 08e89c5 | consumes stele; mount-verify test — pushed | | loomworks-record | (this record + CR v0.2 + manifest v0.67) | step 8 |

7. Carry-forwards to P7-3 (and beyond)

  1. The reference app + docs + config generator — P7-3; the full clone-and-run-and-use bar. It demonstrates the full slot wiring (this record proved the engine's real providers satisfy it; P7-3 builds the canonical example a stranger follows).
  2. The WebAuthn-ceremony-gap close — P7-3 (the reference-UI ceremony).
  3. The loomworks_session cookie rename — P7-3, with the reference app exercising delivery.
  4. **The add_passkey_* lift + persons/signup.py split** — the §4 correction-in-waiting: a later slice may dissolve the asymmetry (Stele ships enrollment routes whose enrollment logic is host-side) by lifting add_passkey_* into stele.webauthn, at which point the doubly-bound persons/signup.py split gets scoped on its own terms.
  5. The published-package path — replace the engine's ../stele editable path dep with a version pin against a published stele.
  6. CR-2026-102 SCOPE_TOTP landed-state thread — orthogonal, walkable (P7-1 found zero envelope TOTP secrets).

8. Methodology lessons banked (for the next consolidation)

  1. Surface the scope question at the boundary, don't assume across it. The engine mount-verify needed stele.router inside the engine repo — right at the step-6 guard line. CC surfaced the form (committed test vs ad-hoc script vs no-mount) before writing engine code, rather than expanding into a mount or assuming the test was in-scope. The guard held because the question was asked, not because the line was never approached.
  2. Verify with the real providers, not just stubs. The standalone mount (stubs) proved the router resolves; only the engine mount-verify (real get_current_principal, real ceremony, real db) proved the contract is satisfiable by a real host. Those are different claims, and the done-bar is the second one. Picking the widest-injection-graph route (passkey-enrollment) proved nearly the whole contract in one route.
  3. A narrow injection beats a structural one. Injecting person_email (the one field the route needs) instead of the whole CurrentUser/host_account structure kept Stele free of the host's view shape. The narrowest injection that satisfies the route is the one that keeps the package most mountable.
  4. A neutral default is a design decision, not a fallback. extract_token defaulting to bearer (agent-neutral) rather than cookie (browser-inherited) was a deliberate choice to avoid the inherited-assumption trap. The default a package ships is its position on what a bare host gets.
  5. Name the beyond-CR additions. The STELE_SECRET_KEYS_PREVIOUS rotation and the lazy-router import both exceeded the CR text. Recording them as named as-built additions (with rationale) keeps the build auditable — the next reader sees what the CR asked for and what the build added, separately.

DUNIN7 — Done In Seven LLC — Miami, Florida Loomworks — Stele Phase 7 P7-2 (Mountable Router + SDK Surface) Completion Record — v0.1 — 2026-06-19