Context
Every runtime Fountain has is an LLM coding agent: claude, codex, gemini, opencode, each a module under apps/fountain/lib/fountain/runtimes/ implementing the Fountain.Runtimes behaviour and speaking ACP (ADR 0014). That is the right shape for the product, and it leaves out a class of work Fountain is otherwise perfect for: a deterministic program that wants a warm machine, a vault, a thread, a schedule, and a human who can read what happened.
The concrete consumer is chant, which is replacing its Temporal-backed ops runtime with Fountain. A chant op is a convergent verb (chant lifecycle apply --env prod, a converge tick, a drift diff) that should run on a schedule inside a persistent sandbox that holds the repo and the toolchain, with the run visible as a turn in a teammate's thread. chant will ship an ACP server (chant acp) so it looks like any other agent to Fountain. What it cannot do from outside is be launched: the runtime registry is a closed map, Fountain.Runtimes.Model expects a provider/model_id, and a turn resolves inference credentials it will never use.
Proposal
Add one runtime, acp, that launches a command the agent config names and speaks ACP to it over stdio exactly as the four existing runtimes do.
agents.runtime accepts "acp". A new field, runtime_command (string, argv or a shell line, resolved inside the sandbox), is required for this runtime and rejected for the others.
model is optional for this runtime. No inference credential is resolved, and no model.failed stage can occur.
prepare_sandbox/*, write_config/2, default_env/* and skills_root/0 are no-ops or minimal: the command owns its own config. Skills still mount, since a deterministic agent may read them.
- Permission requests,
session/cancel, tool-call blocks and stop reasons work unchanged, because they are ACP.
- Metering: with
CREDITS_ENABLED the turn is priced by sandbox time only. There is no token usage to record; turn.usage is null.
- The console and
fountain agents create expose the field. fountain acp (ADR 0015) needs no change.
The one product question is whether runtime_command is a free string or a catalogue entry. A free string is what a self-hoster wants and what the trusted-mode runner already implies. On the hosted service the command runs inside the sandbox under the same isolation as any agent, so it is no wider than a setup_script. I would start with the free string.
Acceptance criteria
Dependencies and scope
No dependency on other issues. The consumer-side work is tracked in chant; that epic link will be added here once it exists. Out of scope: a catalogue of blessed commands, any change to the four LLM runtimes, and the durable-request change filed separately.
Context
Every runtime Fountain has is an LLM coding agent:
claude,codex,gemini,opencode, each a module underapps/fountain/lib/fountain/runtimes/implementing theFountain.Runtimesbehaviour and speaking ACP (ADR 0014). That is the right shape for the product, and it leaves out a class of work Fountain is otherwise perfect for: a deterministic program that wants a warm machine, a vault, a thread, a schedule, and a human who can read what happened.The concrete consumer is chant, which is replacing its Temporal-backed ops runtime with Fountain. A chant op is a convergent verb (
chant lifecycle apply --env prod, a converge tick, a drift diff) that should run on a schedule inside a persistent sandbox that holds the repo and the toolchain, with the run visible as a turn in a teammate's thread. chant will ship an ACP server (chant acp) so it looks like any other agent to Fountain. What it cannot do from outside is be launched: the runtime registry is a closed map,Fountain.Runtimes.Modelexpects aprovider/model_id, and a turn resolves inference credentials it will never use.Proposal
Add one runtime,
acp, that launches a command the agent config names and speaks ACP to it over stdio exactly as the four existing runtimes do.agents.runtimeaccepts"acp". A new field,runtime_command(string, argv or a shell line, resolved inside the sandbox), is required for this runtime and rejected for the others.modelis optional for this runtime. No inference credential is resolved, and nomodel.failedstage can occur.prepare_sandbox/*,write_config/2,default_env/*andskills_root/0are no-ops or minimal: the command owns its own config. Skills still mount, since a deterministic agent may read them.session/cancel, tool-call blocks and stop reasons work unchanged, because they are ACP.CREDITS_ENABLEDthe turn is priced by sandbox time only. There is no token usage to record;turn.usageis null.fountain agents createexpose the field.fountain acp(ADR 0015) needs no change.The one product question is whether
runtime_commandis a free string or a catalogue entry. A free string is what a self-hoster wants and what the trusted-mode runner already implies. On the hosted service the command runs inside the sandbox under the same isolation as any agent, so it is no wider than asetup_script. I would start with the free string.Acceptance criteria
runtime: "acp"withruntime_commandcreates an agent; the same value withoutruntime_command, orruntime_commandon another runtime, is a 422 that names the field.tool_call/tool_call_update/agent_message_chunkblocks to the transcript and the SSE feed, and ends the turn on the ACP stop reason.session/cancelviaPOST /api/conversations/:id/interruptreaches the command and the turn endsinterrupted.session/request_permissionfrom the command produces apermission_requestblock and honours the agent's policy, unchanged from the LLM runtimes.modelis required; a turn on an account with no credentials succeeds.docs/concepts/permissions.mdand the runtime field indocs/sdk.mdgain a row foracp.Dependencies and scope
No dependency on other issues. The consumer-side work is tracked in chant; that epic link will be added here once it exists. Out of scope: a catalogue of blessed commands, any change to the four LLM runtimes, and the durable-request change filed separately.