Multi-tenant agent platform architecture, security model, coordination patterns, and skill development
Folders are agents. Each folder is a tenant: isolated memory, persona, skills, routes. MCP over unix socket. Tool calls go through `gated`; host controls what agents can reach. Defense-in-depth. Container isolation + DNS filtering + per-group MCP sockets + the host-side secret broker.
Each folder is a tenant. A folder path is a routing target, a JID prefix, a
container home and a web vhost — it carries no authority. Authority is a row
in the acl table:
| Principal | Holds | Authority |
|---|---|---|
role:member |
The twelve messaging verbs | Bound to every folder at creation. Reply, send, post, like, edit and the rest — nothing more. |
role:operator |
* on **, WITH GRANT OPTION |
The root of every delegation chain. Held by a person, invoked with /root. |
folder:world/team |
Whatever was delegated to it | Routing, group creation, token issuance, egress — each an explicit row, each bounded by its own scope glob. |
routd evaluates every tool call against those rows, deny-wins, with no
fallback: an action with no matching allow row is refused. Delegation is bounded by
subset-of-held — a principal may pass on only rows it holds, and only those it holds
with the grant option — so authority strictly decreases down every chain and no agent
can escalate past its granter.
When a message arrives:
platform=, chat_jid=, sender=)/home/node/ (group workspace), /workspace/web/ (optional web surface)gated via MCP socket
Containers are ephemeral. All state is in /home/node/ (volume-mounted) or
routd's message store, which is host-side.
| Primitive | Lifecycle | Isolation | Use |
|---|---|---|---|
| Group | Persistent | Folder boundary | Agent identity, memory, persona, skills |
| Topic | Transient | Session ID | One conversation thread, forkable |
| Session | Ephemeral | Claude Code session | LLM context window, reset via /new |
| Layer | Mechanism | Threat Mitigated |
|---|---|---|
| Container | Docker isolation, no --privileged |
Process escape, host filesystem access |
| Network | Per-spawn proxy on an internal network + host allowlist |
Exfiltration, C2 communication |
| MCP | Per-group unix socket, grants enforced by gated |
Cross-tenant tool access, privilege escalation |
| Secrets | AES-256-GCM at rest, env-var injection, no disk persistence | Secret theft via disk access or container inspect |
Per-folder allowlist, enforced by a proxy started fresh for each spawn:
--internal, so the proxy is its only path anywhere
The agent reaches only what the operator permitted. Example allowlist:
["api.anthropic.com", "github.com", "pypi.org"].
See egress docs and
SECURITY.md.
Each group gets its own unix socket (/ipc/<group_id>.sock).
gated brokers every tool call, enforces grants, and logs actions.
Agents cannot reach other groups' sockets — Docker mounts only the agent's own socket.
Secrets are AES-256-GCM encrypted in the secrets table in routd.db.
The container env carries model credentials only — the four env-profile keys, taken
from the host .env and overlaid with the triggering user's own rows. Every
capability credential stays on the host: routd resolves it per tool call,
spends it in the outbound request, and returns only the response. No secret is written to
/home/node/ or to logs.
Three MCP tools for hierarchical coordination:
delegate_group(group, prompt, chatJid) —
hand work down to a child. Child runs async, parent doesn't block.
escalate_group(prompt, chatJid) —
hand work up to parent. Parent responds back through this child.
observe_group(source) —
watch another group's messages without taking over.
Use delegate for specialist work. Use escalate when authority is needed.
Use observe for ambient awareness (sibling monitoring, aggregation).
fork_topic(parent, child) — create a new topic from another's session state.
Child gets a fresh session ID but starts with parent's Claude Code context. Use for:
Skills can specify ALWAYS or NEVER triggers in SKILL.md:
ALWAYS:
- "download this video" → acquire
- "transcribe this" → acquire
NEVER:
- Static pages → do NOT use agent-browser, use acquire instead
Gateway reads these at session start and surfaces them in <skills> block.
Agent uses them for fast dispatch without re-reading every SKILL.md.
Pattern for long-running tasks that survive session resets:
~/state/.json Named after "ralph loop" (stateless iteration). Use for multi-page scrapes, batch processing, or any task that takes >1 session.
For APIs with pagination or long-running operations:
~/state/ or ~/facts/page=N), Stripe list cursorsDetect file changes without storing full content:
~/state/fingerprints.jsonUse for monitoring codebases, watching config files, or tracking external resources.
Chain multiple agents for staged processing:
ingest → classify → route → specialist → review → publish
Each stage is a separate group. Use delegate_group or direct message routing.
Example: support intake → triage agent → specialist teams → summary agent → publish.
| Aspect | MCP Tools | In-Context Skills |
|---|---|---|
| Context window | Tool name + description only | Full SKILL.md in system prompt |
| Authority | Host-enforced grants | Agent-enforced (prompt-based) |
| State | Stateless, always available | Loaded at session start, persists |
| Best for | Platform operations (routing, groups, tokens) | Workflow patterns, domain knowledge, multi-step procedures |
Rule: Use MCP for side effects (send message, create group, issue token). Use in-context skills for knowledge and workflow guidance.
Each skill is a directory under ~/.claude/skills/<name>/ with:
SKILL.md — description, workflow, ALWAYS/NEVER triggers
Gateway reads SKILL.md at session start, injects into system prompt.
Skills compose — agent can invoke multiple skills per turn.
(Research in progress. Placeholder for skill discovery, versioning, dependencies, and performance evals.)
Measurable improvements from skill-based workflows: