> ## Documentation Index
> Fetch the complete documentation index at: https://comis-feature-matrix-channel.mintlify.site/llms.txt
> Use this file to discover all available pages before exploring further.

# Capability Model

> The single capability gate, deny-by-origin on the control plane, and why no capability implies admin

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](/agents/autonomy) profile.

<Note>
  **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.
</Note>

## 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.

| Capability     | Surface it gates                                                                                                                                                                                             |
| -------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `orch:read`    | Read content and workspace files                                                                                                                                                                             |
| `orch:web`     | Fetch untrusted external web content                                                                                                                                                                         |
| `orch:write`   | Write to the agent's jailed workspace                                                                                                                                                                        |
| `orch:analyze` | Cost-bearing media analysis                                                                                                                                                                                  |
| `orch:spawn`   | Spawn sub-agents                                                                                                                                                                                             |
| `orch:graph`   | Run execution graphs                                                                                                                                                                                         |
| `orch:cron`    | Author and run scheduled jobs                                                                                                                                                                                |
| `orch:skill`   | Invoke skills                                                                                                                                                                                                |
| `orch:message` | Send a message to the agent's origin channel (the outward send subset: `message.send`/`reply`/`react`). Editing, deleting, fetching, and attaching stay admin-only and are not part of this capability.      |
| `orch:browse`  | Drive a real browser (outward; off in every default profile)                                                                                                                                                 |
| `orch:mcp`     | Call an allowlisted connected MCP server's tool from a jailed script (inbound; a floor cap on standard/unattended/max — reachability is gated by the per-server `autonomy.mcp.allow` allowlist, not the cap) |

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`](/agent-tools/orchestrate) script does not call RPC
methods directly -- it calls **named tools** through `tool.invoke` over the
lease-authenticated [capability socket](/reference/json-rpc#the-agent-cap-socket-surface-tool-invoke).
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**.

| Tool                                                                                                                       | Capability  | How it runs                                                                      |
| -------------------------------------------------------------------------------------------------------------------------- | ----------- | -------------------------------------------------------------------------------- |
| `read`, `grep`, `find`, `ls`, `jq`                                                                                         | `orch:read` | In-process builtins, run daemon-side under the agent's jailed workspace          |
| `memory_search`, `memory_get`, `session_search`, `sessions_list`, `session_status`, `sessions_history`, `extract_document` | `orch:read` | Routed to the existing registered RPC handler (self-scoped to the agent)         |
| `web_search`, `web_fetch`                                                                                                  | `orch:web`  | Daemon-side on the daemon's network (the jail stays `--unshare-net`), DNS-pinned |

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:

<Steps>
  <Step title="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.
  </Step>

  <Step title="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.
  </Step>
</Steps>

## Connected MCP Tools (`orch:mcp`)

When the operator connects MCP servers, their tools can be surfaced **into** a
jailed [`orchestrate`](/agent-tools/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](/agents/autonomy#connected-mcp-tools), 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):

<Steps>
  <Step title="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.
  </Step>

  <Step title="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.
  </Step>
</Steps>

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.

<Note>
  **`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.
</Note>

### The write and message surfaces are new *typing*, not new capabilities

The [typed mutation surface](/agent-tools/orchestrate#the-typed-mutation-surface-write-and-message)
\-- `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](/agent-tools/scheduling#wake-gate-run-the-model-only-when-it-matters) -- 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](/agents/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:

<Steps>
  <Step title="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](/agent-tools/scheduling#wake-gate-run-the-model-only-when-it-matters):
    an errored, timed-out, or over-capped gate **wakes** the model rather than
    silently swallowing the fire.
  </Step>

  <Step title="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.
  </Step>

  <Step title="A self-DoS is visible, not silent">
    A gate that skips every fire shows up as a **100% skip-rate** in the
    [`comis fleet`](/reference/cli#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.
  </Step>
</Steps>

## One Gate, No Bypass, No Wildcard

A single predicate decides every privileged action. It is a plain membership
test:

```typescript theme={}
// Pure predicate -- returns a boolean, never throws.
checkCapability(held, "orch:spawn"); // true iff "orch:spawn" is in `held`

// Boundary gate -- throws CapabilityDeniedError when the cap is absent
// (including the missing/undefined case). The RPC dispatcher converts the
// throw into a JSON-RPC error response.
requireCapability(held, "orch:spawn");
```

Two properties make least-privilege hold **by construction**:

<Steps>
  <Step title="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".
  </Step>

  <Step title="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.
  </Step>
</Steps>

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:

<Steps>
  <Step title="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.
  </Step>

  <Step title="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.
  </Step>

  <Step title="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.
  </Step>
</Steps>

<Warning>
  **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.
</Warning>

## How Capabilities Get Granted

An agent does not request capabilities; the operator grants them through an
[Autonomy profile](/agents/autonomy). 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.

| Layer                | Responsibility                                                                |
| -------------------- | ----------------------------------------------------------------------------- |
| **Autonomy profile** | Operator picks a posture; it resolves to a capability set                     |
| **Injection**        | The resolved capabilities ride the agent's in-process RPC calls               |
| **The gate**         | Each privileged handler calls `requireCapability` for the capability it needs |
| **Deny-by-origin**   | Admin handlers reject agent-origin calls before capabilities are consulted    |

## Related

<CardGroup cols={2}>
  <Card title="Autonomy" icon="sliders" href="/agents/autonomy">
    Named profiles, the zero-config default, and the legible degrade that
    grant and bound capabilities.
  </Card>

  <Card title="Security Model" icon="lock" href="/reference/security-model">
    The capability axis in the context of the full input/skill/execution/output
    defense layers.
  </Card>

  <Card title="Approvals" icon="circle-check" href="/security/approvals">
    The escalation gate for outward, hard-to-reverse actions.
  </Card>

  <Card title="Audit" icon="scroll" href="/security/audit">
    The content-free trail that records capability denials and origin rejections.
  </Card>
</CardGroup>
