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).
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.
Stated by the Operator, and now the governing principle for where things live:
dunin7.com.doneinseven.com.
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.
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:
dunin7.com, no substitutes. This is the standing direction from 2026-07-17: anything authenticating a person in a DUNIN7 system uses Stele.
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.
require_founder today.
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).
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.
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.
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?
NEXT_PUBLIC_API_URL=https://api.dunin7.com; the browser calls the engine directly at its own hostname. This makes the session-cookie domain change and the engine CORS configuration load-bearing./api/* to the engine server-side, so the browser only ever talks to app.dunin7.com. The session cookie stays first-party. No cookie-domain change is needed. CORS is moot.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.
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:
doneinseven.com — public static pages (talk-about-services). These follow the hosting brief: VPS origin behind the Tunnel, edge-cached. Isolated from dev. Not part of this perimeter build.dunin7.com — the authenticated service perimeter (provide-services). Stays on the Mac Mini for now, because that is where the engine, Stele, Postgres, and the whole authenticated stack live. Moving those to a VPS is a far larger lift than static pages and is not proof-of-concept work.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.
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.
app.dunin7.com → http://127.0.0.1:3001 and api.dunin7.com → http://127.0.0.1:8000 in /etc/cloudflared/config.yml, placed before the .dunin7.com wildcard, mirrored to the home-directory copy, then restart the launchd daemon. (Use 127.0.0.1, never localhost — see section 7.) Lift the dev-only /api/ proxy guard in next.config.ts so it runs in production (Path B, section 5A), preserving the /api prefix strip. Production build of the Operator Layer served at app.dunin7.com. Change WEBAUTHN_RP_ORIGIN to https://app.dunin7.com (section 7 — sign-in rejects otherwise). Set LOOMWORKS_SESSION_COOKIE_SECURE=true and fix the secure=False sites (section 5A). Build the three-level authorization model on the existing designation mechanism. No cookie-domain change, no CORS opening — Path B removes both. Stop-safe: the Operator signs in on app.dunin7.com, end to end, over HTTPS; the perimeter correctly distinguishes the three levels; an unauthenticated request to a protected surface is refused. The api.dunin7.com hostname exists and routes, but the browser reaches the engine through the app.dunin7.com proxy, not directly./api/sales/ into the engine, gated on designation (not founder-only). Retire the cross-domain carry. Stop-safe:* the tool works behind the gate, reached through the menu.doneinseven.com, authenticate-on-dunin7.com, return flow. Built last, on a proven perimeter, adopting the established OIDC-shaped redirect pattern rather than hand-rolling it (open-redirect risk). Test-user identities from Stage 3 exercise it. Stop-safe: outside users can reach Loomworks through the proper arrival experience.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.
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).
86d6a87a-ef68-41aa-9f2a-bb2657944d5286d6a87a-ef68-41aa-9f2a-bb2657944d52.cfargotunnel.com (copy literally)com.cloudflare.cloudflared, running as root, pid was 99421. A second inert plist homebrew.mxcl.cloudflared exists loaded-but-not-running — "restart cloudflared" aimed at that label would appear to succeed and change nothing. Restart the system daemon.
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.
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:
app.dunin7.com → http://127.0.0.1:3001 and api.dunin7.com → http://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.next.config.ts (Path B), preserving the /api prefix strip; confirm the four base-URL call sites work same-origin.WEBAUTHN_RP_ORIGIN to https://app.dunin7.com.LOOMWORKS_SESSION_COOKIE_SECURE=true; fix signup.py:249 and me_reactivate.py:155 to use _cookie_secure().app.dunin7.com.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.
app.dunin7.com (Operator Layer), api.dunin7.com (engine, reached via the app proxy under Path B).app/api: already resolve via the existing wildcard; explicit records optional, not blocking (section 7)./etc/cloudflared/config.yml, UUID and daemon details in section 7.DUNIN7-M4); the Operator Layer has been reachable in production only as a development server, which Stage 1 replaces.DUNIN7 — Done In Seven LLC — Miami, Florida Loomworks — dunin7.com perimeter and sales-generator relocation — Session handoff — v0_2 — 2026-07-18