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 non-admin agent turns 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 system-health 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. Admin-only 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 — the contracts that expose anadmin route without also exposing an rpc route.
A multi-scope contract is excluded because its RPC route is intentionally
agent-reachable; its handler owns the narrower caller-vs-operator authorization
rule. The orchestration surface an agent legitimately reaches (its granted
orch:* methods, and its own session reads) is not in the deny set. 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, _capabilities, and _trustLevel — is
stripped from external WebSocket and REST callers before dispatch. An
external caller therefore cannot forge agent origin or handler trust. After
route admission, the gateway derives admin trust from the authenticated
context independently of which accepted route admitted a multi-scope method.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-only 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.
