DUNIN7 · LOOMWORKS · RECORD
record.dunin7.com
Status Current
Path session-handoffs/loomworks-dunin7-perimeter-session-handoff-v0_2.md

Loomworks — dunin7.com perimeter and sales-generator relocation — Session handoff — v0.2

Version. 0.2 Date. 2026-07-18 Author. Claude.ai, on Operator direction. Status. Session handoff. Captures a scoped-but-not-yet-built plan so a fresh chat can resume without reconstructing it. Nothing in this plan is built. Stage 0 inspection is now complete; its findings are folded in and the immediate next action is the Stage 1 build brief, described in section 8.

Change from v0.1. The Stage 0 inspection ran (Claude Code, 2026-07-18) and contradicted five of v0.1's premises; all five are corrected here. The Operator delegated the one open technical decision to Claude.ai, which chose Path B (same-origin server-side proxy) — recorded in section 5A. A separate public-pages hosting brief was reconciled: the Mac Mini is the interim origin for the authenticated perimeter, with a later VPS/dedicated-host cutover being a Tunnel-config change (section 5B). The corrections change what Stage 1 does; a fresh chat must run from this version, not v0.1. Reads with. loomworks-operator-tools-menu-scoping-note-v0_3, cr-2026-153-operator-tools-menu-v0_1, and the three same-day investigations grounding them (agentic identity, authenticated marketing access, pre-Change-Request verification).


Plain-language summary

What this is. A plan to give the sales-perspective generator a proper, hardened, public home — reached by signing in with Stele — and to build the authentication-and-authorization perimeter that everything else insider will sit behind. It is scoped and agreed but not built.

The decision that shaped it. Everything DUNIN7 provides as a service lives on dunin7.com. Everything DUNIN7 says about a service lives on doneinseven.com. The sales generator is a service, so it moves off the marketing site (doneinseven.com) to dunin7.com, inside the authenticated perimeter. This single rule retires the entire cross-domain token mechanism that was built earlier, because sign-in, menu, engine, and tool then all share one registrable domain.

Who needs access, and why it matters. Three levels, and they are the reason this is being built properly now rather than hardcoded: the founder (everything), DUNIN7 representatives (Aldous and Warwick, who need sales access immediately), and outside customers (test users, who may arrive fast). Three real user types force a real authorization model from day one.

How it will be built. Four staged deliveries, each landing a working thing you can stop safely after, with the riskiest net-new piece (the outside-user redirect flow) built last on a proven perimeter.

The immediate next step. Stage 0 inspection is complete (section 8). One technical decision it surfaced — how the frontend reaches the engine in production — was delegated to Claude.ai and settled as Path B, the same-origin server-side proxy (section 5A). The next action is the Stage 1 build brief, resting on the corrected facts in this version.

Five corrections from the Stage 0 inspection, load-bearing: the operative tunnel config is /etc/cloudflared/config.yml (not the home-directory copy); ingress rules must use 127.0.0.1, never localhost, because cloudflared mis-dials IPv6 on this machine; WEBAUTHN_RP_ORIGIN must change to https://app.dunin7.com or every sign-in rejects; the session-cookie code is in the engine, not Stele; and the DNS records for app/api are optional-not-blocking because a wildcard already resolves them. Details in sections 7 and 8.


1. The domain rule (Operator, 2026-07-18)

Stated by the Operator, and now the governing principle for where things live:

dunin7.com is where DUNIN7's services and their sign-in live. Insiders, outside customers, and eventually agents all authenticate there, because that is where authentication happens — not because dunin7.com is insider-only. An outside Loomworks user given a URL is forwarded to a dunin7.com page where Stele authentication happens, then returned.

Consequence: the sales generator is a service DUNIN7 operates, so it belongs on dunin7.com. It sits on the marketing site today only as an accident of the spike's history. Moving it home dissolves the cross-domain problem (see section 4).

The public example generator stays on doneinseven.com, ungated — it is DUNIN7 talking about the service to draw people in. Correct by the rule.

2. Authentication and authorization are separate (correction, 2026-07-18)

