comis-agent is the shell-fluent CLI an agent runs from inside its own sandbox to orchestrate, schedule, send, search, fetch, and read — the same actions it can take through the typed orchestration tools or the orchestrate(script) surface, expressed as a command line. Who it’s for: agent authors and operators reasoning about what an autonomous agent can do, and confirming it cannot reach more than the typed tools already allow.
Overview
Comis exposes orchestration to an agent through three sibling surfaces that all converge on the same RPC handlers and the samerequireCapability gate:
- The typed orchestration tools —
session.spawn,graph.execute,cron.add,message.send, the web/read tools (the tool-call surface). - The
orchestrate(script)surface — a sandboxed script that calls those same methods over the capability socket (see Orchestrate). comis-agent— this CLI: the same calls, expressed as subcommands.
comis-agent adds no new capability, no new RPC handler, no new gate, and no new transport. Each subcommand routes through the existing capability socket to the existing handler, which enforces the existing requireCapability check. The CLI does no client-side authorization of its own — it sends the call and lets the endpoint decide. That single-gate design is enforced by architecture tests (comis-agent-same-gate.test.ts / comis-agent-no-admin.test.ts): a future subcommand that reached a weaker, admin, or denied path would fail pnpm test:architecture.
The CLI is opt-in and off by default — it is only available inside a jail where the daemon has bound the (sha256-pinned, read-only) binary and minted a capability lease.
Subcommands and capabilities
comis-agent exposes exactly 13 subcommands. Each maps 1:1 to a capability that the typed tools already reach — the capability is derived from the existing capability maps (TOOL_CAPABILITY_MAP / HANDLER_CAPABILITY_MAP), never restated by the CLI:
The first ten subcommands dispatch to a capability-mapped tool or method and are gated by
requireCapability; a lease that does not hold the mapped capability is denied at the endpoint (the same denial the typed tool would receive). The last three — whoami, status, list — are self-scoped reads: any valid lease reaches the agent’s own introspection and session reads (the request carries no agentId; the daemon resolves the caller’s own scope, so an agent can never read another agent’s sessions). status list is accepted as a two-token alias for list.
The 1:1-to-capability table is the same gate the typed tools reach.
comis-agent spawn and the session.spawn tool both arrive at the session.spawn handler behind requireCapability("orch:spawn") — there is no separate, weaker CLI path. This is proved by comis-agent-same-gate.test.ts, which enumerates every CLI_SUBCOMMAND_MAP target and asserts it resolves to the same capability-map gate.The wire: the lease capability socket
comis-agent connects to the loopback capability endpoint over a Unix socket (COMIS_ORCH_SOCKET), presenting its capability lease (COMIS_CAP_LEASE) as the bearer. It sends one newline-delimited JSON line per call — a capability-mapped tool rides tool.invoke, a direct method (session.spawn, cron.add, …) is sent as that method — and the endpoint resolves the lease’s capabilities and dispatches through the same handler gates as every other origin.
This Unix socket is the only egress from the jail. comis-agent does not use the gateway — it never opens an operator-token WebSocket and never reaches the gateway port. The lease is audience-bound: a lease captured from one jail cannot be replayed against a foreign method, and the endpoint strips any caller-supplied internal _-prefixed field before injecting the lease identity, so the CLI cannot forge an agentId even if one appears in its arguments.
COMIS_ORCH_SOCKET / COMIS_CAP_LEASE), it fails loudly with a non-zero exit and an error naming both environment variables and “jail” — it never silently runs against the host and never hangs.
No admin reach: the closed doors
comis-agent has no admin subcommands. There is no secrets, config, tokens, gateway, agents, providers, models, channels, or env verb — they do not exist in the CLI. Even if an agent tried to reach admin another way, three independent barriers stop it:
- The operator
comisbinary is not on the jail PATH — it cannot be invoked from inside the sandbox. - The capability lease holds no admin capability — admin actions are not in the
orch:*capability set a lease can carry. - Admin handlers deny by origin — a call arriving over the capability socket is rejected at the admin handlers regardless of what it claims.
orchestrate(script) path — the same closed door, the same denial. This is proved by comis-agent-no-admin.test.ts, which asserts the CLI table’s intersection with both the derived admin-method set and the denylisted-method set is empty.
skill is excluded by design
There is no skill subcommand, and this is intentional, not a gap. Every skill-mutation method (skills.create, skills.update, skills.delete, skills.import, skills.upload, and skills.list) is on the capability socket’s denylist — the skills_manage mitigation (the same control that gates the SIGUSR2 skill-reload path). The endpoint’s denylist pre-check rejects those methods before capability resolution, for every origin. So the orchestrate(script) surface hits exactly the same closed door.
Offering a skill subcommand would advertise a path that is always denied, so it is omitted. The substrate denylist is not relaxed for the CLI — a skill attempt through comis-agent is denied identically to the script path. A future attempt to re-add a skill-like entry aborts the module at load (the CLI_SUBCOMMAND_MAP soundness assertion) rather than shipping a false affordance.
Pinned and honest: the bound binary
Thecomis-agent binary is a defense surface, so it is locked down:
- sha256-pinned. A committed build manifest (
comis-agent-manifest.json) pins the sha256 of the Comis-built binary. At jail construction the daemon re-hashes the bound bytes and refuses to bind a binary whose hash does not match the pin — a swapped or tampered binary is never made available. - Read-only bound. The binary is bind-mounted read-only (
--ro-bind) into the jail. A writable interpreter or binary inside a sandbox is a host-RCE vector; the read-only bind (plus the writable-path audit) ensures a write to it from inside the jail fails. - In-jail path via
COMIS_AGENT_BIN. When the binary is bound, the daemon setsCOMIS_AGENT_BINto its in-jail path so the CLI resolves. Operators never set this — it is daemon-managed.
COMIS_AGENT_BIN unset, and the comis-agent CLI surface is unavailable. Critically, this degrade is scoped: the orchestrate(script) surface still runs (a script needs Node; it does not need the CLI binary). There is never a silent bind of an unverified binary and never a silent disappearance of the CLI — the WARN announces it.
The CLI surface and the script surface are independent. A tampered/missing
comis-agent binary disables only the CLI; it does not refuse the whole jail. (Contrast: an unavailable in-jail Node refuses the jail entirely — a script cannot run without it.)Worktrees and one-shot spawns
spawn composes with isolated git worktrees and the broader autonomy patterns:
spawn --worktree
comis-agent spawn --worktree runs the child agent in an isolated git worktree — a separate working tree on its own branch, so concurrent agents do not collide on one checkout. Lifecycle:
- Auto-clean only if unchanged. When the child finishes, its worktree is removed only if it is provably pristine — the predicate is an empty
git status --porcelain(no modified, staged, or untracked files) andHEADequal to the base ref (no commits ahead). Anything else means there is work to keep. - Dirty or ahead worktrees are preserved. A worktree with uncommitted changes, untracked files, or commits ahead of base is never deleted. The cleanup can only ever remove a tree that has nothing in it — it can never discard an agent’s work.
- Orphan-sweep. Worktrees whose directory is gone are pruned; completed-and-clean ones are removed; dirty/ahead/in-progress ones are skipped. A removal failure logs a WARN and preserves the entry rather than dropping it silently or aborting the whole sweep.
spawn --async, tmux, and resume
spawn --asyncis the one-shot, fire-and-forget spawn — it rides the already-async-onlysession.spawn; the caller does not block on the child.- tmux multi-agent runs through the jailed terminal driver — the same sandboxed CLI-driving surface, so a driven multi-agent tmux session is bound by the same jail.
- Resume composes with durable runs — a spawned run that supports resume is resumable through the durability layer; the CLI adds no separate resume mechanism.
Exit behavior
Related
Operator CLI
The host
comis CLI — operator token, gateway transport, admin commandsOrchestrate
The script surface: the same capability socket, same gate
Capability Model
The orch:* capabilities, leases, and deny-by-origin
Sandbox
The read-only sha256-pinned bound binary as a defense layer
Environment Variables
COMIS_AGENT_BIN, COMIS_CAP_LEASE, COMIS_ORCH_SOCKET
Autonomy
Autonomy profiles, budgets, and outward quotas
