adolf: bearer-authenticate the agap MCP server, fix audio config schema

openclaw.json now sends `Authorization: Bearer ${AGAP_MCP_TOKEN}` to the agap
MCP server, which requires it as of kb#180. The token is injected from
openai/.env via docker-compose.yml and only substituted here, never inlined.
It maps to agent id `adolf`, which is also what the kb#147 vault gate reads.

Fixes tools.media.audio, which had been added but never restart-validated:
the per-entry `apiKey: "not-needed"` is rejected by the schema
("tools.media.audio.models.0: Invalid input"), and an invalid config makes the
gateway refuse to start outright -- adolf crash-looped on the first restart
after the block landed. The old comment claimed the schema requires a
non-empty apiKey; it is the opposite, apiKey is not a valid per-entry key at
all. Isolated with `openclaw config validate` against the running image
(2026.6.11): {provider, model} and {provider, model, baseUrl} validate, and
adding apiKey alone reproduces the failure. baseUrl is kept -- that is the
per-entry override pointing the openai-shaped provider at the local
faster-whisper server. Provider auth follows the normal model auth order per
docs/nodes/audio.md, and faster-whisper-server has no auth to satisfy anyway.

Two lessons encoded in the comments: `enabled: false` does NOT exempt an entry
from schema validation, and a config edit is not done until a restart boots
healthy -- this sat invalid but latent because the running gateway still held
an older loaded config. The block stays enabled: false; turning STT on is
still a kb#175/#191 decision (GTX 1070 co-residency).

Also adds the proactive-prioritization and todoist-capture design notes and
the vw-mcp prototype.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-07-30 04:41:13 +00:00
parent fc4e1c75ed
commit a5c625b9b6
12 changed files with 1049 additions and 3 deletions

View File

