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 noautonomy 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
standard and holds exactly those surfaces, capability-gated. It
is an explicit grant via a named profile, not a compatibility shim.
Named Profiles
Setautonomy.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. Underunattendedthere 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.
unattendeddoes 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 understandard. 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
denialBreakerNconsecutive floor blocks (default 5) on one run, the denial breaker trips: the run is aborted (anexecution:abortedwith reasondenial_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 anunattendedrun misbehaves, an admin can callautonomy.evictwith itsrootRunId. This is demote-but-continue: the run is not killed — it keeps running, but underdefaultfrom 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 fromlease.revoke(cooperative stop) andrun.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 asdefault(the safe profile) — never something broader. This posture is controlled byevictOnPolicyUnreachable(default true), and it is the same safe target theautonomy.evictdemotion lands on.
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 anorchestrate 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 onlyprofile: (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
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.aggregateUsdis the priced 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 (maxConcurrentSelfAgentsis the alias of the flat field; depth defaults to 3, fan-out to 5).outward.{ originOnly, perTargetGrants, volumeCap }— outward-send governance.originOnlykeeps sends to the agent’s own channel by default; a new target needs an explicitperTargetGrantsentry;volumeCapbounds a mass-recipient send.
Connected MCP Tools
Theorch: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:
allowis the per-server inbound allowlist — the operative gate. Each key is a connected server’s name; itstoolsis the explicit list of tool names reachable on that server (there is no*wildcard — you name each tool), andclassificationis the inbound tier. Absent fromallow⇒ denied by absence.enabledgrantsorch:mcpto theassistantprofile (which has autonomy off and no floor caps) as an explicit opt-in. On standard/unattended/max the cap is already floor-held, soenabledis not required there (it is a harmless no-op for the grant). There is no separate booleanmcp:toggle besideweb/analyze/write/browse; the nested block’s ownenabledfield is the assistant-opt-in signal, mirroringdurability.enabled.
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 anorchestrate 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 onautonomy.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
- 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 paststaleHeartbeatMs(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_started→unknown_after_send→committed) 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 directmessage.send/reply/reactpath 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 typedcomis_tools.message_send/reply/reactoutward methods dispatch that samemessage.*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 onceautonomy.durability.enabledis 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.killpoisons the persisted record (flips it torevoked), 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.)
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: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 theassistant profile (no orchestration
surfaces, the agent only replies) and says so, two ways:
- A WARN at boot naming the downshift, with an actionable hint and
errorKind: "precondition". - A doctor finding an operator can read, naming the same remediation.
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.
Related
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.
