ADR-044: OpenClaw is the sixth managed leaf agent
Status: Accepted Date: 2026-07-10 Deciders: Yuxing Wu Related: reverses the openclaw verdict of ADR-040; completes the correction begun by ADR-042 and recorded in docs/research/openclaw-gateway-integration.md; spec 004-agent-registry (FR-003/FR-003a/FR-013/FR-046/FR-048), spec 008-agent-chat (agent providers), spec 011-provider-switching (projection)
Context
ADR-040 re-added opencode / hermes / cursor as managed agents but kept openclaw out: "a peer gateway, not a leaf", with a five-row capability table declaring four facets N/A. ADR-042's first-principles re-check found three of those five rows wrong (MCP IS injectable, native memory IS disableable, skills ARE drop-in), and the research note reversed the conclusion — openclaw IS a viable managed leaf agent — "pending a real install to verify against."
That install now exists (openclaw 2026.6.11, onboarded, on PATH), and every capability claim below was probed against it — per the ADR-042 discipline that each matrix cell cites a doc or a probe, never an assumption.
Probe-verified capability matrix
| Facet | Mechanism | Evidence |
|---|---|---|
| Chat adapter (spec 008) | openclaw agent --agent main --session-key <key> -m <text> --json --local → stdout is ONE clean JSON blob (payloads[].text, finalAssistantVisibleText, stopReason, completion.finishReason, executionTrace.winnerProvider/winnerModel); logs on stderr. Non-streaming — Coffer's first such provider. --local = fully embedded, no gateway daemon needed, plugin registry preloaded per run. Same --session-key → same session. | live turn, 2026.6.11 |
| Coffer-MCP inject (FR-019) | mcp.servers.<name> in ~/.openclaw/openclaw.json (plain JSON), {command, args?, env?, enabled?} command-map. The container is nested — the manifest's container_key is the dotted path mcp.servers and the JSON transforms descend one object per dot. CAUTION: openclaw rejects interpreter-startup env keys (NODE_OPTIONS, PYTHONPATH, …) inside a server's env at config load; Coffer's entry carries no env. | scratch-config probe: openclaw mcp status lists a hand-written coffer: stdio entry |
| Session-context injection (FR-043/044/048) | PLUGIN_DROP, openclaw flavor: a package dir extensions/coffer-session-context/ (package.json with openclaw.extensions, openclaw.plugin.json {id, configSchema}, index.js exporting definePluginEntry) PLUS plugins.entries.<id>.enabled: true — non-bundled plugins are fail-closed. Hook: api.on("before_prompt_build", h); handler returns {appendSystemContext}. Plugins run in-process at full privilege → can spawn coffer-hook. | openclaw config validate accepts the package + flag pair; live plugin-injection turn (see Consequences) |
| Provider projection (spec 011) | models.providers.coffer = {api, baseUrl, apiKey, models}; apiKey supports ${UPPERCASE_VAR}; model refs provider/model; active model via agents.defaults.model.primary (user fallbacks preserved). models is REQUIRED for a custom provider and each entry needs both id and name; models: [] is valid (the unbound-agent projection). openclaw speaks BOTH openai-completions and anthropic-messages api values. | openclaw config validate probes for each shape, 2026-07-10 |
| Missing-key degradation | A missing env var behind apiKey: "${COFFER_PROVIDER_KEY}" is a startup WARNING only ("Missing env var … feature using this value will be unavailable"), exit 0, other providers unaffected — this overrides the docs' "throws at config load" claim and is what makes codex-style projection viable: persist the reference, inject the key per Coffer-driven turn, and a user-run openclaw without the var degrades gracefully. | probe 2026-07-10 |
| Native-memory disable (FR-046) | plugins.slots.memory: "none" empties the memory plugin slot; restore = remove the key, landing on openclaw's DEFAULT plugin — a user-selected non-default id is not snapshotted (documented FR-046 caveat). | openclaw config validate accepts it |
| Skills (spec 005) | ~/.openclaw/skills/<name>/SKILL.md; SYMLINKED skill folders ARE discovered — Coffer's existing FOLDER delivery works unchanged. | live discovery probe |
| Detection | Default marker ~/.openclaw (env overrides like OPENCLAW_STATE_DIR exist and are treated like every other agent's non-default dir: not resolved). | install layout |
Decision
AgentType.OPENCLAW = "openclaw"is re-added with oneAgentDescriptorrecord — no DB migration (re-adding an enum value the0031migration deleted; old rows were already purged).- The chat adapter is non-streaming: spawn per turn, read stdout to EOF, parse the one blob, emit
TurnStarted→ oneTextDelta→TurnDone(executionTracewinner stamps the assistant message). The upstream session is pinned by a Coffer-derived--session-key coffer-<conversation_id>— nothing is discovered from the stream or persisted per turn. - No cwd semantics. openclaw is a personal-assistant gateway, not a repo-scoped coding CLI:
openclaw agenthas no cwd flag and runs in the agent's own workspace (agents.defaults.workspace). The provider therefore ignores the conversation's cwd (documented, not validated) instead of pretending the turn is project-scoped. - MCP injection extends the JSON transforms with a dotted container path (
mcp.servers) rather than adding a new entry style — the entry itself is the plain command-map Claude Code uses. - Session context ships as a second
PluginFlavor(OPENCLAW) of the existingPLUGIN_DROPmode: three rendered package files plus the fail-closedplugins.entriesenable flag (plugin_enable_config_keyon the spec keeps the hook service generic). Uninstall removes the whole package with no.bak— the content is Coffer-rendered and regenerates byte-identically, and a leftover backup package would still be discovered by openclaw's extension scanner — and removes the enable flag (the config file write keeps its usual.bak). - Provider projection is codex-style (
${COFFER_PROVIDER_KEY}reference on disk, key injected into the subprocess env per Coffer-driven turn), keyed by the connection's wire:openai→api: "openai-completions",anthropic→api: "anthropic-messages". openclaw joins BOTH wires' default compatible-agents sets. - Native memory disable wires
plugins.slots.memory: "none"into the existing FR-046 toggle. - Config-file allowlist:
openclaw.json(keyconfig) + the six workspace instruction files (instructions/soul/identity/user/tools/memory, paths assuming the default~/.openclaw/workspace) + theextensions/directory entry.
Consequences
- Coffer gains its first non-streaming chat provider; the chat surface shows the whole reply at once. No timeout is added on Coffer's side (openclaw's own
--timeoutdefault governs the turn). - Gateway-restart caveat:
--localembedded runs (all Coffer-driven turns) preload the plugin registry per run and see the dropped extension immediately; a long-runningopenclaw gateway(channel-driven use) picks it up only after a restart. Documented in spec FR-048. - openclaw remains also a peer gateway (it orchestrates coding CLIs and hosts channels). Coffer manages its leaf-agent surface only; the gateway-as-LLM-endpoint integration sketched in the research note stays available and unaffected. Note the inheritance effect ADR-040 identified is now a feature: Coffer's writes to
~/.claude/etc. are inherited by openclaw-spawned sub-workers. - The e2e verification for this slice: a live
--localturn with the dropped extension + a stubcoffer-hookconfirmed the injected marker reaches the model (the model repeated it back), and the adapter parsed a real--jsonblob end-to-end.
Alternatives considered
- Keep openclaw out; integrate only as an OpenAI-compatible LLM connection (ADR-040's verdict). Rejected: three of the five capability claims behind it were wrong, and the corrected matrix shows full leaf-facet parity. The connection-style integration remains possible in addition — the two do not compete.
- A new
McpEntryStylefor the nested container. Rejected: the entry shape is the existing command-map; only the container location differs, and a dottedcontainer_keyis data, not machinery. - Uninstall keeps a
.bakof the extension package. Rejected: openclaw scansextensions/for package dirs, so a.bakpackage would still be discovered (as a disabled, stale-id extension); the package is fully Coffer-rendered and reinstall regenerates it byte-identically.