An early framing — "everyone on dunin7.com signs in, everything is open to them" — was wrong and was corrected by the Operator. The correct model:

With only the founder, require_founder hid this distinction. Aldous, Warwick, and test users force it into the open, which is why the authorization model must be real from the start.

3. The three authorization levels (Operator-confirmed)

  1. Founder (the Operator). Everything, including the sales generator and admin surfaces. require_founder today.
  2. DUNIN7 representative (Aldous, Warwick). Sales and marketing tools including the sales generator; not admin, not credit-grant or provisioning routes. They represent DUNIN7 in a sales capacity and need access immediately.
  3. Outside customer (test users). Loomworks itself; not the sales generator, not DUNIN7-internal surfaces. Test users may arrive fast, so this level is in scope now, not deferred.

The engine already has the raw material: require_operator_designation reads a designation from a fixed set (operator, contributor, domain_expert) on a membership. The authorization layer largely exists; it is not yet wired to the sales generator. Build authorization as a seam that GRANTHA can later answer, but hardcode the answer for now — do not build GRANTHA (it is documents with four ratified items and not ready).

4. What the domain move retires

The tools-pass mechanism in CR-2026-153 (the engine mint, the marketing-side entry point, the form-POST-to-cookie carry, the edge verify) existed only to cross registrable domains — tool on doneinseven.com, auth on dunin7.com. With the tool on dunin7.com, the session cookie set with Domain=dunin7.com spans everything, and the menu simply links to the tool.

What retires: the cross-domain carry — the mint-a-pass, POST-it, verify-it-at-the-edge dance.

What stays: the authorization question it was transporting. Same-domain, "is this authenticated person allowed to use this tool?" is answered against the session plus the designation, not a carried token. CR-2026-153's mint/verify is superseded as transport; its authorization intent lives on in the designation check. Preserved in the record as the superseded approach — it was the right answer to the cross-domain problem, which the domain rule made disappear.

5. Where the sales generator lands: the engine (Claude Code recommendation, verified)

Claude Code inspected both candidate homes and recommended the engine, one-sidedly:

So the /api/sales/* chain (currently JavaScript Cloudflare Pages Functions on the marketing project) ports into the engine as Python/FastAPI routes.

Correction from Stage 0: the reusable Anthropic seam is not the Manifestation module (which only consumes it) — it is AnthropicLLMClient + resolve_room_llm. And the engine resolves API keys from the database, not from an environment variable, via resolve_engagement_api_key (a three-tier cascade: engagement, person, system). The ported sales chain must obtain its key that way, not from os.environ as the marketing chain does today. This is a real change in how the chain gets its key.

5A. The production frontend-to-engine decision: Path B (settled by Claude.ai on delegated authority, 2026-07-18)

Stage 0 surfaced a fork that v0.1 had not anticipated, and it gates whether several other changes are needed at all. The Operator delegated the decision to Claude.ai. It is settled as Path B.

The fork. In production, how does the Operator Layer at app.dunin7.com reach the engine?

Why Path B. It is the pattern the dev setup already uses, so it makes the working arrangement permanent rather than introducing a new one. It deletes the three riskiest edits in the whole plan — the four cookie-setter changes, the logout/clear-cookie change, and the cross-origin CORS opening — each a place a subtle error locks the Operator out of their own system. The one thing Path A buys, an independently reachable engine hostname, is not needed yet and can be added later without undoing Path B.

What Path B costs, and the one edit it does require. The dev-only guard in next.config.ts (the /api/ rewrite exists only when NODE_ENV === "development") must be lifted so the proxy also runs in production. Confirmed at next.config.ts:20-24. Two coupled details the rewrite must preserve: it strips the /api prefix (/api/operator/notifications → engine /operator/notifications) — any replacement must keep this or every route 404s; and the client base URL currently reverts to http://localhost:8000 in a production build because NEXT_PUBLIC_API_URL is only set inline by the dev script. Under Path B the client should call relative /api/ (same-origin) so the base-URL revert stops mattering — but this must be verified, because four call sites (SSEProvider.tsx:146, uploads.ts:51, api/assertions.ts:95, api/door3.ts:62) build on the base URL directly and bypass the wrapper.

Consequence for v0.1's plan. The cookie-domain change (v0.1 section 6, Stage 1) and the CORS opening are removed from scope under Path B. The secure=False inconsistency (below) is still worth fixing on its own merits, but the domain change that would have made it urgent is gone.

Cookie hygiene still worth fixing, independent of Path B. Stage 0 found signup.py:249 and me_reactivate.py:155 hardcode secure=False, while login.py uses the environment-aware helper _cookie_secure(). Once the perimeter is served over HTTPS and LOOMWORKS_SESSION_COOKIE_SECURE=true is set, sign-up and reactivate would issue non-Secure cookies over TLS that Safari drops — sign-in would work, sign-up would silently fail to establish a session. Fix all cookie writes to use _cookie_secure(). This is a latent production break independent of the domain question, and cheap to fix while in the code.

5B. Hosting reconciliation: the Mac Mini is the interim perimeter origin (path b)

A separate public-pages hosting brief (dunin7-hosting-architecture-brief-v0_1, 2026-07-18) recommends moving DUNIN7's public static pages off the Mac Mini onto a cheap VPS origin behind the Tunnel, to keep public uptime isolated from the daily dev machine. That brief named a tension it correctly refused to leave silent: is the Mac Mini the build environment or the permanent public origin?

Resolution, via the domain rule:

The Mac Mini is therefore the interim origin for the authenticated perimeter — path (b) in the hosting brief's terms — knowingly temporary. A later cutover to a VPS or dedicated host is a Tunnel-config change, not a rebuild, because behind the Tunnel the origin is a swappable part. This resolves the tension the hosting brief named. Nothing in Stages 1–4 changes; only the understanding that the M4 is interim, not permanent, for the perimeter.

One content bug from that brief, unrelated to this build: foray.dunin7.com still lists zero-knowledge verification as "Future," but the Kaspa Toccata hardfork activated 2026-06-30, making on-chain ZK verification live. That is a wrong factual claim on a public page — a marketing-content fix for whenever the public pages are next touched, not part of the perimeter build.

6. The staged build (four deliveries, stop-safe after each)

Agreed shape: build all of it, but as staged deliveries with a working, safe stop after each — not one long sequence. The two worst moments of the prior day (an API key leak, an ungated preview) both came from rushing a long sequence; staging isolates the risk.

Discipline throughout: Claude Code verifies every step against the running system, not against a summary; halts between stages. Any Stele modification is surfaced to the Operator as a diff before it is applied.

7. The tunnel and infrastructure findings — verified by Stage 0 inspection, 2026-07-18

Exact facts from the Claude Code inspection. These replace v0.1's dashboard-level guesses.

The tunnel is locally configured; do not migrate it. The Zero Trust dashboard confirms dunin7-m4 "cannot be managed from the Zero Trust dashboard as it is a locally configured tunnel." Do NOT click "Start migration" — irreversible, and it would move control away from the local file. Leave it local.

There are two config files, and only one is operative. The running daemon reads /etc/cloudflared/config.yml (owned root:wheel). /Users/dunin7/.cloudflared/config.yml is a byte-identical copy with no sync mechanism — editing it alone changes nothing on the running tunnel, and looks like a ghost failure. Edit the /etc file; mirror to the home copy to prevent drift. Confirmed operative via the daemon's own error log (ingressRule=4 matches the 5-rule current file, not the 4-rule stale backup).

The wildcard shadows nothing today, but any rule after it is dead. *.dunin7.com sits 5th of 6 in the ingress list, routing to http://localhost:3400. cloudflared evaluates top-down, first match wins. New rules for app/api go before the wildcard — conventionally right after loomworks-dev.dunin7.com.

⚠️ The IPv6 landmine — this one 502s a healthy system. cloudflared dials [::1] for localhost; the engine binds IPv4-only (127.0.0.1:8000). An ingress rule written http://localhost:8000 will fail with the engine running perfectly — this exact error is already in the daemon log against existing rules (dial tcp [::1]:3400: connect: connection refused). Every new rule must use http://127.0.0.1:PORT, never localhost.

DNS is optional, not blocking — v0.1 had this backwards. A proxied wildcard DNS record .dunin7.com already exists, so app.dunin7.com and api.dunin7.com already resolve and already reach the tunnel today — they return 502 only because the wildcard ingress rule forwards to port 3400 where nothing listens. The blocking work is the ingress rules, not DNS. Creating explicit app/api CNAME records (proxied, → the cfargotunnel target above) is still worthwhile for explicitness and per-hostname settings, but adding them alone changes nothing observable. If created: proxied (orange-cloud) is mandatory — .cfargotunnel.com only resolves through Cloudflare's edge. The Operator should eyeball one existing record (e.g. forge) in the dashboard first, since proxied records hide their target from dig and the shape could not be verified locally.

WebAuthn — the auth-breaks-on-move finding. WEBAUTHN_RP_ID=dunin7.com is the apex, so passkeys survive the move to app.dunin7.com with no re-registration — good, and non-obvious. But WEBAUTHN_RP_ORIGIN=https://loomworks-dev.dunin7.com must change to https://app.dunin7.com or every sign-in ceremony rejects the origin. In no config comment; would be discovered painfully on move-day.

The rate limiter is functionally broken behind the tunnel. slowapi, keyed on request.client.host — which behind cloudflared is the tunnel, so all users collapse into one bucket. IP-based limiting does not work in this topology and already affects the one existing limited route. The ported sales chain, being LLM-cost-bearing, wants per-principal limiting, which does not exist yet. Flag for Stage 2/3 scoping; not a Stage 1 blocker.

A world-readable Anthropic key sits at /Users/dunin7/loomworks/.dev.vars (mode -rw-r--r--). Git-ignored and untracked (not in history), and likely vestigial since the engine resolves keys from the database, not env. Operator rotation decision — low urgency, non-blocking. Claude Code did not read its value.

8. The immediate next action — Stage 1 build brief

Stage 0 inspection is complete (section 7). The next action is for a fresh Claude.ai chat to write the Stage 1 build brief for Claude Code, using this document as its orientation. Stage 1 is the perimeter, corrected for Path B (section 6). In order, the brief should have Claude Code:

  1. Tunnel ingress — add app.dunin7.comhttp://127.0.0.1:3001 and api.dunin7.comhttp://127.0.0.1:8000 to /etc/cloudflared/config.yml, before the wildcard, mirror to the home copy, restart the launchd system daemon. Verify both hostnames stop returning 502 and route correctly. 127.0.0.1, not localhost.
  2. Lift the production proxy guard in next.config.ts (Path B), preserving the /api prefix strip; confirm the four base-URL call sites work same-origin.
  3. WebAuthn origin — change WEBAUTHN_RP_ORIGIN to https://app.dunin7.com.
  4. Cookie secure — set LOOMWORKS_SESSION_COOKIE_SECURE=true; fix signup.py:249 and me_reactivate.py:155 to use _cookie_secure().
  5. Production build of the Operator Layer, served at app.dunin7.com.
  6. Three-level authorization on the existing designation mechanism — note the shape mismatch: require_founder is Depends-injectable and platform-scoped; require_operator_designation is not injectable, takes a membership positionally, and is engagement-scoped. The three-level model will not have a uniform call shape; the brief must account for this.

Halt after the perimeter works end-to-end (Operator signs in on app.dunin7.com over HTTPS, three levels distinguished, unauthenticated request refused) before Stage 2. Surface any Stele/engine auth diff to the Operator before applying. Verify every step against the running system.

Two production unknowns Stage 0 could not settle, for the Stage 1 brief to resolve or flag: how the engine is started in production (no Makefile, Dockerfile, compose, or README run instructions exist), and confirmation that LOOMWORKS_SESSION_COOKIE_SECURE=true is actually set in the production environment.

9. Facts, now settled by Stage 0

10. Prior-day context this rests on (for a fresh chat)


DUNIN7 — Done In Seven LLC — Miami, Florida Loomworks — dunin7.com perimeter and sales-generator relocation — Session handoff — v0_2 — 2026-07-18