Skip to main content
loomcycle
§ milestone · pr #1000 · loomcycle.cloud

One thousand PRs later: loomcycle.cloud is live.

A thousand pull requests is a marker, not a destination. The actual milestone is smaller and more useful: loomcycle now has a public deployment anyone can reach, sign into, and drive with tokens minted for them by their tenant. The runtime that has been shipping every week for over a year is publicly reachable at loomcycle.cloud. The loomboard chat app that goes with it is at board.loomcycle.cloud. The runtime itself is at app.loomcycle.cloud. Same binary, same code, same license. Apache-2.0. Nothing hosted-only.

1,000pull requests merged
14substrate primitives shipped
5transports (HTTP, gRPC, MCP, TS, Python)
3public subdomains live today

This is a capability tour. Not a release digest and not a retrospective. What loomcycle.cloud makes reachable, how the user system works underneath it, and what a thousand PRs bought.

What's public today. loomcycle.cloud is the landing plus a browser-local token vault that unlocks and hands the bearer to the runtime through an origin-checked postMessage. app.loomcycle.cloud is the runtime itself: the Web UI, the HTTP plane, the operator console. board.loomcycle.cloud is the hosted loomboard chat app, a standalone SPA that reverse-proxies /v1 to the runtime on the same tailscale netns so the app never needs CORS. A tenant operator mints per-user tokens from a Web UI console. Tokens come in two access modes: isolated (scope-locked to the user's own data) and member (whole-tenant HTTP plane, minus operator carve-outs). The sign-in handoff keeps bearers out of URLs by opening the runtime tab synchronously in the click gesture, then delivering the bearer over postMessage, then exchanging it for an HttpOnly session cookie via same-origin POST /ui/session. Everything the tenant produces (agents, skills, documents, memory, credentials, budgets) stays private to that tenant by construction.

The three subdomains, and what they do

Loomcycle.cloud is not one service. It is three related surfaces that each do one job and hand off cleanly to each other.

HostWhat it isRuns
loomcycle.cloud The public landing. Plus a browser-local token vault (AES-GCM under a passphrase) that stores your bearer between visits and hands it to the runtime without ever putting it in a URL. A static site plus a tiny signed-URL handoff.
app.loomcycle.cloud The runtime. The Web UI, the HTTP plane, the operator console, the memory view, the routing view. Everything the loomcycle binary already served. The same denngubsky/loomcycle Docker image self-hosters run. No cloud-specific fork.
board.loomcycle.cloud The loomboard SPA hosted for people who want to chat with their agents from a browser instead of the desktop app. A standalone build of loomboard served by a small Node server that reverse-proxies /v1 to the runtime. Same-origin from the browser's point of view; loomcycle emits no CORS.

The three subdomains share a tailscale netns, so the runtime does not have to be publicly reachable from the loomboard SPA or the vault. Only the front door is public. The runtime hears every call at the same tenant boundary self-hosted deployments enforce, and cross-tenant access requires an admin scope no user token has.

Users are a tenant's own object

A user on loomcycle is not a shared identity across the deployment. Every user belongs to exactly one tenant. The tenant creates the user, mints the user's tokens, sets the user's access mode, and can revoke or delete the user at any time. The runtime never sees a "user" without also seeing the tenant it belongs to.

What that gets you: two tenants can have a user named alice, and the two Alices are different people with different data. No global namespace. No collision.

Users live in a users table the tenant owns. The operator surface for it is small:

The tenant is server-derived from the bearer, so none of these routes carry a tenant field on the wire. An operator cannot accidentally create a user in someone else's tenant.

A Users console in the Web UI wraps the four operations. A TypeScript adapter method wraps them for programmatic use: createUser, updateUser, deleteUser, plus mintUserToken, listUserTokens, and revokeUserToken. @loomcycle/client at 1.50.0 exposes all six.

Two access modes, one honest boundary

Every user token carries an access mode. There are two.

Isolated

An isolated user is confined to its own data scope. Its runs can reach its own memory, its own documents, its own credentials, its own chat history. It cannot see anything at the tenant scope: not the tenant's shared agents, not the tenant's shared documents, not another user's memory.

The isolation is enforced by a runtime bit (RunIdentity.Isolated) stamped at every run-start site the token can reach: HTTP, gRPC, and the MCP-direct path. Widening one route gate does not silently widen the confinement, because the confinement is stamped from the token's own scopes rather than derived from where the request came in.

Discovery is tiered too. An isolated user calling GET /v1/_runnable-agents sees only bundled and system agents, plus its own user-scoped agents when it has any. It never sees the tenant's shared agents. The tier is read from the token, not from a mutable column, so a stale access_mode row can never leak a tenant agent to an isolated caller.

This is the shape you want when your user is an end-customer of your platform. They get an agent, they get their own memory, they get their own chats. They never see anyone else's.

Member

