Skip to content

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 off

IDENTITY.md is the only required file. Everything else has a sensible default, and an agent with nothing but an identity works.

Terminal window
mkdir -p .nouride/agents/nouva
printf '# IDENTITY\n- Name: Nouva\n' > .nouride/agents/nouva/IDENTITY.md

Packs 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 = 8192
temperature = 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 = false

Default-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 value
headers = { "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.

Terminal window
nouride mcp # servers and the tools they offer
nouride mcp add --agent nouva --name browser --url https://mcp.example.com/mcp --token-stdin
nouride mcp rm --agent nouva --name browser

Where an agent works

.nouride/workspace/
shared/ every agent. This is where a relative path resolves
private/<id>/ one agent, and no other can read it

shared/ 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

Terminal window
nouride agents # list, with state
nouride agent pause <id> # until the next restart
nouride agent resume <id>
nouride agent reload <id> # re-read the pack from disk
nouride agent enable <id> # written to config.toml, so it survives a restart
nouride 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.