DUNIN7 · LOOMWORKS · RECORD
record.dunin7.com
Status Current
Path scoping-notes/loomworks-operator-tools-menu-scoping-note-v0_3.md

Loomworks — Operator tools menu and authenticated edge access — Scoping note — v0.3

Version. 0.3 Date. 2026-07-17 Author. Claude.ai, on Operator direction. Status. Third draft. Nothing here is built. Grounded in two inspections filed the same day; where those inspections say a thing does not exist, this note treats it as not existing rather than assuming it can be reached for. Change from v0.2. The five verifications section 11 of v0.2 listed as unanswerable from Claude.ai were answered by Claude Code against the running system, the Cloudflare dashboard, and source (2026-07-17). Four resolve in the design's favour. The fifth found something the note had assumed away: the engine is reachable in production only because the Operator Layer is running as a development server, and the proxy the design depends on exists only in development mode. Sections 3.2, 3.3, 6 and 11 changed. v0.2's position that production hostnames were unproven is preserved in section 6 alongside the correction.

Change from v0.1. Two Operator decisions settled, both from section 10 of v0.1. The carry changed from a link fragment to a form POST that sets a cookie — the pass no longer appears in an address bar at all. Cloudflare Access is confirmed to stay on the record site and the mockups. Sections 3.3, 3.4, 5, 6, 7, 8, 9, 10 and 11 changed. v0.1's fragment recommendation and its reasoning are preserved in section 9 alongside the correction. Grounding. The agentic identity investigation (2026-07-17) and the authenticated marketing access investigation (2026-07-17), both by Claude Code against live source. Change Request 2026-152 (the sales perspective spike), sections 1 and 8. Naming. "Operator tools menu" is a working name. See section 10.


Plain-language summary

What this scopes. A menu inside the Operator Layer that lists the tools the Operator uses — the sales perspective generator today, a FORAY generator in design, more after that — and makes them reachable only by people the Operator has authorised. The person signs in with Stele, as they already do. The menu then hands the tool a signed pass on their behalf. Each tool checks the pass before it does any work.

What changed, and why this exists. The sales perspective generator was built at the edge, on the marketing site, and needed a gate. The gate reached for was Cloudflare Access — a different company's identity system, in front of a company that builds identity systems. The Operator's standing direction is that anything authenticating a person in a DUNIN7 system uses Stele. This note is how that direction is met.

What is new. Four pieces: the menu surface, an endpoint in the engine that mints a signed pass, a small function on the marketing site that receives the pass and turns it into a cookie, and a check in each tool that verifies it. Everything else already exists.

What decision rests on this. Whether to write a Change Request against this design. The two decisions v0.1 left open are settled and recorded in section 10.

Out of scope. Agents. Per-tool authorisation. The public Example Generator, which is public by design and has no identity to prove.


1. What prompted this

Change Request 2026-152 built the sales perspective generator as Cloudflare Pages Functions on the marketing site. It works. It has no gate, and on 2026-07-17 a preview deployment was briefly reachable by anyone holding the address, with four endpoints spending Anthropic budget behind it. The preview was deleted.

The gate designed for it was Cloudflare Access — a perimeter in front of the paths, using Cloudflare's identity rather than Loomworks's.

The Operator's direction, 2026-07-17: anything that authenticates a person in a DUNIN7 system uses Stele. Otherwise the company never proves to itself, or to anyone else, that its own identity substrate works. The moment authentication was needed, the stack reached for someone else's. That is the finding, and it is the reason for this note.

The Operator also confirmed that more tools are coming — a FORAY generator is in design. So this is infrastructure built once, not a one-off gate.


2. What the inspections established

Every claim in this section comes from a Claude Code inspection of live source on 2026-07-17. None of it is assumed.

Stele authenticates humans, and only humans. WebAuthn passkey as first factor, time-based one-time password as second. The engine then sets a session cookie named loomworks_session — HttpOnly, host-only, SameSite Lax, holding a stateless Fernet-encrypted token with a twenty-four hour lifetime. There is no server-side session store.

Stele issues nothing else. Its own contract: "Stele proves who a caller is; your app decides what they may reach." No downstream tokens, no delegation, no authorisation. Everything past resolving the principal is the host's business.