A member is a non-isolated user token. It is a full participant in its tenant. It can read the tenant's shared Library, browse the tenant's Documents, run the tenant's shared agents, use tenant-scoped credentials, read tenant-scoped memory. It cannot mint other users' tokens, cannot delete another user, cannot rewrite tool-use hooks, cannot write budgets. Those stay operator-only.

Members are the shape you want when your user is a team member on the tenant. A developer, an editor, an analyst. They should see the whole workshop, not a private closet.

Until this week, a member's HTTP surface was narrower than its data surface. A member could reach tenant data through a run, but hitting GET /v1/_documents or GET /v1/_memory/search directly from a browser returned 403 at the route gate. The Web UI hid those pages for members rather than render a wall of 403s.

RFC CB, shipped this week, closes that. A non-isolated user token now reads and writes the tenant HTTP plane directly, minus the operator carve-outs. The change is in one place (the auth middleware admits non-isolated principals on member-accessible tenant routes) and it uses the existing isolation boundary. No new scope, no re-mint, no schema change. Existing tokens gain the access they always had inside a run, now on the wire outside one.

Minting a token: what happens

An operator picks a user, picks an access mode, gives it a label, and clicks Mint. The runtime generates a fresh bearer, stores its hash server-side, sets its metadata (subject, tenant, access mode, label, expiry, revoked_at). The plaintext bearer comes back exactly once. The operator hands it to the user through whatever channel their organization uses.

From the user's side: they land on the vault at loomcycle.cloud, unlock the vault, paste the bearer once, and never see it again. The vault holds it as AES-GCM ciphertext under a browser-local key, and hands it to the runtime when they click Sign in. The runtime accepts the handoff, exchanges the bearer for an HttpOnly session cookie, and the user is inside. The bearer never touches an address bar, a browser history entry, or the runtime's HTTP access log.

The sign-in handoff: no tokens in URLs

The obvious way to sign in from a landing page is window.open("/ui?token=..."). It works, and it is what loomcycle did for a while. It is also wrong in two ways: the token ends up in the browser history and in every access log along the way, and any javascript running on either origin can read it out of the URL.

The current handoff avoids both. Three steps.

  1. The user clicks Sign in. The vault opens the runtime tab synchronously in the click gesture, before any async work. Browsers only allow one window.open per user gesture and only if the code has not await-ed anything in between; opening the tab first satisfies that rule.
  2. The tab loads /ui. The runtime's login view listens for a postMessage with a specific type and an origin check against a small allowlist (LOOMCYCLE_UI_LOGIN_ORIGINS, set to https://loomcycle.cloud for the public deployment). The vault sends the bearer as {type: "loomcycle.login", token: "..."} to the opened window.
  3. The login view exchanges the bearer for an HttpOnly session cookie via same-origin POST /ui/session, replies with {type: "loomcycle.login.ok"}, and the vault closes the popup on success.

On the happy path the token never appears in a URL, never appears in history, never appears in an access log. On the sad path (popup blocked, or no ack within eight seconds) the flow falls back to /ui?token= in the same tab. The runtime then 302s the token out of the URL as soon as it exchanges the bearer, so even the degraded path stays clean after the first request.

Two other properties fell out of this design. The vault does not need the runtime to be same-origin; the sign-in tab handles the exchange from its side. And the runtime does not need to trust the vault's origin blindly; the login-view's allowlist decides which senders are accepted, keyed to the deployment's config, not to something the caller can spoof.

Loomboard, hosted for the same handoff

Loomboard is the chat app for people who want to talk to their agents from a browser. It ships as a Tauri desktop app, an npm CLI (@loomboard/app), a React component (@loomboard/chat), and now a hosted SPA at board.loomcycle.cloud.

The vault has a "loomboard →" action next to the "Sign in →" action. It opens board.loomcycle.cloud with the same postMessage shape ({type: "loomboard.connect", baseUrl: "", token: "..."}), the loomboard receiver validates the sender origin against a build-time allowlist, and the SPA auto-connects. No repaste. No token in the URL. Same guarantees as the runtime handoff, keyed to a different receiver.

Loomboard's Node server also reverse-proxies /v1 to the runtime on the same tailscale netns, so the SPA appears to talk to the runtime same-origin from the browser's point of view. Loomcycle emits no CORS headers by design, and the reverse proxy makes that a non-problem.

What a thousand PRs bought

This is the compressed picture. The full arc lives on the blog index; here it fits in a short table.

Substrate primitives (14, all content-addressed, versioned, tenant-scoped)

AgentDef, SkillDef, TeamDef, VolumeDef, ChannelDef, CredentialDef, ScheduleDef, MCPServerDef, WebhookDef, MemoryBackendDef, A2AServerCardDef, A2AAgentDef, plus the non-def surfaces Document, Path, History, Memory (with SQL Memory + vector Memory facets), and Users. Every one has a create/fork/get/list/retire/promote/verify shape; content is content-hashed; lineage is tracked.

