DUNIN7 · LOOMWORKS · RECORD
record.dunin7.com
Status Current
Path change-requests/cr-2026-114-stele-phase-7-p7-1-packaging-floor-v0_1.md

CR-2026-114 — Stele Phase 7 P7-1 — Packaging Floor — v0.1

Version. 0.1 Date. 2026-06-19 Author. Marvin Percival (DUNIN7 Operator) with Claude.ai (CR drafting) on DUNIN7-M4; CC to execute on DUNIN7-M4. CR number. CR-2026-114 (confirmed against the live ledger: highest present cr-2026-113, contiguous 109–113; next free = 114). Phase. Stele Extraction Phase 7 — slice P7-1 of the P7-1 → P7-2 → P7-3 sequence. Status. CR drafting. Markdown primary (technical consumer — CC execution). Lands in loomworks-record at change-requests/.

Grounding line. engine main = fe7c3f1 (CR-B merge), alembic head 0093, suite 2928/0/46. If main is not at fe7c3f1, stop and report drift before grounding.


Plain-language summary

What this CR does. Lifts Stele out of the engine monorepo into its own standalone repository (stele), gives it its own dependency manifest, and gives it an independently-runnable database migration set. The done-bar: a stranger clones the stele repo and the tables build against a fresh database, with zero DUNIN7/Loomworks dependency.

What this CR does NOT do. No SDK surface (P7-2). No mountable router (P7-2). No reference app or docs (P7-3). It does not re-open the §E router fork (resolved B-first / primitives-only). It does not split persons/signup.py or decide the cookie helpers (P7-2). It packages the library and its migrations; it does not build the plugin's HTTP surface.