Stele's session cannot be verified at the edge. It is sealed with a symmetric key. Anything that can verify it can also mint it. A Pages Function could only check it by holding Stele's own sealing secret, which is not distributable. There is no public-key story for a Stele session. Any edge-verifiable artifact must be minted by the engine on top of Stele.

The engine mints no asymmetric signatures today. The cryptography package is installed, so Ed25519 is available, but nothing in the engine imports or uses it. The engine's only asymmetric operation is verifying WebAuthn assertions, where the private key lives on the person's authenticator.

A Cloudflare Pages Function can verify Ed25519 natively, through the Workers WebCrypto implementation, with no library. The marketing project imports no cryptography today and pins no compatibility date, because it commits no Wrangler configuration — bindings are set on the Cloudflare dashboard.

The marketing site and the engine are on different registrable domains. loomworks.doneinseven.com and dunin7.com. No cookie can cross between them; this is a browser rule, not a configuration gap. The session cookie is additionally host-only, so it does not even span subdomains of its own setter.

The engine has no redirect flow of any kind. No OAuth, no single sign-on callback, no magic link. Every authentication endpoint is a JSON POST that returns a body and sets a cookie. The browser is never sent a Location header.

The engine's production cross-origin allowlist is empty by default. No marketing origin is permitted today.

The engine's public reachability is not established in its own repository. No tunnel or deployment configuration is committed. Documents describe an intent to sit behind a Cloudflare tunnel at api.loomworks.dunin7.com. The Operator Layer's production hostname is stated nowhere.

Authorisation in the engine is coarse. require_operator_designation checks a designation from a fixed three-value set on a membership. require_founder compares the caller's identifier against a single stored platform-founder identifier, and fails closed if unset. There is no concept of "may person X use tool Y" for humans. The one per-capability model, verify_companion_authorization, authorises an agent, not a person.

The Operator Layer has no menu. A signed-in Operator lands on a list of engagements. There is no tool list, no launcher, no extensible navigation. A tools menu is a new surface.

Stele Agentic ID has no edge story. It is a separate product with its own registry, sharing no principals table and no session with human Stele. Its verifier returns a verdict — pass or fail with a reason — not a bearer credential. There is nothing an edge function could hold and present.


3. The design

The pass does not travel over a network between the two systems. It travels through the Operator's browser.

That single choice removes most of what section 2 says does not exist. The engine and the marketing site never speak to each other, so no redirect flow is needed, no cross-origin allowlist change is needed, and the engine needs no public reachability beyond what the Operator Layer already requires of it.

3.1 The menu

A new surface in the Operator Layer, reachable when signed in. It lists the tools. For each, it shows what the tool is and offers a way in.

The Operator Layer already authenticates against the engine and already carries the session cookie first-party. The menu adds no authentication of its own — it is behind the same Stele sign-in as everything else in the Operator Layer.

The menu is data-driven from the start. A new tool is a new entry, not a new gate. That is the whole reason this is infrastructure rather than a one-off.

3.2 The mint

A new endpoint in the engine. It requires an authenticated principal — the existing get_current_principal dependency, which resolves the session cookie and enforces that the second factor was completed. It then checks authorisation (section 4) and, on success, mints a short-lived pass.

The pass is a signed statement, not an encrypted one. Ed25519, using the cryptography package already installed. It carries the person's identifier, the tool it is good for, an issued-at time, and an expiry. Thirty minutes.

The engine holds the private key. Nothing else does. The public key is not secret and is distributed freely.

The private key is supplied as a base64-encoded Ed25519 seed, on the existing environment-variable path — the same single-line string mechanism loomworks_secret_key already uses. Verified 2026-07-17: that path carries a seed unchanged. It does not cleanly carry a multi-line PEM — the engine has no PEM normalisation helper and no precedent for multi-line environment values, and the key-rotation list parser splits on commas, which a base64 seed never contains. The seed is not a preference; it is the shape the existing mechanism accepts.

This is the only genuinely new capability in the design. Everything the mint depends on — resolving the session, checking a designation — exists and works today. What does not exist: the engine loads, holds, and signs with no asymmetric key of any kind. Its sole use of the cryptography package is symmetric Fernet. Loading and signing are both new code.

3.3 The carry — a form POST that sets a cookie

The menu asks the engine for a pass. It renders the tool's entry as a form that POSTs to the marketing site, with the pass in a hidden field. The Operator clicks; the browser performs a top-level navigation carrying the pass in the request body.

