Overview & design
Nouride is one long-lived process. Everything below happens inside it — agents, gateways, the tool loop, the scheduler, the HTTP server and the dashboard it serves.
The path a message takes
Discord WS Telegram poll WhatsApp bridge (IPC) dashboard / CLI │ │ │ │ └───────────────── gateway adapters ───────┴───────────────────────┘ │ a normalised envelope ▼ access gate ── is this sender allowed to reach an agent at all? │ ▼ router ── mention? gateway owner? channel binding? default agent? │ ▼ queue ── ordered per (agent, chat), parallel across them │ ▼ hooks ── is it a slash command? answer it and stop │ no ▼ agent registry ── load the persona pack, resolve its tools and skills │ ▼ prompt → context → executor ──────────┐ ▲ │ the model asked for a tool │ ▼ │ tool registry → approval gate → run it └──────── tool results ┘ │ final text ▼ gateway.send()The access gate runs first, before routing and before anything is queued. A stranger’s message does not reach an agent, does not cost a model call, and does not appear in a conversation.
The queue is keyed by conversation, not by agent. Two agents in one chat take turns; ten chats are answered in parallel. That is also why memory scales with concurrent turns rather than with agents — ten busy channels is ten contexts assembled at once, whether one agent or ten are involved.
Three side channels bypass parts of this, all SQLite-backed so they survive a restart:
- The scheduler runs a due job and delivers the result itself, rather than through the queue — a scheduled job’s output may belong in another chat, at a webhook, or nowhere, and has to come back as text either way for the run record.
- The agent bus delivers inter-agent messages as system events in the target’s context.
- The approval gate parks a tool call until a human answers, in chat or on the dashboard.
The repository
nouride/├── apps/│ ├── engine/ the daemon — the only long-lived process│ └── dashboard/ React SPA, built to static assets the engine serves├── packages/│ ├── shared/ types, the message envelope, logger, errors, ids│ ├── runtime/ the ONLY place Bun.* and node:* are used│ ├── llm/ OpenAI + Anthropic clients, SSE, tokens, cost│ ├── queue/ keyed work queue: ordered per key, parallel across keys│ ├── persona/ agent pack loader — Markdown plus config.toml│ ├── secrets/ named credentials: environment, or a writable store│ ├── policy/ the command classifier behind the approval gate│ ├── mcp/ Model Context Protocol client│ └── gateways/ Discord / Telegram / WhatsApp adapters├── plugins/│ ├── wa-bridge/ the WhatsApp bridge — a submodule, GPL-3.0│ └── nougate/ the LLM gateway — a separate service, consumed over HTTP└── content/ what a human edits: the agent template, the shipped skillsFour rules the code actually holds to
These are enforced by bun run lint:check, not by convention.
1. Bun.*, bun:* and node:* appear only in packages/runtime. Everything else imports
@nouride/runtime. Swapping runtimes means rewriting the adapter files, not grepping the whole tree —
and it is what makes the business logic testable without a process, a socket or a disk. The rule is
enforced on the module specifier in every form, so from "path" is the same violation as
from "node:path".
2. packages/* never imports from apps/*. A library that reaches back into the daemon is not a
library. The test for what becomes a package is one question: could a second tool use this without
dragging the daemon along? Speaking the Anthropic wire format, ordering work per chat, reading a
persona directory — yes. The tool loop, the router and the approval gate — no, they only mean
something in terms of an agent registry and a session store.
3. SQL lives in a *.store.ts file, and everything else takes a store. Enforced from both ends:
one check refuses a database handle outside a store, another refuses a query outside one. It is a
convention rather than a list, which is the point — find apps packages -name '*.store.ts' is the
complete inventory of what a move to another database would have to rewrite.
4. The dashboard never imports engine code. It speaks HTTP to the control API like any other client, which is why the same API serves the CLI.
And one that is not enforceable but is load-bearing: config holds secret names, never values.
api_key = "nougate" means “the secret called nougate”, resolved at use from the environment or the
secret store. That is why a config.toml is safe to commit, print, and render in the dashboard.
Two processes, sometimes three
The daemon is one process. Two things can sit beside it:
| The WhatsApp bridge | Its own executable, spoken to over newline-delimited JSON. It is a separate process because Baileys is GPL-3.0 and process isolation is where that boundary sits |
| The LLM gateway (nougate) | A separate service reached over HTTP, like any other provider endpoint |
Either can instead be compiled into the binary. The reason to is measured: a second process pays for a second Bun runtime — about 36 MB of shared pages plus 14.5 MB of private heap — while a second listener or a second module pays neither. The reason not to is one crash domain: a bug in a WhatsApp reconnect is then one stack frame from the agent loop.
Bundling the bridge additionally makes the binary GPL-3.0. Bundling the gateway does not.
Why one daemon at all
The alternative — a container per agent, or a microservice per capability — buys isolation nobody asked for and pays for it in memory, in operational surface and in the thing that actually matters here: an agent is a directory of Markdown, not a program. Assembling a prompt does not need a process.
The numbers back it up. Six agents cost 2.1 MB more than one. The heap is flat in the number of agents because the prompt is assembled per turn and not held resident.
Where the boundaries actually are
Worth being clear about, because the approval gate is easy to mistake for one.
| The service account | The daemon does not run as root and nouride service install refuses to generate a unit that does |
| The unit hardening | ProtectSystem=strict, ProtectHome=read-only, PrivateTmp, NoNewPrivileges, RestrictSUIDSGID |
| Workspace roots | An agent’s file and exec tools reach its own directory and nothing else, unless you list more |
| The access gate | Who may reach an agent at all |
| The command classifier | Which commands ask before running — not a security boundary. A determined model routes around any classifier |
The classifier exists so that ordinary work does not surprise you. The first four are what hold.