> ## Documentation Index
> Fetch the complete documentation index at: https://noorle.com/docs/llms.txt
> Use this file to discover all available pages before exploring further.

# Architecture Overview

> Host-based routing across one server, where the agent loop runs relative to every sandbox, and the three execution tiers it calls into

Two things explain most of Noorle's shape: requests are dispatched by **hostname**, and the agent loop runs in the **control plane, outside every sandbox**.

## Host-based routing

One server accepts every request and inspects the `Host` header to decide which service handles it.

```mermaid theme={"dark"}
graph TD
    R["Host header"]
    R --> P["Portal — web UI + JSON BFF"]
    R --> A["api.*<br/>Management API"]
    R --> H["hooks.*<br/>Inbound callbacks"]
    R --> Au["auth.*<br/>Identity boundary"]
    R --> M["mcp-{handle}.*<br/>MCP gateway"]
    R --> Ag["agent-{handle}.*<br/>Agent gateway (A2A)"]
```

| Host                                             | Service           | What it speaks                               |
| ------------------------------------------------ | ----------------- | -------------------------------------------- |
| [`portal.noorle.com`](https://portal.noorle.com) | Portal            | HTTPS — SPA plus a JSON backend-for-frontend |
| `api.noorle.com`                                 | Management API    | REST                                         |
| `hooks.noorle.com`                               | Inbound callbacks | Channel webhooks and workflow-run resumes    |
| `auth.noorle.com`                                | Identity boundary | OIDC-shaped authorization code flow          |
| `mcp-{handle}.noorle.cloud`                      | MCP gateway       | MCP over Streamable HTTP                     |
| `agent-{handle}.noorle.cloud`                    | Agent gateway     | A2A JSON-RPC, and AG-UI over SSE             |

The two gateway hosts are the only wildcard matches. The match requires a dot before the base domain, so a lookalike apex cannot slip into gateway dispatch.

<Note>
  The Portal is the web UI — the SPA and the JSON backend that serves it are the same host.
</Note>

## Where the agent loop runs

This is the load-bearing architectural fact, and the one most often misread.

**The control plane is the agent loop.** Model routing, memory, tool dispatch, the journal, autonomy decisions, scheduling, multi-agent coordination — all of it runs in the host process, outside every execution sandbox.

An agent has **no path back into the control plane**. It cannot reconfigure itself, escalate its own permissions, or reach another tenant's resources, because it is not running anywhere that could. It requests capabilities *through* the loop, and the loop decides.

Execution happens in tiers the loop calls into. None of them is "the runtime"; the loop picks per workload.

```mermaid theme={"dark"}
graph TD
    CP["Control plane<br/>agent loop, memory, autonomy, journal"]

    CP -->|"code_runner, plugins"| W["WebAssembly<br/>strict CPU + memory caps<br/>capability-based filesystem"]
    CP -->|"session work"| S["Sandbox<br/>ephemeral container<br/>created and destroyed with the session"]
    CP -->|"persistent work"| C["Computer<br/>one machine per agent<br/>alive across turns"]
```

| Tier            | Lifetime    | Bound to                                       |
| --------------- | ----------- | ---------------------------------------------- |
| **WebAssembly** | Per call    | Nothing — fresh instance, strict caps          |
| **Sandbox**     | The session | One session; torn down with it                 |
| **Computer**    | Long-lived  | One agent; shared across that agent's sessions |

A broker daemon runs *on* a Computer, but it is only a broker: it executes what the control plane sends it. It is not the agent loop.

## Request path for a tool call

Every capability call, from every surface, converges on the same path.

```mermaid theme={"dark"}
graph TD
    In["Request<br/>MCP gateway / agent / workflow"]
    In --> Auth["Authenticate<br/>API key, JWT, or session"]
    Auth --> Ctx["Resolve context<br/>account, principal, surface"]
    Ctx --> List["Assemble the tool surface<br/>exposure scope + workspace anchoring"]
    List --> Call["Tool call"]
    Call --> Door["Admission door"]
    Door --> HL["1. Hardline floor (code, no I/O)"]
    HL --> Deny["2. Account deny ledger"]
    Deny --> Res["3. Recorded human decision"]
    Res --> Pol["4. Invocation policy"]
    Pol --> Tree["5. Execution policy"]
    Tree --> Out{"Outcome"}
    Out -->|allow| Ex["Execute in the right tier"]
    Out -->|pause| Wait["Wait for a human — interactive surfaces only"]
    Out -->|deny| Err["Refuse"]
    Ex --> J["Append to the journal"]
    Wait --> J
    Err --> J
```

Two properties worth stating plainly:

**Executing a capability requires a witness the admission door is the only thing that can produce.** A dispatch path that skipped the door would not compile. This is enforced by the type system, not by discipline.

**A policy read failure denies.** There is no last-known-good fallback for invocation policy, deliberately — a fallback would be a back door to failing open.

## Data

| Store              | Role                                                                                                  |
| ------------------ | ----------------------------------------------------------------------------------------------------- |
| **Postgres**       | Accounts, agents, gateways, capabilities, threads, workflows, curated memory                          |
| **Object storage** | Plugin artifacts, files, thread summaries, knowledge documents                                        |
| **Redis**          | Caches, locks, leases, counters, rate limits, idempotency, and short-lived session and protocol state |
| **SlateDB**        | The journal. **Sole authority** for run and message history                                           |
| **ClickHouse**     | A rebuildable *projection* of the journal, for query and analytics                                    |
| **Qdrant**         | Vector index for knowledge bases and curated memory recall                                            |

The journal split matters. **SlateDB is authoritative; ClickHouse is derived.** A ClickHouse outage degrades query surfaces and returns a typed error — it does not stop the agent loop, because strict context reads never touch it. ClickHouse can be truncated and re-derived from the journal.

Likewise Qdrant is a projection: Postgres holds curated memory authoritatively, and recall falls back to a keyword search when the vector index is unavailable.

## Multi-tenancy

Every resource carries an account id, and every repository query is floored on it. Authority is evaluated above that floor by a dedicated layer that sits between the domain and the runtime — handlers do not decide authorization themselves; they build an identity context and call a management service that re-reads role and grants per operation.

An authority denial is a 403. A row that does not exist under the tenancy floor is a 404 — validation runs only after authority, so a denied caller cannot probe for what exists.

## Identity

Identity is issued by a separate **auth server** at `auth.noorle.com`, with its own database that no product code reads. It is the sole issuer for every principal type: humans at first login, and service and agent principals on request.

Both databases use the same UUID for the same identity — there is no mapping table.

API keys are **not** boundary-issued; they are platform-local. If the identity boundary is unavailable, sign-in fails, but API-key verification is unaffected.

See [Authentication](/docs/learn/auth/overview).

## What this design buys you

**Capability escape does not become platform escape.** The most dangerous code — your plugin, an agent's shell command — runs furthest from the decision-making.

**Autonomy policy cannot be prompted away.** The gate is not an instruction to the model; it is a code path the call has to survive.

**History survives infrastructure loss.** The conversation rebuilds from an object-store-backed journal, not from a cache and not from the analytics store.

***

Next: [Use cases](/docs/learn/use-cases) for shapes that compose well out of this.