The pass never appears in an address, a history entry, a bookmark, or a server access log. Request bodies are not logged the way addresses are.

A small new function on the marketing site — the entry point — receives the POST. It verifies the pass (section 3.4). On success it sets an HttpOnly, Secure, SameSite Lax cookie on loomworks.doneinseven.com holding the pass, with the cookie's lifetime matching the pass's own expiry, and returns a redirect to the tool. On failure it refuses and sets nothing.

Verified 2026-07-17: the Workers runtime supports this, and setting a cookie in response to a cross-site POST is allowed — SameSite governs when a cookie is sent, not when it is set. One implementation detail that must not be discovered late: Response.redirect() returns an immutable response whose headers cannot be extended. Setting a cookie and redirecting in one response requires constructing it directly — new Response(null, {status: 302, headers: {Location, 'Set-Cookie'}}). The marketing project has no example of this, because it has never set a cookie or returned a redirect at all.

From that moment the Operator is on the marketing origin with a cookie the page's own JavaScript cannot read — HttpOnly means it is invisible to script. Every subsequent call to /api/sales/* carries it automatically, same-origin.

A cross-site form POST is not subject to the cross-origin allowlist. It is a navigation, not a scripted fetch. This is the mechanism SAML's POST binding has used for twenty years, for exactly this problem.

3.4 The verify

Each tool endpoint reads the pass from the cookie and verifies it before doing any work: signature valid against the public key, not expired, and naming this tool. The entry point performs the same verification before setting the cookie, so a bad pass never becomes a cookie in the first place.

One verification path, used twice. The cookie is only where the pass lives; the pass is the credential.

Failure is a refusal, not a redirect — the tools do not know how to authenticate anyone and should not learn. A refused call says the pass is missing or expired and points the Operator back to the menu.

The public key reaches the Pages Functions as an environment variable, the same mechanism ANTHROPIC_API_KEY uses today. Verification is crypto.subtle.verify with no library.

A tool that cannot verify does not serve. If the public key is absent, the endpoint refuses. This is the one place the design deliberately departs from the existing rate limiter's fail-open posture: a limiter failing open costs money, an authorisation check failing open costs the gate.


4. Authorisation, and the seam it becomes

Version one asks require_founder. It exists, it is fail-closed, and the Operator is the only user. The mint refuses anyone else.

That is deliberately the smallest thing that works. The alternative — inventing a per-tool authorisation model — would be building GRANTHA's job, in the wrong place, against a substrate whose own boundaries are, per its Ratification Ledger, largely reconstructed with four ratified items. That would have to be unpicked.

The seam is clean because GRANTHA's own model makes it clean. GRANTHA's stated posture: "GRANTHA authorizes; it never enforces. Every enforcement point is a local governor that must ask and honor the answer."

The mint endpoint is a local governor. It asks, and it honours the answer by minting or refusing. The pass transports an authorisation decision; it does not make one. When GRANTHA lands, the mint changes what it asks. It does not change what it does, and nothing downstream of it changes at all.

That is why this design does not wait for GRANTHA and does not duplicate it.


5. What this does not do

Agents. Stele Agentic ID returns a verdict from a bounded chain walk against a signed registry statement, not a credential. There is nothing for an edge function to hold. If agents ever need to reach these tools, that is a different mechanism, and it would rest on acts_for — a nullable pointer to a human whose consent and binding model the agentic product's own version two exclusions mark as deliberately undecided. Named as out of scope with a reason, not deferred silently.

Per-tool authorisation. Version one is founder-or-nothing. Which person may use which tool is GRANTHA's question.

The public Example Generator. It is public by design — a stranger describes their work and sees what Loomworks would do. Requiring an identity to view a marketing illustration would ask someone to enrol a passkey before they know what is being sold. There is no identity there to prove because there is no identity. If it ever needs gating, that is a credit question, not an authentication one.

Replacing Cloudflare Access on the record site and the mockups. Settled by Operator direction, 2026-07-17: Access stays where it is. The record site and the Harvest Surge mockups are static documents behind a perimeter — nobody is being authenticated as a person to do work there, and no identity claim is being made about them. The standing Stele direction addresses tools that authenticate a person. Access is the right instrument for a document perimeter and reaching for it there is not the failure this note exists to correct.

