Skip to main content
What it does. Decides how much an agent can do on its own — spawn sub-agents, run scheduled jobs, write to its workspace, browse the web — and makes that decision legible. You pick a named profile (or accept the default); Comis resolves it into a concrete set of capabilities and guard rails, logs exactly what it turned on, and tells you the one field to change it. Smart defaults must never surprise you.
Autonomy vs. capabilities. A profile is the operator-facing dial. It resolves to a set of capabilities (the orch:* permission tokens an agent holds) plus guard rails (budget ceiling, spawn-rate limit, message quota). Capabilities are enforced by a single gate on every privileged action — see the Capability Model for the enforcement side. This page is about choosing and reading the posture.

The Zero-Config Default

If you set no autonomy block at all, every agent resolves to the standard profile. That is the great-out-of-the-box posture: the agent can orchestrate (spawn sub-agents, author cron jobs, run execution graphs, use skills, read and analyze content, write to its jailed workspace) under always-on guard rails, and it messages only its own channel under an hourly quota.
~/.comis/config.yaml -- nothing to set; standard is the default
There is no un-gated path to orchestration: an agent with no autonomy config resolves to standard and holds exactly those surfaces, capability-gated. It is an explicit grant via a named profile, not a compatibility shim.

Named Profiles

Set autonomy.profile to pick a posture. There are four:
unattended is active; max ships later. unattended’s capability set stays standard-equivalent (the structural floor still bounds every cap — no over-grant), but its mode behaviors are on in this release: a run that would ask a human resolves to deny + escalate (never a blocking prompt), a denial breaker aborts a run that keeps hitting a floor block, and an operator can evict an in-flight run back to default — see The unattended profile below. max’s extra surface (sandbox auto-allow) is not in this release: selecting it resolves to standard’s capability set and attaches a not-yet-available notice (logged at boot). Comis never silently grants a capability whose enforcement floor is not yet built, so you are told rather than over-granted. (Durable runs are not one of the deferred surfaces — they ship as an opt-in knob orthogonal to the profile; see Durability & Resume below.)

The unattended profile

unattended is for a run with no human present — a long-lived cron pipeline, an overnight batch, a background coordinator. Its capability set is exactly standard’s (the structural floor bounds every cap; there is no over-grant), but its mode changes how the run behaves when it reaches a decision a human would normally make. The whole point is that such a run never stalls waiting on a person and never burns its budget retry-looping a thing it is not allowed to do.
  • Never-hang: a would-ask becomes deny + escalate. Under standard, an action that needs approval pauses for a human. Under unattended there is no human to pause for, so that same action does not block — it resolves to a denial and the platform escalates it to your operator out-of-band. The run keeps going on what it is allowed to do; it does not sit on a prompt forever.
  • Outward actions still escalate, never auto-send. unattended does not loosen the outward floor. A send to a new channel, a browser action, any hard-to-reverse outward step is still proposed for approval, never auto-decided — exactly as under standard. Never-hang changes waiting into escalation; it does not change propose into send.
  • The denial breaker stops a retry-loop. A run that keeps trying a blocked capability would otherwise spin — each attempt denied, the budget draining for nothing. After denialBreakerN consecutive floor blocks (default 5) on one run, the denial breaker trips: the run is aborted (an execution:aborted with reason denial_breaker) and the operator is told, rather than letting it retry-loop the budget away. An actual allowed step in between resets the counter, so a single stray denial inside productive work never accumulates to a trip.
  • An operator can evict an in-flight run to default. If an unattended run misbehaves, an admin can call autonomy.evict with its rootRunId. This is demote-but-continue: the run is not killed — it keeps running, but under default from its next gated decision onward (so it reverts to the conservative posture mid-flight, no waiting for a restart or the next spawn). This is distinct from lease.revoke (cooperative stop) and run.kill (hard stop).
  • Fail-closed: an unresolvable mode is treated as default. Mode resolution is fail-closed. If the run’s autonomy mode cannot be determined for any reason, it is treated as default (the safe profile) — never something broader. This posture is controlled by evictOnPolicyUnreachable (default true), and it is the same safe target the autonomy.evict demotion lands on.