Transports (5, uniform)

HTTP (the primary), gRPC, MCP (stdio + HTTP; loomcycle is both an MCP client and an MCP server), TypeScript via @loomcycle/client, Python via pip install loomcycle. Every substrate op lands on every transport; the wire shape is one op-discriminated envelope; adapter methods are thin wrappers. gRPC and HTTP share extracted connector cores for the ops that need identical behavior on both wires.

Agentic memory

Facts, notes, and documents in one per-scope memory. A background consolidator turns finished chat transcripts into distilled facts with provenance and bi-temporal validity. Hybrid vector plus full-text retrieval with Reciprocal Rank Fusion. Honest source selectors on the search API. Every write carries a provenance envelope (tenant, session, run, agent, model, compaction generation). Every read is tenant-folded server-side. A human-facing operator console (@loomcycle/memory-view) renders it all.

Documents as searchable knowledge

Prose bodies embed on write, Mermaid diagrams extract labels across ten dialects, images use captions plus persisted vision descriptions. Inline [[name]] links become graph edges; ![[target]] transcludes at export. Backlinks, related, unlinked mentions in one call each. Per-chunk history and diff. JSON Canvas round-trip. Tags and per-document type/status as first-class query axes.

Agent teams

TeamDef is a state-machine graph over agents. States, transitions (success, pushback, conditional), and per-state handlers (agent, parallel plus consolidator, terminal). Two drivers run the same graph: a deterministic Go walk engine and a bundled LLM team-orchestrator agent. Web UI teams board with a live Mermaid preview.

Safe code execution

A distroless default image plus two opt-in answers. A loomcycle-toolbox image with a preloaded dev toolchain for single-tenant trusted deployments. An isolated builder sidecar with per-session containers, --network none, --read-only, tmpfs /work, every Linux capability dropped, and a pluggable runc / runsc / kata runtime. Loomcycle drives the sidecar over HTTP-MCP; it never runs a container engine itself.

Trust and governance

Per-tenant encrypted credentials (RFC AR) with a per-tenant DEK derived from a deployment KEK. Per-scope token budgets with soft-warn and hard-cap tiers (RFC AW). Per-call cost attribution ledger with a credential_source label (RFC AV). Data retention subsystem that ages out retired def versions, aged chat sessions, and dead-agent memory with export-then-delete guarantees (RFC BM). Subject erasure on every transport with a durable residue receipt (RFC BC). Every plane tenant-folded on every read.

Operator experience

A Web UI that renders every substrate primitive as a browsable list. A memory-view console for facts, notes, and search. A teams board for state machines. An ontology panel for the tenant taxonomy. A routing view for the LLM provider cascade. A directory surface for users, tenants, and subject inspection. Everything a tenant operator can do on the console is also an HTTP endpoint, a gRPC method, a Python call, a TypeScript call, or an MCP tool.

What loomcycle.cloud is not

A hosted-only service. The same Docker image self-hosters run is the one running on app.loomcycle.cloud. The same Web UI is embedded. The same tenant boundary is enforced. The cloud deployment is a reference, not a fork. If you would rather run loomcycle on your own hardware, in your own cluster, on your own TrueNAS, the code you would run is identical to what runs at loomcycle.cloud.

What loomcycle.cloud adds are three things a self-hoster does not need out of the box: a landing page with a token vault, a small handoff protocol so the vault can sign the user in without touching URLs, and a hosted loomboard SPA for people who want to chat with agents from a browser. All three are in the same open-source repos as the rest.

What's next

Three lines are queued.

Programmatic tenant onboarding. Right now a tenant is created by an operator with an admin bearer. A self-serve tenant signup (email verification, tenant naming, first-user creation, first-token minting) is the natural next step for a public deployment, and it composes with the primitives already there rather than requiring new ones.

MCP and gRPC parity for the user administration surface. User CRUD and token minting exist on HTTP and on the TypeScript adapter. The MCP tool and the gRPC RPC come next; the shape is small enough that both fit into one release.

The RFC CC memory-provenance line. PR #1000 opened today with the first phase of a memory feature that records the source span every fact came from, so a downstream reader can verify whether the source actually supported the claim. The span is derived deterministically from the transcript the consolidator already has, which keeps the extractor prompt byte-identical to what has been shipping (its eval baseline stays green). Phase two adds a deterministic gate that checks the derived span for structural properties before a fact is written. Phase four backfills existing facts by re-running the same span matcher against their source transcripts.

Companion reading: agentic memory for the whole memory system; data retention for the retention subsystem; the Document viewer for the reading surface; agent teams for the multi-agent workflow primitive; warm containers, cloneable agents, resident sub-agents for the sandbox + cloning arc. Docs: docs/CUSTOMIZING_AGENTS.md, docs/DOCUMENTS.md.