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.
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.
DUNIN7/stele (private; main = 1077803, pushed) — three slices on 0d5e265:
src/stele/api.py (new, +402) — the eight-route mountable APIRouter, the injection-dependency slots, and a default current-principal resolver over the neutral resolve_session. The authorization boundary is stated in the module: Stele authenticates (resolves who, operates on the principal's own credentials); the host authorizes (what-here) via the resolve_current_principal slot.src/stele/__init__.py (+191) — the curated public API: 65 primitives exported eagerly and FastAPI-free, plus a lazily-imported stele.router and its slots, plus the mount-contract docstring and the stated authorization boundary.src/stele/kek.py (22 changed) — the no-arg KEK fallback env-var key renamed LOOMWORKS_SECRET_KEY → STELE_SECRET_KEY. The injectable path (EnvKeyEncryptionKeyProvider(secret_key=...)) is untouched.pyproject.toml (+1) — fastapi==0.135.3 pinned to the engine's resolved version.
loomworks-engine (main = 08e89c5, pushed) — one slice on ff0c4cf:
tests/test_stele_router_mount.py (new) — the mount-verify test (verification only; api/app.py and the auth routers untouched).
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_KEY → STELE_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.
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).
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.
import stele is FastAPI-free; 65 symbols in __all__; submodule paths intact and identity-equal to top-level (engine-safe, §3.4); the lazy stele.router resolves to the eight-route APIRouter; override-slot identity holds.STELE_SECRET_KEY; the old name is dead; an injected provider ignores the env entirely; STELE_SECRET_KEYS_PREVIOUS rotation works.stele.router and wires the engine's real providers into the slots (get_current_principal → resolve_current_principal, the engine's get_db_session / get_secret_key / get_webauthn_config / get_add_passkey_store, and persons.signup.add_passkey_* for the ceremony, plus a host-supplied person_email). It drives POST /me/security/passkeys/begin end-to-end against the test DB for a seeded principal — the widest injection graph (resolver + db + secret_key + webauthn_config + add_passkey_store + ceremony + person_email + the stele.credentials primitive) — and returns 200 with add_id + options, the existing passkey correctly excluded. A second test confirms an unwired mandatory slot fails loud. Override-slot keys are asserted identity-equal to stele.api's slots, so the override seam works with real providers, not just stubs.
| 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 |
include_router(stele.router) and wire the named slots.stele.* primitive imports (intact, identity-equal); stele.router is not mounted in the engine's production surface.stele's KEK fallback reads STELE_SECRET_KEY; the engine's deployment env stays LOOMWORKS_SECRET_KEY (the engine injects, never fires Stele's fallback).loomworks_session cookie rename — P7-3, with the reference app exercising delivery.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.../stele editable path dep with a version pin against a published stele.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.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.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.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.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