orch:spawn and orch:cron — checked
by a single gate on every privileged action. Capabilities are deliberately
orthogonal to the gateway’s network scopes, and the control plane is
unreachable to any agent-originated call by construction. This page describes
the model; the operator-facing dial that grants capabilities is the
Autonomy profile.
Why a separate axis from gateway scopes. External callers authenticate to
the gateway and carry a network scope (
rpc, admin, mcp-client). An
agent’s own tool calls, by contrast, reach the RPC dispatcher directly, in
process — they never pass through the gateway scope check. That in-process path
is exactly the bypass the capability gate closes: the gate lives at the handler
boundary that every call reaches, scope check or not.The Closed Capability Union
Capabilities are a closed string-literal union — one member per orchestration surface. The runtime list is the single source of truth; the static type is inferred from it, so the two can never drift.
Every member is namespaced
orch:. None of them is, or implies, a gateway
scope — the two sets are disjoint, and an architecture test asserts the
intersection is empty.
The Curated Tool-Capability Surface (tool.invoke)
An autonomous orchestrate script does not call RPC
methods directly — it calls named tools through tool.invoke over the
lease-authenticated capability socket.
A second curated map — the tool→capability map — decides which tools that
surface exposes and which capability each requires. It is default-deny: a
tool that is not on the map has no capability and is undispatchable.
The map above is representative; the authoritative list is
TOOL_CAPABILITY_MAP
in the daemon (the same table the dispatch gate, the lease audience, and the
generated in-jail SDK all read, so they cannot drift). Two invariants hold by
construction, asserted by architecture tests:
1
No cap-mapped tool is denylisted
Every tool on the map is checked against the never-delegate denylist at
module load. The administrative/destructive tools (
mcp_manage, mcp_login,
gateway, the *_manage family) are never on this surface — they stay
unreachable, and deny-by-origin covers the control plane behind them.2
An unmapped tool is undispatchable
A
tool.invoke for a tool absent from the map raises CapabilityDeniedError
before any lease is even consulted — default-deny by absence. The MCP
export-policy is not the gate (it would admit the wrong set); the cap-map
plus the denylist are the only gates.Connected MCP Tools (orch:mcp)
When the operator connects MCP servers, their tools can be surfaced into a
jailed orchestrate script as
comis_tools.mcp.<server>.<tool>(), gated by a dedicated capability, orch:mcp.
orch:mcp is a floor capability held by default on the autonomy-bearing
profiles (standard/unattended/max) — exactly like orch:write. Holding the cap
is not the reachability gate: a call is reachable only if the specific
{server, tool} pair is on that agent’s
inbound allowlist, which is empty by
default. So a fresh agent holds orch:mcp yet reaches no MCP server until
the operator allowlists one — granting the cap by default opens nothing.
Reachability is therefore default-deny at the allowlist layer (the cap being
held is the same floor-cap + surface-opt-in shape as orch:write + its
writeSurfaceEnabled gate):
1
The capability grant (floor)
orch:mcp is in the standard/unattended/max floor set (the assistant
profile, which has autonomy off, holds it only via an explicit
autonomy.mcp.enabled: true opt-in). The cap being present is necessary but
not sufficient — it authorizes the mcp surface to exist, not any specific
server.2
The per-agent inbound allowlist (the operative gate)
A
{server, tool} pair absent from autonomy.mcp.allow — or on a server
with no explicit inbound classification — is denied at the daemon-side
executor. autonomy.mcp.allow defaults {}, so an unlisted server or tool
is unreachable by absence. This is the layer that keeps the surface dark by
default.web_fetch), so the jail stays --unshare-net; the result re-enters wrapped as
untrusted data. The MCP control plane stays firmly on the far side of the
gate: mcp_manage/mcp_login are on the never-delegate denylist, and the
underlying mcp.* management methods are refused to any agent-origin call — so
a jailed script can invoke an allowlisted server’s tool but can never connect,
disconnect, or authenticate a server.
orch:mcp is not the mcp-client gateway scope. They share the letters
“mcp” and nothing else: orch:mcp is an orch:-namespaced capability gating
an agent’s inbound use of connected MCP tools from inside the jail; the
mcp-client gateway scope governs an external network caller. The two axes
stay disjoint — the emptiness of their intersection is architecture-tested,
exactly as for every other capability.The write and message surfaces are new typing, not new capabilities
The typed mutation surface —comis_tools.write(...) and comis_tools.message_send/reply/react(...) —
shares its posture with orch:mcp, and it is worth stating plainly:
orch:write, orch:message, and orch:mcp are all floor capabilities, on
by default in standard and up (only assistant withholds them). The capability
being held is never the operative gate — a separate per-surface opt-in is:
autonomy.write: true for the typed write surface, and the per-server
autonomy.mcp.allow allowlist (default {}) for connected MCP tools. Calling any
of these capabilities “default-off” would be inaccurate — the honest statement
is that each surface is default-deny (inert until its opt-in is set), while the
underlying floor capability is held.
Wake-Gate Suppression Bound
A scheduled cron job may carry a wake-gate — a pre-run script that decides whether that fire invokes the model. The gate is a caller of the same curatedtool.invoke surface above, and it stays inside the same envelope as any other autonomous script:
- No new capability. The gate runs under the agent’s own resolved autonomy capabilities in the standard jail. Authoring and running a gated job is the existing
orch:croncapability, and the gate’s tool reach is exactly the agent’s grantedorch:*set (through the tool-capability map) — never a gate-specific grant. A job cannot use a gate to reach a tool the agent does not already hold. - Per-fire least privilege. Each fire mints a fresh attenuated lease under its own root, so one job’s gate can never act for — or suppress — another job.
1
It cannot escalate
A gate holds no capability the agent lacks, so it can reach no tool and no
surface the agent could not already reach directly. It is also
fail-open:
an errored, timed-out, or over-capped gate wakes the model rather than
silently swallowing the fire.
2
It cannot suppress another job
The only suppression a gate can cause is on its own job — an explicit
skip verdict on its own fires, a self-inflicted denial of service. Because
each fire runs under its own per-fire root, a gate never reaches a different
job’s fires or another agent’s runs.
3
A self-DoS is visible, not silent
A gate that skips every fire shows up as a 100% skip-rate in the
comis fleet health report, so a monitor gated
into silence is legible rather than a quiet gap. Each denied tool call inside
a gate is the same content-free CapabilityDeniedError audit signal as
anywhere else.One Gate, No Bypass, No Wildcard
A single predicate decides every privileged action. It is a plain membership test:1
No wildcard branch
Unlike the gateway’s scope check — which has an asterisk-implies-all rule —
the capability predicate has no lattice and no catch-all. A held set is
exactly the capabilities it lists; no entry (an asterisk string included)
confers any other capability. No capability can ever imply
admin, rpc,
or “all”.2
Disjoint from network scope
Capabilities are
orch:*; gateway scopes are rpc | admin | mcp-client.
Because the two vocabularies do not overlap, holding a capability can never
be mistaken for holding a scope, and vice versa.CapabilityDeniedError discriminated by kind: "capability_denied", so the
denial is recorded as a content-free security signal in the audit trail.
Deny-by-Origin on the Control Plane
Capabilities govern what an agent may do. A second, stricter rule governs the control plane: a non-admin agent turn may never reach it, regardless of capabilities. Administrative handlers (the ones that manage secrets, tokens, config, channels, and the other privileged-surface methods) reject a non-admin agent-origin call outright — before any capability is even consulted. An admin-trust agent turn (an explicit operator grant) is the exception: it inherits the admin user’s control-plane access (detailed in the trust-tier note below). The deny-by-origin set is exactly the true control plane — theadmin-scoped methods. The orchestration surface an agent legitimately reaches
(its granted orch:* methods, and its own session reads) is not in this set:
those are capability-gated or agent-reachable, not denied by origin. A handful of
methods stay control-plane-only even within otherwise-agent-facing namespaces:
the message subset that is not part of orch:message
(message.edit/delete/fetch/attach) and the lifecycle ops that delete or
export an arbitrary session (session.delete/export/reset_conversation).
This is sound because of how agent origin is established:
1
Internal markers are injected in-process only
The legitimate path that tags a call as agent-originated (injecting
_agentId and _capabilities) runs inside the daemon, never across the
network boundary.2
Inbound internal fields are stripped at the gateway
Every field whose name begins with an underscore — the whole internal-field
registry, including
_agentId and _capabilities — is stripped from
external WebSocket and REST callers before dispatch. An external caller
therefore cannot forge agent origin.3
Presence becomes an unforgeable signal
With inbound markers stripped, the presence of
_agentId on a call is a
sound, unforgeable “this came from an agent” signal — which is what lets the
admin handlers deny-by-origin safely.How Capabilities Get Granted
An agent does not request capabilities; the operator grants them through an Autonomy profile. The profile resolves to a concrete capability set, those capabilities are injected onto the agent’s in-process calls, and each privileged handler checks the one it requires. The mapping from method to required capability is fixed in the daemon — an agent cannot raise its own grant.Related
Autonomy
Named profiles, the zero-config default, and the legible degrade that
grant and bound capabilities.
Security Model
The capability axis in the context of the full input/skill/execution/output
defense layers.
Approvals
The escalation gate for outward, hard-to-reverse actions.
Audit
The content-free trail that records capability denials and origin rejections.
