Roles
A role is a spawn-time profile that decides whether a spawned
agent may itself delegate — spawn or drive further children — and,
if so, which roles it's allowed to spawn. It's set once, at
agent_start, and enforced by the daemon for the life of the
session.
Roles are an MCP/HTTP surface today — there is no agentproto sessions start --role flag. Set role on the agent_start MCP tool
or the POST /sessions/agent HTTP body; see below.
The 3-layer profile
A role bundles three things:
disposition— a system-prompt fragment prepended to the child's first turn. Soft: it sets the mindset ("you are the leaf, do the work yourself" vs. "decompose, delegate, verify"), but nothing stops a capable model from ignoring it if the tools to disobey are still in its hands.toolPolicy.delegation("allow"|"deny") — the delegation gate. When"deny", the daemon tries to keepagent_start/agent_promptout of the child's toolset at spawn time — but this is spawn-path-specific and, on the default gateway, not airtight. It's a real gate on the orchestrator sub-gateway path and a best-effort default elsewhere; see below for which paths it covers and where it leaks.skills[]— declared but currently inert. The field is typed onRoleProfile(role.ts:33) and parsed from a role pack'sROLE.md(role-pack.ts:84), but nothing in the spawn path readsrole.skills—session-spawn.tsonly consults theagent_startcall's ownskillsand the configdefaults.skills(session-spawn.ts:580,652).role.ts:12-13flags it as intended for "a future pack-carried role"; treat it as unimplemented, not a working per-role skill set.
Plus two fields that place the role in the spawn lattice:
level(number) — privilege level; higher is more privileged.spawnableRoles(optional string array) — a closed allowlist of role names this role may spawn, by name. When set, it replaces the level comparison below for this role.
How the delegation gate works
toolPolicy.delegation is resolved at the agent_start injection
point (session-spawn.ts:510), before the child runs — but it is not
one universal, airtight gate. It acts through two separate spawn-time
mechanisms, and a third path escapes it entirely:
- Orchestrator sub-gateway — genuinely gated. A role that denies
delegation has its
orchestratorrequest dropped outright, whatever the caller asked for (session-spawn.ts:547, guarded on!delegationDenied). The scoped sub-gateway (/mcp/orchestrator) is the only path that mountsagent_start/agent_promptfor an orchestrating child, and it demands a scope token — it deliberately does not inherit the loopback bypass (http-server.ts:668-675). A denied role never receives the sub-gateway, so it has nothing to spawn with. - hermes default gateway — best-effort, loopback-open. Only when
the adapter is
hermesand the caller passed no explicitmcpServers, the daemon defaults the child to its own/mcpURL, and for a denied role appends?denyTools=agent_start,agent_prompt(session-spawn.ts:538-542;DELEGATION_TOOL_NAMES,role.ts:64). The daemon reads that deny-list back from the requesting URL's own query string (parseDenyToolsQuery→handleMcp,http-server.ts:652-666), not from a trusted per-session registry. And the bare/mcpendpoint is loopback-open: any request from127.0.0.1/::1without anX-Forwarded-Forheader skips the token check (authorize/isLoopback,http-server.ts:479-491). A co-located child handed the gated URL can therefore reconnect to the plain/mcpand getagent_start/agent_promptback — on this path the strip is a default, not a wall. - Caller-supplied
mcpServers/ non-hermes adapters — not touched. If the caller passes explicitmcpServers, or the adapter isn'thermes, thedenyToolsdefault never fires (session-spawn.ts:525,538). Whatever delegation tools such a child ends up with are whatever the caller wired;toolPolicy.delegationdoes not reach in and remove them.
The part that toolPolicy locks down hard is the orchestrator sub-gateway:
promptAppend can't re-open it (it's never consulted at the drop,
session-spawn.ts:547) and a child can't mint its own scope token.
promptAppend layers text on top of the resolved role's disposition —
it can specialize it, never replace it. Beyond that path,
delegation-deny is a spawn-time default the child's own wiring can
route around, not a universal sandbox — and the daemon still cannot
strip a native CLI subagent/Task tool it never routed in the first
place (see the built-ins below).
The two built-ins
| Role | level | toolPolicy.delegation | Disposition |
|---|---|---|---|
executor | 0 | deny | Leaf — execute the task directly, never spawn or delegate. |
supervisor | 100 | allow | Decompose, delegate the parts that benefit from a separate agent, verify their output. |
Both built-in dispositions explicitly tell the agent not to use its own
CLI's native subagent/Task tool: the daemon cannot strip tools that are
not routed through its MCP gateway, so the rule must be followed in the
prompt. executor is told never to spawn or delegate; supervisor is
told to delegate via agent_start so the caller gets an observable
session.
executor is the floor of the lattice — canSpawn short-circuits to
false for any role whose own delegation is "deny"
(role.ts:192-194), before the level comparison, so an executor may
spawn nothing, not even a peer at its own level. Whether the child even
holds agent_start to attempt it is the separate, weaker toolset
question covered above.
Role-level default for deferred tool loading
A role can also carry a deferredTools default (role.ts's
RoleProfile.deferredTools, optional boolean). executor defaults it
on — an executor never delegates anyway (the tool gate above already
strips agent_start/agent_prompt for it), so the daemon's own
~190-tool /mcp surface is mostly dead weight in its context; hiding it
behind tool_search (see deferred-tools.ts) saves real turn-0 tokens
without losing any capability (every tool stays fully callable, just
absent from tools/list until searched; on a harness with native tool
search the role default yields to eager loading). supervisor has no opinion
(undefined) and falls through to whatever the daemon's own boot-time
default is.
This composes with the same ?denyTools=-carrying self-mount URL
described above: when the resolved role (or an explicit
agent_start.deferredTools override, which always wins) has an opinion,
session-spawn.ts appends &deferred=1 or &deferred=0 to the injected
mcpServers ref — e.g. an executor's hermes self-mount ends up
...?denyTools=agent_start,agent_prompt&deferred=1&callerSessionId=....
Precedence, highest first: explicit agent_start.deferredTools → a
caller-supplied mount's own ?deferred= → a harness whose manifest declares
capabilities.nativeToolSearch (claude-code ⇒ eager, since it already
defers MCP tools behind its own ToolSearch — see
MCP in coding CLIs)
→ the resolved role's deferredTools → (no override at all) the gateway's
own defaults.mcp.deferredTools config.json default, which is off
globally unless an operator opts in.
The same ?deferred=1|0 query works standalone on any /mcp connection
(not just the daemon's own self-mount), and composes with ?denyTools=
exactly like the delegation gate above — denyTools is applied as the
outermost wrap either way, so an excluded tool name never reaches
registration regardless of deferred status.
canSpawn: the non-escalation rule
Every spawn made through an orchestrator sub-gateway (a session
started with orchestrator: true/{...}) is gated by canSpawn(parentRole, childRole), checked before the child's tools are ever injected:
- If the parent's own
toolPolicy.delegationis"deny", it can spawn nothing — full stop. The lattice below is moot. - Else, if the parent has
spawnableRolesset, the child must be named in that allowlist. - Else (the default, open mode): non-escalation —
child.level <= parent.level. A role may spawn a peer or a subordinate, never something more privileged than itself.
Unbounded same-level recursion (a supervisor spawning a supervisor
spawning a supervisor) is allowed by this rule — it's a separate,
deliberate pattern bounded by the orchestrator's maxDepth /
maxChildren caps, not by the role lattice.
Worked example with the built-ins:
supervisor(level 100) may spawnexecutororsupervisor.executor(level 0, delegation denied) may spawn nothing.
A denied spawn returns role_spawn_denied with the parent/child role
names and levels, plus (in allowlist mode) the allowed set.
Depth-derived default
Omitting role on agent_start doesn't leave the child roleless —
it derives one from spawn depth against a cutoff:
depth < cutoff → supervisor, depth >= cutoff → executor. The
cutoff defaults to 1 (root spawns default to supervisor; spawns
made through an orchestrator sub-gateway default to executor) and
is overridable via defaults.defaultRoleDepthCutoff in
~/.agentproto/config.json.
Introspection
role_list
The role_list MCP tool enumerates every role the daemon currently
knows — the two built-ins plus any installed role pack — read-only,
pure visibility into the same registry agent_start's role field
and the spawn gate use:
{
"roles": [
{ "name": "executor", "level": 0, "delegation": "deny", "spawnable": [] },
{ "name": "supervisor", "level": 100, "delegation": "allow", "spawnable": ["executor", "supervisor"] }
]
}spawnable is precomputed with the same canSpawn check the daemon
uses at spawn time, so a caller can discover what it may spawn before
attempting agent_start with orchestrator.
The "Roles you may spawn" context line
When a delegating role's disposition is composed into the child's first turn, a line is appended automatically if its spawnable set is non-empty:
Roles you may spawn: executor, supervisor.This lets a delegating agent know its options at runtime instead of
guessing. It's omitted entirely for a role with an empty spawnable
set (an executor sees nothing extra).
Pack-carried custom roles
Beyond the two built-ins, a role can be installed as a role pack
— discovered the same way skill packs are (packages/cli/src/commands /skill-install):
- Standalone:
<dir>/roles/<slug>/ROLE.md, one folder per custom role —~/.agentproto/roles/<slug>/ROLE.mdin production. - Adapter-carried: any installed
@agentproto/adapter-*package may declaremetadata.roles: string[], each entry rawROLE.mdmarkdown embedded directly in the package.
ROLE.md format
Frontmatter (flat key: value, dotted keys, comma-separated lists —
no YAML dependency) plus a body that becomes the disposition
verbatim:
---
role: reviewer
level: 50
toolPolicy.delegation: deny
skills: code-review, security-review
---
You review code for correctness and security issues. You do not
edit files or spawn sub-agents — flag problems, suggest fixes, and
let the calling agent decide what to do with your findings.Required fields: role (the name), level (a finite number),
toolPolicy.delegation (allow or deny). Optional: skills,
spawnableRoles (both comma-separated) — but skills is parsed and
then ignored (see the 3-layer profile); only
spawnableRoles currently affects behavior. A malformed ROLE.md throws
when parsed directly, but registry loading treats a broken pack as
partial-discovery-safe — it's skipped, not fatal to the whole
registry.
Built-ins always win a name collision — a pack cannot shadow or
widen executor/supervisor, regardless of where the merged
registry is consumed (resolveRole, listRoles, spawnableRolesFor
all merge through the same function).
Trust boundary
Installing a role pack is an operator decision, same as installing an
adapter or a skill pack — a ROLE.md can declare
toolPolicy.delegation: allow at any level. The optional
maxGrantableDelegation knob (defaults.maxGrantableDelegation in
config.json) caps this: a pack that self-grants allow at a level
above the configured cap is forced to deny at load time — the pack
still declares its intent, but the daemon refuses to grant it. No cap
configured means no restriction.
Relationship to --orchestrator
These answer different questions:
- Role — may this child delegate at all, and if so, to whom?
The gate. Set via
roleonagent_start. --orchestrator— mount the scoped sub-gateway so it actually can. Set viaorchestratoronagent_start(CLI:--orchestrator/--orchestrator-json, seeverbs/sessions.md).
A role that denies delegation drops orchestrator outright, no
matter what the caller requested. A role that allows delegation still
needs orchestrator (or caller-supplied mcpServers) mounted before
it has anything to spawn with.
CLI surface
agentproto sessions start does not currently expose a --role or
--prompt-append flag — role assignment is MCP/HTTP-only:
- MCP:
agent_start'srole(string) andpromptAppend(string) fields. - HTTP: the same fields on the
POST /sessions/agentbody.
role_list is likewise MCP-only today; there is no agentproto role-list verb.
Plugins
A plugin extends agentproto run-swarm's runtime registry with new substrates, dispatchers, participant executors, or state stores — the four port kinds the MultiAgentRuntime kernel composes per swarm.
Runtime profiles
A runtime profile is a bundle of file scaffolding the CLI can drop into a user's repo to make a workflow work out of the box.