Moltis
Features

Moltis as an ACP Agent

Moltis has long been an ACP client: it spawns and drives external agents such as codex-acp, claude-agent-acp, and Cursor’s agent acp. See External Agents for that direction.

moltis acp is the inverse. It lets any ACP clientZed, Block’s buzz-acp, or a bespoke harness — drive Moltis the same way it would drive any other agent, while Moltis’s own stack (sessions, memory, tool policy, sandboxing, providers) stays behind the protocol.

Usage

moltis acp

The command speaks JSON-RPC over stdin/stdout and serves exactly one client per process, matching how every ACP harness spawns agents.

A prompt runs a real Moltis turn — the same path the Web UI takes, with your configured providers, tools, memory, and session history — and streams the reply back as session/update notifications while it runs.

No server required

moltis acp boots the Moltis stack inside the spawned process and binds no socket, so you do not start moltis serve first and it will not take a port.

Startup uses a headless profile. Providers, tools, memory, sessions, and configured MCP servers are ready before the protocol starts, while channel accounts and server-only background workers are not started.

Headless startup does not create or inject a gateway API key. There is no gateway listener for sandboxed tools to call in this mode.

If sandbox networking is set to trusted, HTTP tools fail closed in ACP mode. The trusted-domain approval proxy is not started because ACP has no domain approval UI and the headless process does not bind listener ports.

It does open the databases under the data directory, which is what lets an ACP session show up in the Web UI’s session list. A gateway running at the same time shares that state rather than being talked to over a socket.

Checking a client without a model

moltis acp --echo

--echo serves a built-in agent that echoes the prompt back, without loading providers, databases, or tools. Use it to verify a client’s handshake in isolation — if --echo works and a real run does not, the problem is your Moltis configuration rather than the client.

Why this is a surface, not a channel

ACP sits beside the Web UI and GraphQL rather than alongside Telegram or Nostr. A channel exists to manage external correspondents — allowlists, sender identities, OTP flows, per-account settings. An ACP client is the trusted local parent process that spawned Moltis: there is no sender to gate and no account to configure, so none of the channel contract applies. Do not expose the stdio stream to an untrusted process. The controller can ask Moltis to use configured tools and can provide stdio MCP servers, which is equivalent to granting that controller the current user’s privileges.

stdout is the wire

Anything written to stdout that is not protocol framing corrupts the stream and the client disconnects with a parse error. moltis acp therefore redirects all logging to stderr for the lifetime of the process, including at --log-level trace. If you add output to any code path this command reaches, send it to stderr.

Plugging into a client

Any harness that spawns an ACP agent binary can point at Moltis. For example, buzz-acp spawns $BUZZ_ACP_AGENT_COMMAND:

BUZZ_ACP_AGENT_COMMAND=moltis BUZZ_ACP_AGENT_ARGS="acp" buzz-acp

Buzz is one client among several here, not a dependency — Moltis’s Nostr channel integration is a separate, unrelated feature.

Sessions

ACP sessions map onto Moltis session keys in a dedicated acp: namespace, so a moltis acp run cannot collide with a Web UI or channel session. The ACP SessionId is the Moltis session key, which means a client can hand an id straight back to session/load to resume a conversation.

The namespace is enforced, not merely conventional: session/load rejects any id outside acp: with invalid_params before the backend is consulted, so a client cannot name a Web UI or channel session and drive it. session/new likewise refuses to return a key a backend minted outside the namespace.

The absolute cwd supplied by the ACP client becomes the session’s Moltis project and the working directory used by filesystem and execution tools. The directory must exist. Loading an existing session rebinds it to the cwd in that load request.

Client-provided MCP servers

session/new and session/load accept stdio MCP servers. They are scoped to that ACP session: their tools are added only to that session and they are shut down when the client disconnects or replaces the session setup. HTTP and SSE MCP transports are not advertised or accepted.

Each MCP command must be an absolute path. This makes executable selection deterministic; it is not a sandbox. The command runs in the session cwd with only the environment variables explicitly supplied in the ACP request and does not inherit the Moltis process environment. Server names and environment variable names must be unique within the request. Internally, client MCP tools receive a session-specific namespace so they cannot override configured tools or impersonate an MCP server allowed by an agent preset.

Resource limits

The stdio surface rejects JSON-RPC frames over 4 MiB, prompts over 1 MiB or 256 blocks, and MCP setup data over 1 MiB. One connection may retain up to 64 sessions and run up to four session operations concurrently. History replay and turn updates each have an 8 MiB output budget; persisted history reads stop at that limit before parsing or allocating the remainder of the session file. These limits bound accidental or malicious memory use and prevent one controller from starting an unlimited number of simultaneous provider calls.

Protocol payload bodies are never written to Moltis logs, including at trace level. MCP response bodies and child-process stderr are likewise omitted because they may contain prompts, tool results, or credentials. In ACP mode, payload- processing tracing targets are hard-filtered from both stderr and the in-memory gateway log buffer, so a more-specific RUST_LOG directive cannot re-enable them.

Protocol support

MethodStatus
initializeSupported; negotiates the version and advertises capabilities
authenticateNo-op — the client is a trusted local parent process
session/newSupported; returns a namespaced Moltis session key
session/promptSupported; streams session/update notifications, then returns a stop reason
session/cancelSupported; aborts the in-flight turn, resolving prompt with cancelled
session/loadSupported when the backend can resume; rejects ids outside the acp: namespace, then replays history before responding
fs/*, terminal/*Not requested from the client
session/request_permissionNot yet routed through Moltis’s tool gate

Unknown session ids are rejected with invalid_params.

Only text and resource-link prompt blocks are accepted. Image, audio, and embedded-resource blocks are rejected because those capabilities are not advertised.

What a turn streams

Moltis broadcasts a run’s progress as the same events the Web UI renders. They map onto session/update like this:

MoltisACP
reply tokensagent_message_chunk
reasoningagent_thought_chunk (sent incrementally)
tool call startedtool_call with status in_progress
tool call finishedtool_call_update with status completed or failed

Tool updates include capped raw arguments and output or error content. When a turn finishes, Moltis reconciles streamed reply chunks with the authoritative final message and emits any missing suffix before returning end_turn.

Web-UI affordances without an ACP equivalent — queueing, iteration counters, voice-pending markers — are dropped rather than shown, so the client’s transcript is what the agent actually said.

Tool calls are reported, not gated. A client sees tools as they run but cannot veto them: session/request_permission is not yet wired to Moltis’s tool policy, so tool approval remains governed by your Moltis configuration, not by the ACP client. If you need a client to approve individual tool use, that is not available yet.

Architecture notes

The protocol crate declares its traits with #[async_trait(?Send)], so the handler is pinned to the thread running a tokio::task::LocalSet, while Moltis’s services are Send + Sync. The AcpBackend trait in crates/acp is where the two meet: implementations are Send + Sync and never learn a LocalSet exists, and streaming flows back through a plain channel that the protocol layer drains while the turn runs.