@@ -29,6 +29,71 @@
},
},
// Audio understanding (kb#175, STT source 2: "local STT on Agap").
// OpenClaw's bundled media-understanding pipeline (docs/nodes/media-
// understanding.md) auto-transcribes inbound audio attachments before the
// agent turn runs -- no plugin code needed, this is pure config. A voice
// note sent to Adolf over the existing Matrix DM (source 1, see below)
// gets transcribed by the `openai`-shaped entry below, which is redirected
// via baseUrl/apiKey to the LOCAL faster-whisper server (openai/docker-
// compose.yml's `faster-whisper` service, same compose project as this
// container, reachable by service name) instead of hosted OpenAI --
// confirmed supported via src/media-understanding/runner.entries.ts's
// per-entry baseUrl override (docs/gateway/config-tools.md "Tools and
// custom providers"). apiKey is a dummy: faster-whisper-server has no
// auth, but OpenClaw's schema requires a non-empty value.
//
// ⚠️ NOT ACTIVATED YET (2026-07-26): the `faster-whisper` container does
// not exist (never started -- `docker ps -a` shows no such container).
// Starting it opens a 4th tenant on the single 8GB GTX 1070 already at
// ~6.2GB with bge-m3 + gemma3:4b + tei-reranker (~1.8GB headroom) and
// could evict tei-reranker, silently breaking Hindsight recall (see
// DESIGN-a2a-agents.md §3b and kb#191, which is the unimplemented
// residency-guard/VRAM-alert task -- still in Backlog). This config
// block is deliberately inert until #191 lands or alvis explicitly
// accepts the co-residency risk; `enabled: false` below.
// SCHEMA FIX 2026-07-30 (kb#180 restart). This block previously carried a
// per-entry `apiKey: "not-needed"`, which the schema REJECTS:
// tools.media.audio.models.0: Invalid input
// and an invalid config makes the gateway refuse to start outright -- adolf
// crash-looped on the first restart after the block was added. Two lessons
// encoded here: `enabled: false` does NOT exempt an entry from schema
// validation, and a config edit is not "done" until a real restart boots
// healthy (this block sat invalid but latent because the running gateway
// still held an older loaded config).
//
// The old comment claimed the schema "requires a non-empty apiKey" -- it is
// the opposite: `apiKey` is not a valid per-entry key at all. Verified with
// `openclaw config validate` against this image (2026.6.11): entries with
// {provider, model} and {provider, model, baseUrl} validate; adding
// `apiKey` is the sole cause of the failure. Per docs/nodes/audio.md,
// provider auth follows the normal model auth order (auth profiles, env
// vars, models.providers.*.apiKey) -- and faster-whisper-server has no auth
// to satisfy anyway, so no key belongs here. `baseUrl` is kept: that is the
// per-entry override that redirects the `openai`-shaped provider to the
// LOCAL faster-whisper server.
tools: {
media: {
audio: {
// Still deliberately inert: the `faster-whisper` container does not
// exist, and starting it opens a 4th tenant on the 8GB GTX 1070 (see
// the kb#191 residency-guard note above). This block is now merely
// SCHEMA-VALID rather than boot-breaking; flipping this to true is a
// separate decision that still belongs to kb#175/#191.
enabled: false,
echoTranscript: true, // let the sender see what Adolf heard before it acts
echoFormat: '📝 "{transcript}"',
models: [
{
provider: "openai",
model: "deepdml/faster-whisper-large-v3-turbo-ct2", // must match WHISPER__MODEL in openai/docker-compose.yml
baseUrl: "http://faster-whisper:8000/v1",
},
],
},
},
},
// Browser tool — bundled plugin, off by default. Enables a dedicated,
// agent-only headless Chromium profile ("openclaw") driven through the
// gateway's loopback control service. Chromium is already in the image
@@ -146,9 +211,27 @@
// of which Adolf's Matrix persona drives turn-to-turn; reach the
// hindsight MCP directly (unscoped) for that admin work instead of
// paying for it on every Adolf turn. 29 tools -> 9.
//
// kb#169: this raw MCP surface previously pointed at /mcp/adolf/ --
// the single unpartitioned bank with content from every human's
// conversations. #153 scopes the hindsight-memory PLUGIN's
// recall/retain hooks by interlocutor, but this MCP tool surface is a
// second, independent path to memory that #153 does not touch: a
// model call to e.g. `recall` here bypassed interlocutor scoping
// entirely. Repointed to /mcp/adolf-shared/ (option 2 of #169) --
// the household-shared bank (0 facts as of 2026-07-26, pre-existing
// per agent-registry.yaml's memory.banks target list). This surface
// can now only ever read/write the shared bank, never a private one,
// regardless of which human is talking to Adolf -- safe by
// construction, no dependency on #153 landing first. Option 1 (drop
// entirely) was not chosen because, until #153's scoped plugin tools
// land, this is still Adolf's only path for explicit "remember
// this"/"what do you recall" turns; option 3 (dynamic per-interlocutor
// bank selection) is not supported -- this config's url is a single
// static bank_id per MCP server entry, not a per-request parameter.
hindsight: {
type: "http",
url: "http://hindsight:8888/mcp/adolf/",
url: "http://hindsight:8888/mcp/adolf-shared/",
toolFilter: {
include: ["recall", "retain", "reflect", "list_memories", "get_memory", "update_memory", "list_directives", "create_directive", "delete_directive"],
},
@@ -210,7 +293,7 @@
},
},
// agap-mcp (kb#64) -- the SAME shared agap-mcp instance Claude Code uses
// (network_mode: host, :3100, unauthenticated on localhost). Grants Adolf
// (network_mode: host, :3100, bearer-authenticated since kb#180). Grants Adolf
// the same access as Claude: vault (vw_* for credential fetching) plus
// gitea/ha/zabbix/radicale. Reached via host.docker.internal like
// kanboard/marketplace above.
@@ -228,8 +311,24 @@
agap: {
type: "http",
url: "http://host.docker.internal:3100/mcp",
// kb#180: agap-mcp's listener is authenticated now (DESIGN §4 --
// :3100 is host-networked and the LAN carries VPN peers, so an
// open JSON-RPC listener handed ha_call_service/wiki_edit/todoist
// writes to anyone). Same pattern as marketplace above:
// AGAP_MCP_TOKEN lives in Vaultwarden, is injected into this
// container via openai/.env -> docker-compose.yml, and is only
// substituted here -- never inlined. The token maps to agent id
// `adolf` in AGAP_MCP_AGENT_TOKENS, which is also what the kb#147
// vault gate reads to allow vw_* (adolf = trust_class trusted).
headers: {
Authorization: "Bearer ${AGAP_MCP_TOKEN}",
},
toolFilter: {
include: ["vw_get_password", "vw_get_item", "vw_list_items", "vw_create_login", "vw_update_password", "ha_get_state", "ha_list_entities", "ha_call_service", "ha_get_history", "zabbix_get_problems", "zabbix_get_hosts", "zabbix_get_items", "zabbix_get_triggers", "radicale_list_calendars", "radicale_list_events", "radicale_get_event", "radicale_put_event", "radicale_delete_event", "radicale_move_event", "todoist_list_tasks", "todoist_list_projects", "todoist_create_task", "todoist_update_task", "todoist_complete_task", "wiki_search", "wiki_read", "wiki_edit"],
// kb#170: added todoist_capture_idea (classify + create in one
// call — see agap-mcp/src/capture.js) so Adolf's proactive-
// secretary persona can capture+tag an idea in one tool call
// instead of list_projects+create_task+manual tagging.
include: ["vw_get_password", "vw_get_item", "vw_list_items", "vw_create_login", "vw_update_password", "ha_get_state", "ha_list_entities", "ha_call_service", "ha_get_history", "zabbix_get_problems", "zabbix_get_hosts", "zabbix_get_items", "zabbix_get_triggers", "radicale_list_calendars", "radicale_list_events", "radicale_get_event", "radicale_put_event", "radicale_delete_event", "radicale_move_event", "todoist_list_tasks", "todoist_list_projects", "todoist_create_task", "todoist_update_task", "todoist_complete_task", "todoist_capture_idea", "wiki_search", "wiki_read", "wiki_edit"],
},
},
},
@@ -280,6 +379,15 @@
"kimi-quota-footer": {
enabled: true,
},
// Todoist idea capture (kb#170 component 1) — installed external
// plugin, bind-mounted read-only from openai/todoist-capture-plugin
// (see that project's docker-compose.yml adolf.volumes) onto
// .openclaw/extensions/todoist-capture. Registers a `/idea` native
// command; no hooks (no allowConversationAccess/allowPromptInjection
// needed) — it POSTs straight to agap-mcp's /capture-idea route.
"todoist-capture": {
enabled: true,
},
},
},
}