Adding an engine driver
Connect a new agent engine to Murage: use a custom ACP or OpenAI-compatible instance with no code, or write a ProviderDriver with a contract test.
An engine is the program that does a bot's thinking and acting. Murage talks to every engine through the same small interface, so adding one never touches the app's UI. Before you write code, check whether you need to.
Option 1: no code, any ACP agent
If your agent CLI speaks the Agent Client Protocol over stdio, point a customAcp instance at it in ~/.murage/config.json:
{
"instances": {
"my-agent": {
"driver": "customAcp",
"displayName": "My Agent",
"environment": { "MY_AGENT_TOKEN": "..." },
"config": { "cli": "my-agent acp" }
}
}
}
config.cliis the whole command, arguments included. You can also set it in the app: Settings → Engines → Set CLI… on the instance's row.- The driver has no sign-in flow of its own. Run the CLI once in a terminal and sign in there; Murage spawns it with your login intact.
- The model picker shows one "Agent default" entry. Whatever the CLI is set to run is what runs.
environmentis passed to the CLI. Keys that belong to other engines are stripped, so a custom CLI can't bill against another engine's login.- If the agent asks for permission through ACP, the request becomes a normal approval card.
Option 2: no code, an OpenAI-compatible endpoint
The built-in openai-compat driver talks to any OpenAI-compatible endpoint, hosted or local. It's chat only: no tools, agents, files, computers or connected apps.
{
"instances": {
"my-endpoint": {
"driver": "openai-compat",
"displayName": "My Endpoint",
"environment": { "MY_ENDPOINT_KEY": "sk-..." },
"config": {
"url": "https://api.example.com/v1",
"apiKeyEnv": "MY_ENDPOINT_KEY",
"model": "my-model"
}
}
}
}
Restart Murage after editing config.json. The file is written owner-only, but environment values sit in it as plain text, so use keys scoped to that one engine.
For local models with tools, use Settings → Models → Local models instead. It finds Ollama, LM Studio, llama.cpp, vLLM and others, and tests whether a model can really call tools.
A typo in driver or an invalid config never breaks the app: the instance shows as unavailable with the reason, and every other engine still loads.
Option 3: write a driver
Write a driver when an engine needs its own protocol handling, install recipe, model catalog or sign-in detection. The interface lives in server/contracts.ts; read it first.
The pieces
A driver is a ProviderDriver:
driverKind: a unique string.metadata: display name, whether several instances are allowed, and how the picker presents it.install(optional): per-platform install commands for Mac, Windows and Linux, a docs link and a sign-in command. Leave it out for API-key engines that need no local program.models: a default model and the options the picker shows.decodeConfig(raw): turn the stored config into your typed config.defaultConfig().create(input): build a liveProviderInstance.
The instance exposes snapshot() (available or unavailable, with a reason, sign-in state and version), and an adapter with sendTurn, interruptTurn, respondToRequest, hasSession, stopAll and onEvent.
The rules
These come from CONTRIBUTING.md and a driver PR must follow them:
- Add
server/drivers/<name>.tsimplementingProviderDriver, and register it with one line inserver/drivers/builtIn.ts. decodeConfigthrows on invalid config, andcreaterejects (never throws synchronously) on failure. The registry turns both into an unavailable shadow instead of crashing the fleet. Don't remove or work around that.- Emit only canonical
RuntimeEvents carrying your owndriverKind. The bus drops events from the wrong driver. - A missing or broken CLI must show as
snapshot()returning{ state: "unavailable", reason }, and a failed spawn as a failed turn. Never a hang, never a crash. - Bring a contract test following the fake-CLI pattern: a scripted fake process plus
recordEvents.
Platform rules that bite drivers
- Never build command strings for a shell. No
shell: true, no spawning throughcmd.exewith quoted strings. Model names, personas and MCP config travel through argv. On Windows, resolve.cmdshims to their JS entry and spawnprocess.execPath. - POSIX-only calls such as
process.kill(-pid)need a gated Windows equivalent such astaskkill /T, not a silent failure. - Never log keys, echo them in events or put them in argv where another local process could read them.
Tool schemas
If your engine consumes MCP tools, remember that every engine converts tool schemas in its own lossy way. Murage's rule for tool inputSchemas is: no oneOf, anyOf, allOf, const or format. Advertise one flat object and put per-variant rules in descriptions. Accept inputs with one obvious meaning even if they're shaped a little wrong, and when you refuse one, say what's supported with an example the model can copy.
Testing
Use the fakes in server/testing/. The Claude and Codex contract tests (server/drivers/claude.test.ts, server/drivers/codex.test.ts) are the models to copy: they spawn scripted fake CLIs, assert the event stream, argument and environment hygiene, interrupts and the permission broker, and switch failure modes with environment variables. Don't mock child_process, and don't sleep in tests.
Before you start a big one
Open an issue first and agree on the approach. One driver per pull request.
See also: Architecture overview and Contributing.