Files
AgapHost/agap-mcp/docker-compose.yml
alvis fc4e1c75ed agap-mcp: authenticate the :3100 listener (kb#180), pin bw CLI, add capture/classifier
Listener auth (kb#180, DESIGN-a2a-agents.md §4)
-----------------------------------------------
agap-mcp binds :3100 on every interface (network_mode: host) and the LAN
carries VPN-terminated peers, so an unauthenticated JSON-RPC listener handed
ha_call_service / gitea_wiki_write / wiki_edit / radicale+todoist writes and
POST /capture-idea to any LAN peer. Only vw_* was gated before (kb#147), and
only at ENFORCE=1.

src/listener-auth.js now requires `Authorization: Bearer <token>` resolving to
a known agent id on every route except /health, which stays open so a
misconfigured token map is still diagnosable. Two gates stay deliberately
layered and independently switchable: "are you an agent at all?" (this file)
vs "are you trusted enough for the vault?" (trust-gate.js), both reading the
same token map.

Also closes an SSE session-hijack hole: /messages previously trusted any
sessionId with no credential, so a guessed or leaked id was full tool access.
Sessions are now pinned to the caller identity captured at the /sse handshake,
comparing agent id *and* token.

Auth defaults ON, and boot fails loudly if the token map is empty rather than
serving 401 to everyone while /health reports ok. Rollback is
AGAP_MCP_REQUIRE_AUTH=0.

Verified live: unauthenticated and bad-token /mcp -> 401, unauthenticated
/capture-idea -> 401, /health -> 200, both real agent tokens -> 200 with 36
tools, including from inside the adolf container.

Pin the bw CLI
--------------
The Dockerfile installed @bitwarden/cli unpinned. Rebuilding jumped
2026.2.0 -> 2026.7.0, whose WASM cipher deserializer rejects any stored login
carrying `"uri": null` ("invalid type: JsValue(Object({...})), expected a
string") -- 33 of 49 items in this vault have that shape. `bw list` then exits
1, server init fails, and the container crash-loops. Pinned to 2026.2.0.

Do not unpin: 2026.7.0 cannot authenticate against this Vaultwarden
(2025.12.0) at all -- it refuses plain HTTP outright and 404s on the identity
endpoint over HTTPS. Updating the CLI requires upgrading Vaultwarden first.

capture / classifier
--------------------
Adds the POST /capture-idea REST endpoint and the idea classifier behind it
(consumed by the todoist-capture plugin), with tests. Carried in the same
commit because server.js wires both this and the auth boot path.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-30 04:40:51 +00:00

76 lines
3.8 KiB
YAML

services:
agap-mcp:
build: .
restart: unless-stopped
network_mode: host
environment:
- PORT=3100
- NODE_TLS_REJECT_UNAUTHORIZED=0
- HTTPS_PROXY=
- HTTP_PROXY=
- ALL_PROXY=
- https_proxy=
- http_proxy=
- all_proxy=
- BITWARDENCLI_APPDATA_DIR=/bw-data
- BW_EMAIL=adolf46@proton.me
- BW_PASSWORD=${BW_PASSWORD}
- VAULTWARDEN_URL=http://localhost:8041
- GITEA_URL=http://localhost:3000
- HA_URL=http://192.168.1.4:8123
- ZABBIX_URL=http://192.168.1.4:81
- RADICALE_URL=http://localhost:5232
- RADICALE_USER=alvis
# kb#147 (A2A-15) — vault trust gate, OFF by default (0). Flipping this
# to 1 requires a container restart AND real values for
# AGENT_REGISTRY_PATH/AGAP_MCP_AGENT_TOKENS below to be populated first
# (see src/trust-gate.js) — that restart is the deliberate handover
# step this task does NOT perform (never restart the live agap-mcp
# service unattended). Until both are set, vw_* tools behave exactly
# as before this change.
- AGAP_MCP_ENFORCE_VAULT_TRUST=0
- AGENT_REGISTRY_PATH=/agent-registry.yaml
# JSON map {"<bearer-token>": "<agent-id>"}. Real per-agent tokens must
# be generated, stored in Vaultwarden (e.g. AGAP_MCP_TOKEN_ADOLF,
# AGAP_MCP_TOKEN_CLAUDE_CODER), and referenced here via .env — never
# inlined in this committed file. Empty object = no caller resolves to
# any agent, i.e. fail-closed once ENFORCE is turned on.
- AGAP_MCP_AGENT_TOKENS=${AGAP_MCP_AGENT_TOKENS:-{}}
# kb#180 (DESIGN-a2a-agents.md §4) — authentication of the LISTENER
# itself, a strictly larger gate than the vw_*-only one above. With
# this on (the default in code), /mcp, /sse, /messages and
# /capture-idea all require `Authorization: Bearer <token>` resolving
# to an agent id in AGAP_MCP_AGENT_TOKENS; /health stays open for this
# healthcheck. :3100 is bound on every interface (network_mode: host)
# and the LAN carries VPN-terminated peers, so an open listener means
# any peer can call ha_call_service / gitea_wiki_write / wiki_edit /
# todoist writes.
#
# ACTIVATION IS NOT AUTOMATIC-SAFE: with auth on and
# AGAP_MCP_AGENT_TOKENS empty, the process REFUSES TO START (loud
# crash instead of denying every caller while /health says ok). So
# AGAP_MCP_AGENT_TOKENS must be populated in this directory's .env
# BEFORE the next restart of this service, and every caller
# (Adolf/shared-mcp.json, Claude Code .claude.json, the
# todoist-capture-plugin) must be given its token — see the kb#180
# migration list. Set AGAP_MCP_REQUIRE_AUTH=0 in .env only as a
# deliberate emergency rollback to the old open listener.
- AGAP_MCP_REQUIRE_AUTH=${AGAP_MCP_REQUIRE_AUTH:-1}
volumes:
- /home/alvis/.config/Bitwarden CLI:/bw-data
# Read-only: agent-registry.yaml is the version-controlled source of
# truth for trust classes (kb#134/kb#147) — mounted, never copied, so
# a registry edit takes effect on container restart with no rebuild.
- /home/alvis/agap_git/openai/agent-registry.yaml:/agent-registry.yaml:ro
# kb#190: /health responds 200 with no auth/side effects (confirmed).
# This is a SEPARATE compose project from openai/docker-compose.yml
# (network_mode: host, reached from adolf-llm etc. via
# host.docker.internal), so it cannot be wired into that file's
# depends_on/condition chain -- this only gives it its own status.
healthcheck:
test: ["CMD-SHELL", "node -e \"fetch('http://localhost:3100/health').then(r=>process.exit(r.ok?0:1)).catch(()=>process.exit(1))\""]
interval: 15s
timeout: 10s
retries: 5
start_period: 20s