A consequence worth naming: the sales perspective generator needs no Access application at all. Change Request 2026-152 section 8 specifies one. Under this design the pass is the gate, and an Access application in front of the same paths would be a second, redundant door using the identity system this note exists to stop reaching for. That section will need amending when this is built.


6. Named limitations

The cookie is a bearer credential for its lifetime. HttpOnly keeps it away from page script, Secure keeps it off plaintext connections, and SameSite Lax keeps it from riding cross-site requests. But anything holding the browser profile holds the pass until it expires. Thirty minutes bounds it.

The two clocks. A pass minted at one moment asserts an authorisation decision made at that moment. If the decision changes inside the pass's lifetime, the pass is stale and the tool does not know.

This is not a new problem and should not get a new answer. GRANTHA has it open under the same name — carried-grant freshness, the two-clocks problem, marked in motion. A thirty-minute lifetime is this note's interim answer. When GRANTHA settles the question, this should adopt that answer rather than keep its own.

Nothing revokes a pass early. There is no server-side store to invalidate — the pass is stateless by design, like the Stele session it descends from. A pass wrongly issued is good until it expires. GRANTHA's clamp lane is the eventual answer; version one has none.

Pass injection is not defended against. Someone able to make the Operator's browser POST a pass of their choosing could seat that pass as the cookie. With a single authorised person and a mint gated on require_founder, an attacker cannot obtain a valid pass to inject. This is recorded as a known gap rather than a solved problem, and it is the thing to revisit first if the authorised set ever grows beyond one.

Key custody is unestablished. The engine holds only symmetric secrets today, through environment configuration. Where an asymmetric private key lives, how it rotates, and what happens when it is compromised are not answered by anything in the repository. Rotation is not designed here.

The path to the engine exists because the Operator Layer is running as a development server. This is the sharpest limitation in the design and it was found, not assumed.

Verified 2026-07-17, by probing what is actually running rather than reading the repository: the engine is reachable from a browser in production — at loomworks-dev.dunin7.com/api/, through a live tunnel to the Operator Layer on port 3001, whose /api/ proxy forwards to the engine on 127.0.0.1:8000. It is session-cookie-gated and has no Cloudflare Access in front of it. An unauthenticated call to /api/me returns a not-authorised response, which proves the route is live.

The design's premise holds. The menu talks to the engine the way the rest of the Operator Layer already does, and that works today.

The dependency underneath it is not one anyone chose. The Operator Layer's proxy configuration rewrites /api/* to the engine only when the environment is development. A production build emits no rewrites. So the publicly-reachable Operator Layer is a development server, and the path this design depends on exists because nobody has built the frontend for production yet.

Building the frontend properly would cut the auth path, and the way out is not small. The documented intended engine host, api.loomworks.dunin7.com, has no tunnel ingress rule at all — it falls through a wildcard to an unrelated service. Wiring it would require opening the engine's cross-origin allowlist, which is empty in production. And the session cookie is host-only, so it would not reach a sibling subdomain unless Stele set a domain on it — an engine change nobody has scoped.

This is recorded, not solved. Whoever eventually productionises the Operator Layer needs to know they would be cutting this design's path to the engine. It belongs in the Change Request as a stated dependency, not discovered later.

Prior position, corrected. v0.1 and v0.2 said "nothing is proven about production hostnames — neither repository pins them, and the deployment shape is an operational fact the code does not settle." That was true of the repositories and wrong as a conclusion. The running system settles it, and the answer is more specific and more fragile than "unproven" suggested.

A related finding, not this design's problem. A development server on the public internet serves unminified source, source maps, and stack-trace overlays. No data is exposed — the engine's endpoints refuse unauthenticated calls — but it is an information-disclosure surface a production build would not have.


7. Why this rather than moving the tools into the engine

Change Request 2026-152 section 1 says the real build goes in the Operator Layer if the thesis holds. It held. Moving the sales generator into the engine would remove the cross-domain problem entirely: no pass, no mint, no entry point, no verify.

Set aside, because the Operator confirmed more edge tools are coming. A FORAY generator is in design, and FORAY already runs a live worker at the edge. Porting each tool into the engine pays the port cost every time and forecloses the edge chain that works today. Building the pass once pays once.

This is a genuine trade, not a free win. Two deployments must stay in step. A tool at the edge cannot consult the engine cheaply. If the edge tools stop multiplying, the port becomes the better answer and this design becomes overhead.


8. Alternatives considered and set aside


9. The carry: what v0.1 proposed, and why it changed

v0.1's position. The pass travels in the address fragment — …/sales-generator/#pass=… — the page reads it on load, clears it with history.replaceState, and holds it in memory. v0.1 recommended this, and recorded copy-and-paste as the reversible alternative if the fragment's history exposure proved uncomfortable.

The reasoning at the time, preserved. Fragments are never sent to a server and never logged, which is why they are the standard answer to this shape of problem. v0.1 was written immediately after an inspection that had just established five things did not exist, and it optimised hard for the smallest possible build — three new pieces rather than four.

Why it was corrected. The fragment design's own analysis named the flaw: an address copied and shared is a working credential shared. Bookmarking, screen-sharing in the second before the strip runs, or simply copying the address all leak a live pass. The thirty-minute lifetime bounds that; it does not remove it.

The deciding argument is what the Operator confirmed about scope: this is the front door to every DUNIN7 tool, not a one-off gate. The FORAY generator inherits whatever is chosen here, and so does everything after it. Saving one piece of build was worth it for a tool used twice; it is not worth it for infrastructure. And the fragment design had already shrunk the build from five pieces to three, which made the fourth affordable.

Copy-and-paste, also recorded in v0.1, is better than the fragment on every security axis and worse on every use axis — two extra actions, every time, forever, plus a paste box in the tool that exists only to work around the transport. The POST-to-cookie is better than both: no address to leak, no manual step, and the pass invisible to page script.

Settled by Operator direction, 2026-07-17: the POST-to-cookie.


10. Decisions

Settled

  1. The carry — the form POST that sets a cookie. Section 3.3. Fragment and paste both recorded in section 9.
  2. Cloudflare Access stays on the record site and the mockups. Section 5.

Open

  1. Naming. "Operator tools menu" is a working name. The thing is a menu, a mint, an entry point, and a verify; the menu is the part the Operator sees.
  2. The pass's lifetime. Thirty minutes is proposed, not derived. It is the interim answer to the two-clocks question until GRANTHA settles it.
  3. Whether the menu lists the public Example Generator. It needs no pass, but the Operator may want it listed for reach rather than for gating. Listing an ungated tool in a gated menu is a small honesty question.

11. What was verified before a Change Request — answered

v0.2 listed five questions and said none could be answered from Claude.ai. Claude Code answered all five on 2026-07-17, against the running system, the Cloudflare dashboard, and source. Four resolve in the design's favour. The fifth found the limitation now recorded in section 6.

  1. Is the engine reachable from a browser in production? Yes — at loomworks-dev.dunin7.com/api/*, through the tunnel to the Operator Layer, whose proxy forwards to the engine. Session-gated, no Cloudflare Access. But only because that Operator Layer is a development server. See section 6. The documented api.loomworks.dunin7.com has no ingress rule and does not reach the engine.
  1. What compatibility date does the marketing Pages project use? 2026-05-10, on both production and preview, no flags. Recent enough for Ed25519 in Workers WebCrypto with no flag and no change. This one settled purely by reading the dashboard.
  1. Where would the signing private key live? The existing environment-variable path carries a base64-encoded seed unchanged. A multi-line PEM does not ride it cleanly. See section 3.2. Custody and rotation remain undesigned — see section 6.
  1. Does anything else consume require_founder? Three routes, all credit-admin — two grant routes and one provisioning route. It is a pure read-only check with no writes, no nonce, and no shared mutable state. A mint endpoint consuming it disturbs nothing; they share the policy, not a resource.
  1. Can a Pages Function set a cookie and redirect to a cross-site form POST? The runtime can; the project does not. The marketing project has no cookie handling, no redirects, and no entry point outside /api/. All of it is new. See section 3.3 for the immutable-response detail.

What remains genuinely unsettleable, and belongs to the Operator:


12. Two findings from the inspection, unrelated to this note

Recorded here because they surfaced during it and should not be lost.


DUNIN7 — Done In Seven LLC — Miami, Florida Loomworks — Operator tools menu and authenticated edge access — Scoping note — v0_3 — 2026-07-17