The three decisions this CR makes, named up front.

  1. actor.py stays host-side. It is a Principal→ActorRef adapter (constructs the host memory substrate's contract object, reads no host data). Under B-first it is not part of Stele's standalone library; it is the host's adapter over Stele's Principal. Stele's standalone module set is therefore 9 modules, not 10. (Grounding §G1.)
  1. The migration set is a fresh consolidated baseline, not a revision lift. Stele's tables are scattered across engine revisions 0029/0082/0085/0090/0092 with heavy host interleave (persons-seed INSERTs, mirror triggers, host FKs). A standalone repo has no migration history to preserve, so P7-1 authors one fresh revision that builds the settled Stele schema directly, carrying none of the in-engine expand→contract scaffolding. (Grounding §G2.)
  1. Stele ships 3 tables, not 4 — via KEK-direct TOTP encryption. The engine's envelope crypto reads a fourth table (data_encryption_keys) at runtime for per-scope DEK isolation. Stele standalone has exactly one secret scope (TOTP). P7-1 switches the TOTP secret to the KEK-direct path that sessions already use, dropping the DEK table. Three tables: principals, webauthn_credentials, recovery_codes. (Grounding §G3 — this is the one runtime-behavior change in P7-1; §6 carries its verification.)

Why P7-1 lands alone. The repo lift is mechanical (the import-graph blockers are few and resolved below). The dependency manifest is bounded. The migration extraction is the risk — and the TOTP re-wrap rides with it. Landing both alone, behind a clean clone-and-run done-bar, isolates that risk before P7-2 builds the router on top.


The state P7-1 starts from (settled — do not re-establish)


The three import-graph blockers and their resolution

Step 0's brief named category 5 (import-graph coupling) the load-bearing category: every Stele→engine import is a BLOCKER — a mountable plugin cannot import upward. The Phase-7 Step 0 report focused on the router fork and under-delivered category 5; CC's CR-grounding inventory (stele-p7-1-grounding-inventories-v0_1.txt) completed it. Three Stele→engine imports exist. Each is resolved here, with the brief's triage (move-in / host-supplied / Operator-call).

Blocker 1 — loomworks.memory.base (ActorRef) in actor.pyhost-supplied (actor.py stays host-side)

actor.py constructs ActorRef(kind=…, id=…, display_name=…) — it produces the host memory substrate's contract object. ActorRef is owned by loomworks.memory.base, "referenced, not moved" by design. actor.py reads no host data (Phase 6b made companion_name a passed-in parameter precisely so stele/ imports nothing from persons/). The coupling is contract-shaped, not data-coupled.

Resolution: actor.py is a host-integration adapter, not a core Stele primitive. It does not move into the standalone stele repo. It stays in the engine as the host's adapter over Stele's Principal. Stele's standalone library is the 9 remaining modules. The CR's grounding pass (§3 below) confirms no other module imports actor such that excluding it breaks the 9.

Blocker 2 + 3 — loomworks.credentials.envelope + loomworks.credentials.kek in person_totp / registry / sessionmove-in, KEK-direct (no DEK table)

The envelope path encrypts the TOTP secret via per-scope DEK (SCOPE_TOTP), and the encrypt/decrypt path reads data_encryption_keys at runtime. As-is, absorbing this crypto pulls a fourth table into Stele.

Resolution: Stele absorbs the KEK provider (EnvKeyEncryptionKeyProvider, environment-backed via LOOMWORKS_SECRET_KEY, no table) and switches the TOTP secret to the KEK-direct path that session.py already uses (KEK MultiFernet directly, Level A, no DEK). The data_encryption_keys table and the envelope/SCOPE_TOTP machinery are not absorbed — they stay engine-owned for the engine's other (multi-scope) secrets.

Consequence (named, not hidden): Stele gives up per-scope DEK isolation and cheap KEK rotation (rewrap_all_deks) for the TOTP secret. This is reclaimable if Stele later grows a second secret kind (re-introduce the envelope then). At one scope, the multi-scope mechanism is complexity without payoff. This is a behavior change to a Stele primitive — see §6 for its verification (existing TOTP secrets must decrypt under the new path or be re-wrapped).

The KEK-provider + LOOMWORKS_SECRET_KEY config dependency comes along either way — it is the injection seam P7-1 declares. It matches the loomworks_secret_key ExtractionSkill test pattern already on record.


Seed-alignment check

The seed's authentication framework (v0.12, lines 105–137) commits to: identity is the UUID; credentials are a WebAuthn passkey and/or an authenticator code; recovery uses recovery codes / additional authenticators / operator-mediated verification; email is never identity. It is silent on encryption architecture — it never names envelope-with-DEK vs KEK-direct, and never specifies how the TOTP secret is held at rest beyond its being an authenticator credential.

P7-1 alters one Stele primitive's behavior (TOTP encryption path). It does not alter principals, the credential or recovery primitives, the WebAuthn ceremony, or any identity read/write. The seed-alignment discipline names the one change rather than smoothing it.


What P7-1 commits to — three deliverables, in dependency order

Each deliverable opens with a CR-Step-0 grounding pass (CC inspects the live tree) before the build, per the inspect-first cadence. Where this CR states a fact Phase-7 Step 0 did not enumerate, CC confirms it against the live tree first.

Deliverable 1 — Standalone repository stele

Lift the 9 core modules into their own repo. Zero residual DUNIN7/Loomworks import.

The 9 (the 10 from Step 0 §A minus actor):


__init__  base  credentials  models  person_totp  recovery  registry  session  webauthn

The CR-Step-0 grounding pass must:

What stays behind in the engine: the host-side callers — api/routers/{signup,login,me_security,claim,auth_dev}.py, persons/{signup,login,onboard}.py, and actor.py (now the host's adapter). Post-lift, these import Stele as an installed package, not a local module.

The re-point decision (resolved here, blast radius named): P7-1 lifts Stele AND re-points the engine to consume it as an installed package. Rationale: a lift that breaks the engine's imports without re-pointing them is a half-step (the handoff's lean, adopted). The engine must keep building and its suite stay green (2928/0/46) at every step.

  1. Create the stele repo; copy the 9 modules + the moved-in KEK code into it; build its manifest and migration set (Deliverables 2, 3); verify the repo stands alone (clone-and-build, fresh DB).
  2. Install stele as a package into the engine's environment (editable/local path install at this stage is acceptable; the published-package path is a later concern).
  3. Re-point the engine's imports from loomworks.stele. to stele., one cohesive step, suite green after.
  4. Only then remove the engine's local src/loomworks/stele/ copies (minus actor.py, which stays and is re-pointed to import stele).

Deliverable 2 — Own dependency manifest

Stele's own pyproject.toml declaring the third-party deps the 9 modules + moved-in KEK code actually import. Nothing inherited from the engine's manifest.

From CC's Inventory 1, the third-party set is:


sqlalchemy    — base, credentials, models, person_totp, recovery, registry, session
pydantic      — registry (the Principal view model)
webauthn      — webauthn (py_webauthn) — the load-bearing ceremony dep
pyotp         — person_totp (TOTP)
bcrypt        — recovery (recovery-code hashing)
cryptography  — session (Fernet/MultiFernet — and now the KEK-direct TOTP path)

The CR-Step-0 grounding pass must:

Deliverable 3 — Independently-runnable migration set (the riskiest)

Stele's own alembic root + one consolidated baseline revision that builds the 3-table settled schema against a fresh database, with no engine revisions present.

The schema the baseline builds (the final Stele state, per CR-B end-state + the KEK-direct resolution):

The CR-Step-0 grounding pass must:

The extraction shape (decided): a fresh consolidated baseline. One revision, down_revision = None, building the 3-table schema as-of the settled state. It replays none of the engine's 0029→0093 incremental history — a standalone repo has no history to preserve. It carries none of the persons-seed INSERTs, the mirror triggers, or the host-FK interleave; those existed only to make the in-engine expand→contract reversible.

The engine's own chain is unaffected. The Stele migration set is additional and standalone, for the new repo's consumers. The engine keeps building at 0093 exactly as today. P7-1 does not alter the engine's migrations/versions/.


§6 — The TOTP re-wrap: P7-1's one runtime-behavior verification

This is the deliverable that is not a pure relocation. Switching the TOTP secret from envelope (SCOPE_TOTP) to KEK-direct changes how the secret is encrypted at rest. The CR must carry this as an explicit, verified migration of existing encrypted secrets — not a silent swap.

The CR-Step-0 grounding pass establishes, against playground_dev:

Two cases, and the CR commits the handling for each:

  1. The standalone repo's own fresh database (the clone-and-run done-bar): no existing secrets. KEK-direct is the only path. Nothing to re-wrap. The done-bar is met by the baseline building clean and a fresh signup writing + reading a KEK-direct TOTP secret.
  1. The engine's playground_dev (and any deployed DB), post-re-point: existing totp_secret values are envelope-encrypted. After the engine consumes stele with KEK-direct, those secrets must still decrypt, or be re-wrapped. The CR commits a re-wrap step: a one-time data migration (engine-side, since it touches the engine's live data) that reads each envelope-encrypted totp_secret, decrypts it via the envelope path (still available engine-side), and re-encrypts it KEK-direct. This runs once, in the engine, as part of the re-point. It is not part of the standalone Stele migration set (the standalone repo never holds envelope-encrypted secrets).

Verification (on playground_dev, rolled back where mutating):

If the CR-Step-0 pass finds the re-wrap is larger or more entangled than this (e.g. envelope and KEK-direct ciphertext are not column-compatible), halt and surface to the Operator before building — fold-vs-separate is the Operator's call at the moment of discovery (CR-B Lesson 5).


Carry-forwards held out of P7-1 scope (named so they are not pulled in)


Standing disciplines carried in


The arc from here

  1. CC executes P7-1, deliverable by deliverable, each opening with its CR-Step-0 grounding pass, inspect-first, halt-before-push. Likely commit shape: repo creation + 9-module copy + KEK move-in; manifest; migration baseline; engine package-install + import re-point; engine local-copy removal; the TOTP re-wrap data migration. CC sequences within the add-and-verify discipline.
  2. A completion record filed to loomworks-record at phases/stele-extraction-phase-7/ (or change-requests/), recording the as-built deltas, the re-wrap verification, and the clone-and-run done-bar met.
  3. P7-2 (SDK surface + mountable router — CR-2026-115) and P7-3 (reference app + docs + config gen — CR-2026-116) follow as separate CRs, each its own drafting + execution arc. CR numbers confirmed against the ledger at draft time, not assumed here.

Two-role division holds: CR-drafting (Claude.ai) separate from execution (CC). Fresh chat per role.


Open items riding alongside (not P7-1 blockers)


DUNIN7 — Done In Seven LLC — Miami, Florida CR-2026-114 — Stele Phase 7 P7-1 — Packaging Floor — v0.1 — 2026-06-19