A Smart Node is one machine. On it run a set of named services, one Postgres cluster, and a registry that says what exists. Everything else, including every agent, is a row in that registry. None of it is set up by hand.
That sentence is the whole design, and the rest of this page is what it means in practice.
The entities
The system knows a fixed number of kinds of thing. Each kind has exactly one home. Not "usually lives here" but "exists only here". Adding an agent means adding a row, and the directory, the memory space, the group membership and the permissions all follow from that row. None of them is a separate act of setup.
| Entity | Declared in | Also materialised as |
|---|---|---|
| Agent | sn_config.agents | A directory under /srv/agents/, a memory space, a Unix group |
| Channel | sn_config.channels, agent_channels | A binding between an address and an agent |
| Tool | sn_config.tools, granted via agent_tools | An executable body plus its usage text in the prompt |
| Policy | sn_config.policies | Text composed into the prompts of agents in that category |
| Person | sn_config.users, user_channels | The identity a message is resolved against |
| Message | sn_core.messages | An append-only bus row plus its delivery state |
| Event | sn_core.events | An append-only audit record |
| Document | the node's document store | A file under version control |
| Task | the node's planner |
This is the difference between a system and a wrapper around a model call. A wrapper has no entities. It has a prompt and a key.
The perimeter
Everything above sits on the customer's machine, inside the dashed boundary in the diagram. Mail is polled over IMAP, Telegram is long-polled, Slack runs over an outbound socket: every channel dials out, so none of them needs a port opened toward the node. What you publish beyond that is a firewall decision on your own machine. Details on install.
What leaves: the model call, and whatever the channels you enabled must reach in order to send.
What happens to one message
There is not one path. There are four, and which one runs is decided by the channel the message arrived on. The commonest is the wake loop, which handles mail, Slack and internal traffic. The numbers below are the numbered badges in the diagram.
- An adapter polls the channel and writes the message into
sn_core.messages. Arrival and handling are separate steps. - The recipient is resolved by matching
recipientagainst the channel bindings inagent_channels. Internal traffic matches an agent's slug directly. No routing table, no routing column: the answer is a query. - The dispatcher claims that agent and wakes it. The claim fails closed, so two wakes cannot run at once.
- The prompt is composed from the registry: the agent's own definition, the usage text of each tool it holds, the rules for its categories. Not read from a file a person maintains.
- The model call goes out through the gateway.
- The reply is written back as an outbound row and a timer sends it. Every outgoing message exists as a record before it exists as an email.
The other three paths: a fast path for chat channels where latency matters, a synchronous path for web and panel chat, and a flow path where the runner drives a sequence of steps. They differ in how the agent is invoked, not in where anything is stored.
Where things are stored
One Postgres cluster. The three schemas below carry everything this page has described; a fourth holds extensions. The document store and the planner are separate databases on the same cluster.
| Schema | Holds |
|---|---|
sn_config | The registry: agents, channels, tools, policies, people, integrations |
sn_core | The domain: messages, delivery state, events, model sessions |
public | Login |
sn_core.messages and sn_core.events are append-only, enforced by a database trigger rather than by convention. Grants, policy changes and permission changes are written into the same event log as everything else, so the record of what an agent was allowed to do sits next to the record of what it did.