The two unattended knobs are denialBreakerN (default 5) and evictOnPolicyUnreachable (default true); both live in the autonomy.* config table and apply to every profile, but they are the dials that matter most for a no-human run.

What standard turns on

The standard profile resolves these nine floor capabilities: orch:message is on by default but scoped to the agent’s origin channel only — it answers where it was spoken to. The cap covers the genuinely-outward send subset (message.send/reply/react); editing, deleting, fetching, and attaching stay admin-only. A send to a new channel always escalates for approval (it is never auto-decided), configured via message.channels. Two surfaces are off in every default profile because they are outward and hard to reverse: orch:browse (a real browser) and messaging a new channel (beyond the agent’s own). Opt into the browser explicitly with browse: true; even then it always escalates for approval — it is never auto-decided.

Learned procedures stay advisory

When an agent solves a task with an orchestrate run and that approach proves out across verified successes, the learning layer distils it into an advisory procedure doc — a learned skill that records the run’s tool footprint. It surfaces into the agent’s <available_skills> listing like any other learned skill, bounded by a per-agent budget (learning.reflect.maxProcedureDocsSurfaced, default 10) that, when exceeded, keeps the most-corroborated procedure docs in a stable listing order, so a burst of procedures can never bloat every prompt. Crucially, surfacing a procedure doc grants no new capability and no new execution path: it is guidance the model re-authors into a fresh orchestrate script, run under the same capability gate and jail as any other run. A learned procedure can never widen the agent’s autonomy posture — the profile and its guard rails still bound every tool the re-authored script may touch.

Overriding Individual Knobs (Progressive Disclosure)

