Skip to main content
Comis governs what an in-process agent may do with a small, closed set of capabilities — permission tokens like 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.
The MCP call itself runs daemon-side (on the daemon’s network, like 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 surfacecomis_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 curated tool.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:cron capability, and the gate’s tool reach is exactly the agent’s granted orch:* 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.
The gate is an efficiency layer, not a guaranteed-fire security control, and it matters not to confuse the two. A gate decides whether a fire wakes the model; it is not a mechanism that guarantees a monitor fires. That distinction bounds the worst case a hostile or broken gate can reach:
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:
Two properties make least-privilege hold by construction:
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.
When a caller lacks the required capability, the gate raises a 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 — the admin-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.
Deny-by-origin is trust-tiered. A control-plane method rejects an agent-origin call from a non-admin turn (guest/user trust, or a turn with no resolved trust) — the confused-deputy floor: a low-trust or prompt-injected agent can never reach the control plane through the in-process dispatcher. An admin-trust agent turn — one acting for a user the operator explicitly granted admin via elevatedReply.senderTrustMap (resolved per-message, default user) — INHERITS that user’s control-plane privileges and is allowed through to the admin handler (which re-checks admin trust, defense-in-depth).The trust signal is sound: _trustLevel is in the internal-field registry (stripped from external callers, like _agentId) and is re-injected in-process from the framework request context after the call’s own params, so neither a tool nor the agent can forge it; an absent trust resolves to non-admin (denied). Admin is reached only via an explicit operator grant, never a default.

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.

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.