Version. 0.1
Date. 2026-06-14
Status. Scoping note. Sequences the extraction of the identity/authentication code into a named Stele module inside the engine — extract-in-place, not a separate service. Precedes a change request; nothing here is built. Operator-facing; Markdown primary (technical consumer: Claude Code), HTML companion optional.
Decision recorded. Extract first; recovery-completeness lands after, inside the Stele boundary (recovery code is fully internal to Stele with zero outside callers, so the extraction is safe to do around it and recovery is cleaner to finish inside a named module).
Source grounding. Built on the Claude Code coupling map (engine c210715, 2026-06-13) and the four-product + Stele standing note v0.3 (§8). The coupling map supplies the change list (every import that moves) and the drag risks; this note turns that into an ordered, halt-before-push extraction. Verify against engine-database truth at each phase, not the test suite alone (standing discipline).
Stele — the principal layer (the part of the engine that says who you are and authenticates you) — already exists as engine code that was never named or drawn as a boundary. This note sequences pulling it into a named module inside the engine (not a separate service yet), so it becomes a clean, identifiable thing the other products (OVA, FORAY, Loomworks) reference. Most of the work is mechanical — moving code into a stele module and updating imports — because Stele slots under the existing identity chokepoint and references the shared actor type, so the 119 places that depend on the chokepoint and the 93 places that use the actor type do not change. There is exactly one part that is a real refactor, not a move: sign-up, where creating a principal is today fused atomically with provisioning the account and personal engagement. That seam gets its own isolated phase and its own design decision (what a principal-without-onboarding means, and how it heals). The decision needed from the Operator: approve the phase order and the sign-up transaction approach (§4), then this becomes a change request.
Stele is the principal layer named in standing note v0.3 §8: it establishes, persists, and authenticates who (human or agent), holds roles/attributes, and mints/revokes credentials. This note is only about the extraction — giving the latent engine code a named boundary. It does not add the agent-authentication path (RFC 7523/8693) — that is new construction, sequenced as a separate effort after the extraction. It does not split Stele into a separate service — that is deferred until a second consumer needs it.
The connection to the Companion is already documented (v0.3 §8.3) and is preserved unchanged by this extraction: Stele slots under the resolve-current-person dependency and feeds the actor-construction helpers; the Companion above that line does not change.
The identity rows (PersonRow, WebauthnCredentialRow, RecoveryCodeRow), the registry view (Person, get_person_by_id, get_person_by_email), the credential and auth modules (credentials.py, webauthn.py, recovery.py, totp.py, session.py), the credential/auth half of login.py and signup.py, and the actor_from_person / actor_from_companion constructors lifted out of orchestration/helpers.py.
memory.base.ActorRef — the substrate type for every memory event (93 consumers); Stele produces it, memory/ owns it. credentials.kek / envelope — the shared MultiFernet key-encryption key (also used by api-keys, system-config, the credential store); the KEK stays in credentials/, Stele references it, only the session encode/decode logic moves. get_current_person — stays a FastAPI dependency in api/deps.py that delegates to Stele while keeping its name and signature, so the 119 Depends(get_current_person) sites are untouched.
personal_engagement.py (it writes engagement rows + founding memory events — Loomworks provisioning that happens to live under persons/); the sign-up onboarding tail (commons membership + personal engagement); PersonRow's account-lifecycle columns (credit/ reads them ~34 times); the whole delegation/ package; and verify_companion_authorization (the OVA seam — sits beside Stele, not inside it; agent authentication = Stele, agent governance = Loomworks/delegation).
PersonRow mixes identity fields with account-lifecycle fields that credit/ reads. On day one Stele owns the persons table whole; credit/ imports PersonRow from Stele's new module path. The physical split (identity table vs. account table) is forced only when Stele becomes a separate service — not now.Each phase is its own commit (or small commit set), halt-before-push, tested against engine-database truth. Phases are ordered lowest-risk-first so the boundary is proven before the one hard refactor.
Create the stele module. Move the pieces with zero external callers: WebauthnCredentialRow, RecoveryCodeRow, credentials.py (the identity-credential parts), webauthn.py, recovery.py, totp.py, and the session encode/decode logic from session.py (referencing the KEK left in credentials/). Risk: lowest — nothing outside the set imports these. Purpose: stand up the module and prove the boundary holds before touching anything with external coupling.
Move PersonRow (whole table, lifecycle columns included — see §2.4) and the registry view Person into Stele. Repoint the import paths: PersonRow ~49 sites (heaviest in credit/), Person ~40 sites. These are mechanical import changes — the symbols don't change, only where they're imported from. Alternatively Stele re-exports to shrink the churn; decide in the CR. Risk: low but wide (touches credit/ and api routers); purely mechanical, fully reviewable.
Lift actor_from_person and actor_from_companion out of orchestration/helpers.py into Stele (they need only ActorRef + Person). Repoint ~17 call sites (all in orchestration/). Leave verify_project_membership and the engagement helpers in helpers.py (they import persons.memberships — Loomworks). Risk: low; contained to orchestration/.
get_current_person at Stele
get_current_person stays in api/deps.py with its name and signature unchanged; its body now calls Stele's resolver (get_person_by_id + decode_session, both now in Stele). The 119 Depends(get_current_person) sites do not change. This is the connection point from v0.3 §8.3 made real. Risk: low mechanically, but it is the auth resolver — test the full sign-in → session → resolve → actor path against the database here, not just the suite.
Split account creation into "Stele mints the principal" → "Loomworks provisions the account and engagement." This is the one phase that is design, not movement. It carries the transaction-boundary decision (§4) and gets its own scrutiny, its own commit, and the hardest testing. Do this last so everything underneath is already a clean, proven boundary.
Recovery code is fully internal to Stele (zero outside callers). Once Stele is a named module, completing recovery (the open recovery-completeness work, 2026-06-12) is finishing a defined module rather than scattered code. Scoped separately; named here as the immediate next effort after extraction.
The RFC 7523/8693 agent-authentication path (new construction, not extraction) and the actor_from_agent constructor. Sequenced after the extraction and recovery; its own scoping.
Sign-up (signup.py:297–347) does four things in one atomic transaction: mints the principal (Stele), creates the commons + engagement membership, creates the personal-engagement row (via personal_engagement.py), and writes its founding memory events. The first is Stele; the last three are Loomworks provisioning. Extracting Stele means cutting this atomic flow.
After the split, the flow is "Stele mints the principal" → "Loomworks provisions the account/engagement." What happens if provisioning fails after the principal exists? Today this is impossible (one transaction). After the split, a principal can exist without a personal engagement, and that state must have a defined meaning and a heal path.
The choice shapes Phase 4's CR. The lean (A) keeps the extraction uniformly low-risk now and pushes transactional decoupling to the same later moment as the service split — but B is the more truthful long-term shape for an independent Stele, so if the service extraction is near, B may be worth paying for now rather than twice.
Settled (from the coupling map + the extract-first decision): the boundary (§2), the two deferrals (§2.4), the phase order (§3), and that recovery follows the extraction inside the boundary.
Needs the Operator before this becomes a CR:
PersonRow/Person to shrink the ~89 import changes, or the imports are repointed outright. Minor; can be decided in the CR. Default: repoint outright for a clean boundary, unless the churn is judged not worth it.On the Operator's go-ahead for (1) and (2), this becomes the Stele extraction change request, phase by phase, halt-before-push.
DUNIN7 — Done In Seven LLC — Miami, Florida Loomworks scoping note — Stele extraction (principal layer as a named in-engine module) — v0.1 — 2026-06-14