Most operators set only profile: (or nothing). When you need a single adjustment, set just that field — an explicit field overrides the profile, so you do not have to restate the whole posture.
~/.comis/config.yaml -- standard, but with a tighter budget and the browser on
The per-surface toggles (web, analyze, write, browse) each map to the matching orch:* capability. The scalar guard rails (aggregateBudgetUsd, maxConcurrentSelfAgents, maxSelfSpawnRatePerMin, cronSelfMax), the lease.leaseMaxTtlMin renewal ceiling (in minutes — the maximum a jailed-surface capability lease can renew up to, so revoke actually stops renewal), and the message block (channels, maxPerHour) are all individually overridable. The always-on bounds are also exposed as nested blocks for finer control, each defaulting safe so a runaway agent is bounded out of the box:
  • budget.{ aggregateUsd, tokens, wallClockMs } — the per-root-run ceiling. aggregateUsd is the priced limb(theaggregateBudgetUsdfieldisitsalias);tokensandwallClockMsboundaloopevenonanunknownpriced(subscription/limb (the `aggregateBudgetUsd` field is its alias); `tokens` and `wallClockMs` bound a loop even on an unknown-priced (subscription /0) model the $ limb would count as free.
  • rate.{ perRootCallsPerSec, perSocketCallsPerSec, connectionChurnPerMin } — the call-rate ceiling that bounds a call storm without tripping a legitimate burst.
  • spawn.{ maxConcurrentSelfAgents, maxSpawnDepth, maxChildrenPerAgent } — the tree-wide spawn shape (maxConcurrentSelfAgents is the alias of the flat field; depth defaults to 3, fan-out to 5).
  • outward.{ originOnly, perTargetGrants, volumeCap } — outward-send governance. originOnly keeps sends to the agent’s own channel by default; a new target needs an explicit perTargetGrants entry; volumeCap bounds a mass-recipient send.
See the full field list in the config reference.

Connected MCP Tools

The orch:mcp capability — which lets a jailed orchestrate script reach connected MCP server tools — is a floor capability held by default on the autonomy-bearing profiles (standard/unattended/max), like orch:write. Holding the cap opens no server, though: reachability is gated by the per-server autonomy.mcp.allow allowlist, which is empty by default. So a fresh agent holds orch:mcp yet reaches no MCP tool until you allowlist the exact {server -> tools} it may call.
~/.comis/config.yaml -- expose two allowlisted MCP tools to one agent
autonomy.mcp defaults to { enabled: false, allow: {} }. On standard/unattended/max the orch:mcp cap is floor-held regardless of enabled, so a fresh agent’s MCP surface is dark at exactly one operative layer — the empty allowlist. Setting allow is the only step needed to make a tool reachable:
  • allow is the per-server inbound allowlist — the operative gate. Each key is a connected server’s name; its tools is the explicit list of tool names reachable on that server (there is no * wildcard — you name each tool), and classification is the inbound tier. Absent from allow ⇒ denied by absence.
  • enabled grants orch:mcp to the assistant profile (which has autonomy off and no floor caps) as an explicit opt-in. On standard/unattended/max the cap is already floor-held, so enabled is not required there (it is a harmless no-op for the grant). There is no separate boolean mcp: toggle beside web/analyze/write/browse; the nested block’s own enabled field is the assistant-opt-in signal, mirroring durability.enabled.
The classification is a three-tier inbound gate: Reachability is thus unlisted ⇒ unreachable at every step: an unlisted server, an unlisted tool on a listed server, or a listed server with no inbound classification are all denied. Enforcement is the daemon’s, at the capability socket — the config only ever widens from the dark default, and never past what the operator explicitly names. The connected MCP tool runs daemon-side so the jail stays --unshare-net, and the management plane (connecting or authenticating a server) is never reachable from a jailed script. This inbound allowlist is a separate control from the outbound /mcp/v1 export policy that governs which of this daemon’s tools are exposed to external MCP clients; turning one on never affects the other.

One-shot auto-repair

When an orchestrate script fails on a recoverable error — a bad import, a misused comis_tools call, or a TypeError — the runner does one utility-model re-prompt that regenerates the script and re-runs it once. It turns a small model’s near-miss into a working run in the same turn, without bouncing the failure back to the main model for another inference turn. This is class-gated off the model’s capability class — it is not a profile knob and there is no config toggle: The class is resolved from the agent’s model (an operator capability-class override on the provider, otherwise the provider family). There is no autonomy.autorepair field to set: the model tier is the whole control, so there is no dead toggle to drift out of sync. When the class is absent or unresolved it defaults to the repair-eligible side (small), so an unknown-tier or keyless local deployment still self-repairs rather than silently not. The repair is bounded to exactly one attempt — a repaired script that fails again surfaces the original error, never a retry loop. It runs on a cheap utility model (the same keyless-safe path other utility passes use) and rides normal spend/budget accounting, so the agent’s budget bounds it like any other call. The regenerated script re-runs in the identical jail, capability, and lease envelope as the original — it gains no new privilege.

Durability & Resume

By default an in-flight autonomy run lives only in the daemon’s memory: kill the daemon mid-pipeline and that run is gone. Turn on autonomy.durability.enabled (off by default — a default install builds no durable stores and is byte-identical) and a long-running run becomes resumable across a restart, with an exactly-once guarantee on everything it sends outward.
~/.comis/config.yaml -- make a cron-fired pipeline survive a restart
What it gives you:
  • Resume or honest orphan — never a silent vanish. As a run advances it stamps a checkpoint (its spawn tree, the consumed budget, a heartbeat) to memory.db. On restart the daemon scans the live runs (after channels are up) and resumes each one, or — if its record cannot be trusted — orphans it and notifies the operator out-of-band. A run is never left in limbo. A daemon-wide watchdog sweeps any run whose heartbeat has lapsed past staleHeartbeatMs (the same fate: resume or orphan-and-notify).
  • Exactly-once outward sends — a crash never re-DMs or re-posts. Every agent-originated outward send is wrapped in a durable three-state ledger (send_attempt_startedunknown_after_sendcommitted) keyed on a unique (rootRunId, step-index) pair. A send that already committed is a no-op on replay; a send that crashed between the attempt and the platform acknowledgement is reconciled on recovery — the owning channel is asked “did this actually send?” — rather than blind-replayed. This is security invariant #12: a restart mid-pipeline never double-sends. It covers both the direct message.send/reply/react path and the sub-agent completion-announcement path — the announcement dead-letter queue consults the same ledger on restart, so a recovered announcement is never delivered twice. The typed comis_tools.message_send/reply/react outward methods dispatch that same message.* path, so they ride the same ledger. This dedup is active only under durability. With durability off (the default), an outward send is a best-effort pass-through — still delivered, just not deduped across a crash; the exactly-once guarantee attaches only once autonomy.durability.enabled is on.
  • A resumed run never broadens its authority. On resume the lease is re-minted from the run’s persisted, already-attenuated capabilities — never re-attenuated from a live parent (which could be broader), and never a superset of what the run originally held. A lease.revoke / run.kill poisons the persisted record (flips it to revoked), so a later boot can never resurrect pre-revoke capabilities — a revoked record is orphaned on resume, not re-minted.
  • DAGs resume at node-boundary granularity. A cron-fired multi-node execution graph interrupted by a restart resumes its incomplete nodes; already-completed nodes are not re-run, and any outward send a re-run node repeats is deduped by the same ledger. (Intra-node LLM/tool state is not checkpointed — resume is per node, not mid-node.)
The config knobs (all under autonomy.durability):
Honest per-channel coverage of the exactly-once guarantee. The “was this actually sent?” reconciliation needs a channel that can be queried for its recent sends. Discord and Slack can (they fetch recent history and match the bot’s message), so a crashed-mid-send on those resolves cleanly to sent (acknowledge once, no replay) or not-sent (replay once). iMessage, IRC, and Email cannot be queried for their sent history — so a crashed-mid-send on those resolves to unresolved, which Comis handles by parking the send and escalating to the operator rather than guessing. That is the safe, surfaced outcome: never a silent double-send, and never a silent drop — you are told to decide. This is the truthful shape of invariant #12: the guarantee is full and automatic on queryable channels, and parked-and-surfaced on the ones that cannot answer.
Resume scope. A resumed run is re-anchored under the bounded-autonomy meter (bounded, killable, and with its outward ledger reconciled across the restart) from its persisted checkpoint — it is not re-entered into the agent loop from the exact step it was on. Resuming the model turn itself from a mid-run step needs a wider checkpoint and is a later enhancement; today’s guarantee is “the run survives, stays bounded, and never double-sends,” not “the model picks up its half-finished thought.”

Legible Boot Logging

At startup Comis logs one INFO line per agent stating the resolved posture, so you can read what every agent was granted — and how to change it — from the boot logs alone, without turning on debug logging:
The changeField names the one config field that changes all of the above: autonomy.profile. The line also carries an m1Notice whose text is profile-specific: for an unattended agent it states that the never-hang mode behaviors are active (deny + escalate, the denial breaker, operator evict) and that the capability set stays standard-equivalent; for a max agent it states that the extra surface (sandbox auto-allow) is not yet available.

Honest, Legible Degrade

Autonomy needs a jail to run its orchestration surfaces safely. On Linux, that jail is built from an unprivileged user namespace. If the host preflight for that namespace fails, Comis does not quietly run the agent unjailed. Instead it downshifts that agent to the assistant profile (no orchestration surfaces, the agent only replies) and says so, two ways:
  1. A WARN at boot naming the downshift, with an actionable hint and errorKind: "precondition".
  2. A doctor finding an operator can read, naming the same remediation.
To resolve it, enable unprivileged user namespaces on the host (for example, sysctl kernel.unprivileged_userns_clone=1, and on AppArmor-restricted distros kernel.apparmor_restrict_unprivileged_userns=0) and restart. If you intend to run an assistant-only agent, set autonomy.profile: assistant explicitly to silence the warning.
There is never a silent unjailed fallback. A failed precondition always lands on the assistant posture (orchestration disabled) — the agent stays safe and you are told. The actual host probe that produces the preflight result ships with the sandbox jail; the downshift path and its logging are wired independently, so the behavior is legible the moment the probe lands.
The agent reads its own copy of this guidance. A bundled autonomy skill (packages/daemon/bundled-skills/autonomy/) auto-seeds into every agent’s workspace and is surfaced to the model: it is the on-demand, model-facing decision guide for when to reach for an orchestration surface and how to route a multi-step task through orchestrate. A one-paragraph always-on doctrine carrying the same contract and routing rule also rides every run’s system prompt, so the floor applies even when the model never opens the skill.

Capability Model

How capabilities are enforced by a single gate, and why no capability ever implies admin access.

Safety

The budget cap, circuit breaker, and rate limits that bound an autonomous agent.

Config Reference

Every autonomy.* field, its type, and its default.

Security Model

The orthogonal capability axis and the deny-by-origin control-plane boundary.