Agents & persona packs
An agent is a directory. Not a process, not a container, not a row in a table.
.nouride/agents/nouva/├── IDENTITY.md # required — name, role, emoji, bio├── SOUL.md # personality, behaviour rules, tone├── USER.md # who this agent works with├── AGENTS.md # the other agents: who they are, how to reach them├── MEMORY.md # persistent notes — the agent writes this itself├── config.toml # model, channel bindings, tools, workspace, MCP servers└── skills.toml # which pooled skills this one has switched offIDENTITY.md is the only required file. Everything else has a sensible default, and an agent
with nothing but an identity works.
mkdir -p .nouride/agents/nouvaprintf '# IDENTITY\n- Name: Nouva\n' > .nouride/agents/nouva/IDENTITY.mdPacks hot-reload. /reload in a chat, nouride agent reload <id>, or the dashboard’s Files tab —
no restart.
What each file is for
IDENTITY.md — who this is. Name, role, emoji, a line of biography. Short.
# IDENTITY
- **Name:** Nouva- **Role:** Engineering assistant- **Emoji:** 🛠️- **Bio:** Works on the Nouverse stack. Answers in the language it was asked in.SOUL.md — how it behaves. Tone, standing rules, what to do at the edges. This is the file worth
spending time on.
# SOUL
You are Nouva. Keep answers short and factual.
- Say plainly when you do not know something.- Never invent a tool result.- When a request needs a capability you do not have, say which one is missing.USER.md — who it works with. Family, colleagues, what each person goes by, what they care
about. This is where a fact goes if it must survive /new.
AGENTS.md — the other agents on this daemon, and how to reach them.
MEMORY.md — the agent’s own notes, written by its memory_write tool. This is the only thing
in an installation that cannot be rebuilt. It is not in git, and it accumulates. The daemon keeps
dated copies under data/backups/agents/ for that reason.
Isolation is by construction
An agent loads only its own directory. What one agent knows about another comes from its own
AGENTS.md and from messages on the bus — never from reading another agent’s files.
There is no permission check making that true. It is true because nothing in the loader looks anywhere else.
config.toml
Everything omitted falls back to the daemon defaults.
[agent]name = "nouva"display_name = "Nouva"emoji = "🛠️"enabled = true
# `provider` names a [providers.*] profile in the daemon config. That profile owns the endpoint,# the wire format and the credential — so moving this agent to a different deployment is one word.[llm]provider = "nougate-anthropic"model = "claude-sonnet-4-20250514"max_tokens = 8192temperature = 0.7
# Chats this agent answers in, keyed by GATEWAY INSTANCE id, because two bots on one platform are# two different audiences. Empty means it answers only when addressed by name — or always, if a# gateway instance names it as its owner.[channels]"discord:main" = ["guild:123:channel:456"]"telegram:nouva" = ["-1001234567"]
[tools]enabled = ["read", "web_search", "web_fetch", "memory_read", "memory_write", "person_record", "send_file"]approval_required = ["exec", "write", "edit"]blocked = ["rm", "shutdown", "reboot"]
# Directories this agent's file and exec tools may reach, beyond its own pack.# Empty means its own directory only — safe, but it cannot touch a project.[workspace]roots = ["/home/you/projects/thing"]skill_roots = ["/home/you/projects/thing"]
[security]sender_allowlist = []exec_secrets = []roots and skill_roots are deliberately different lists
A root is somewhere the agent may read and write. A skill root is somewhere it accepts instructions from, and those land in the system prompt — the most trusted position in the request.
Auto-loading skills from everything reachable would make a pull request into a prompt-injection vector. A repository you cloned yesterday should be the first without being the second.
Project skills are read-only to the agent, and a pack skill of the same name wins.
Skills are not configured here
There is one pool at agents/skills/, and every agent gets all of it. Exceptions go in skills.toml
beside the pack:
[skills]document-authoring = falseDefault-on rather than an opt-in list, because a list written today cannot name a skill that arrives next month. Skills →
MCP servers
External tools, declared per agent. Declaring a server is the whole opt-in — its tools are discovered
on connect and offered as <server>__<tool>, because the names come from the server and cannot be
listed in [tools] enabled before you have connected once.
[[mcp_servers]]name = "nouva-mcp"transport = "stdio"command = "python3"args = ["/path/to/server.py"]env = { MEMORY_DIR = "/data/memory" }
[[mcp_servers]]name = "browser"transport = "streamable-http"url = "https://mcp.example.com/mcp"auth_secret = "example-mcp-token" # the NAME of a secret, never the valueheaders = { "X-Client" = "nouride" }All three transports work: stdio starts a process here; streamable-http and sse dial a remote
endpoint (sse is the older shape — prefer streamable-http unless the server only speaks sse).
An MCP tool runs without asking, exactly like a built-in nobody listed. To gate one, name it in
approval_required by its qualified name.
Its results and its tool descriptions are marked as untrusted before the model sees them: both are text written by somebody who is not you. Marking is not gating — a server you would not run a command from is a server you should not connect.
For stdio, the child gets a minimal environment — PATH, HOME, locale, TMPDIR,
SSH_AUTH_SOCK — not the daemon’s whole one. Pass anything else through env.
Changes take effect on the next restart, because clients are built at boot.
nouride mcp # servers and the tools they offernouride mcp add --agent nouva --name browser --url https://mcp.example.com/mcp --token-stdinnouride mcp rm --agent nouva --name browserWhere an agent works
.nouride/workspace/ shared/ every agent. This is where a relative path resolves private/<id>/ one agent, and no other can read itshared/ is the working directory, and that is what makes handover possible. Two agents in one
channel delegate by tagging; with a workspace per agent, the build being handed over sat outside
every directory the second agent was allowed to open, so the sandbox refused it however the path was
spelled. The mention could carry an instruction and never the work — and nothing reported that as a
failure. The second agent simply answered that the file was not there.
Concurrent writes are not a concern in that flow: the queue is keyed by conversation and deliberately excludes the agent, so two agents in one chat take turns.
private/<id>/ is for drafts and scratch. Anything another agent may need to pick up does not belong
there, and each agent’s prompt says exactly that.
The persona pack is not a workspace. Work used to land in agents/<id>/ — observed on a real
run, with an operator’s project built next to IDENTITY.md in a git-tracked directory. That is why
the two are separate paths, and why workspace_dir can point at another disk without moving anything
else.
Running agents
nouride agents # list, with statenouride agent pause <id> # until the next restartnouride agent resume <id>nouride agent reload <id> # re-read the pack from disknouride agent enable <id> # written to config.toml, so it survives a restartnouride agent disable <id>Pause and resume are runtime state. Enable and disable are written to the config file. The distinction matters at 2am: pausing something is not the same as deciding it should not come back.
The harness block
An agent learns its own capabilities from a harness block generated fresh into its system prompt each turn, from live state: which tools it has, whether a human can be asked right now, its round-trip budget, and the commands a person in this chat can type.
That is why an agent knows not to promise something it cannot do, and why the answer changes when you
switch a tool off. /prompt in a chat shows the whole thing.