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>
76 lines
3.8 KiB
YAML
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
|