agap-mcp: MediaWiki + Todoist tools, vault trust-gate, registry wiring

Commits a cluster of entangled agap-mcp / Adolf-tooling WIP that had accumulated
uncommitted in shared files (server.js, the three MCP-config layers). Bundled as
one commit because server.js interleaves all of it and cannot be cleanly split;
each stream is named here for the record. Authorized by alvis 2026-07-23.

- **kb#95 — family MediaWiki tools:** new src/mediawiki.js (wiki_search / wiki_read
  / wiki_edit, MediaWiki login->CSRF->edit flow, no new deps), registered in
  server.js and fetched from the family.alogins.net Vaultwarden login item.
  Proven standalone against family.alogins.net (search/read/edit, revid 1520 on a
  bot-userspace page). Wired into all three layers: openai/shared-mcp.json,
  adolf/openclaw.json, openai/agent-registry.yaml.

- **kb#147 — vault trust-gate (A2A-15), DORMANT:** new src/trust-gate.js (+ two
  test files), requireVaultAccess() around the vw_* tools, gated by
  AGAP_MCP_ENFORCE_VAULT_TRUST (docker-compose.yml, default 0). OFF by default —
  vw_* behaviour is byte-for-byte unchanged until an operator sets ENFORCE=1 and
  populates AGAP_MCP_AGENT_TOKENS from Vaultwarden. That activation is a separate
  human step; kb#147 remains escalated for human verification and is NOT verified
  by this commit. js-yaml added to read the registry. agent-registry.yaml mounted
  read-only as the trust-class source of truth.

- **Todoist tools:** new src/todoist.js (initTodoist + 6 todoist_* tools),
  registered in server.js, sourced from the TODOIST_TOKEN Vaultwarden item.

- **kanboard cutover cleanup:** removes src/kanboard.js and its imports — the
  kanboard_* slice moved to the standalone kanboard-mcp on 2026-07-06.

- **openai/validate_capability_grants.py:** cross-checks the registry against the
  live openclaw.json + shared-mcp.json layers; passes (exit 0).

No secrets committed: all tokens come from Vaultwarden via env/.env; the trust
gate's AGAP_MCP_AGENT_TOKENS defaults to `{}` (fail-closed). node_modules/ now
gitignored, package-lock.json tracked.

NOT YET ACTIVATED: agap-mcp has not been rebuilt and adolf-llm/adolf not
restarted, so the wiki/todoist tools are wired but not live. That restart is the
outstanding step on kb#95 (and stays a human/orchestrator action).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-07-23 06:48:07 +00:00
parent 288cb6b36a
commit b548a8f345
15 changed files with 2876 additions and 365 deletions

View File

@@ -44,11 +44,18 @@ schema_version: 1
# ── trust classes (§5) ──────────────────────────────────────────────────
# "human > trusted > sandboxed > untrusted". Numeric rank lets routing/grant
# code do `>=` comparisons instead of string-matching an ordered list.
#
# default_budget_usd/budget_duration (kb#147): defense-in-depth defaults fed
# into each agent's LiteLLM virtual-key spec (agent_registry.py:
# litellm_key_spec()) when the agent doesn't override them. Moot for money
# TODAY (§3a: no metered API by default, only free local-small is reachable
# without opt-in) but real once anything metered is opted into a tier pool —
# a budget cap should already exist rather than being bolted on later.
trust_classes:
human: { rank: 3, note: "vault access yes; not MCP-tool-scoped, IS the reasoning" }
trusted: { rank: 2, note: "vault access yes (DECIDED, kb#147); outward actions per ask-first rules" }
sandboxed: { rank: 1, note: "no vault, no outward sends; scoped MCP allowlist; KB access project-scoped" }
untrusted: { rank: 0, note: "anything ingesting the open web; its OUTPUTS are tainted, not just its access restricted" }
trusted: { rank: 2, note: "vault access yes (DECIDED, kb#147); outward actions per ask-first rules", default_budget_usd: 50.0, budget_duration: "30d" }
sandboxed: { rank: 1, note: "no vault, no outward sends; scoped MCP allowlist; KB access project-scoped", default_budget_usd: 5.0, budget_duration: "30d" }
untrusted: { rank: 0, note: "anything ingesting the open web; its OUTPUTS are tainted, not just its access restricted", default_budget_usd: 0.0, budget_duration: "30d" }
# ── runtimes ─────────────────────────────────────────────────────────────
# Backbones deliberately OUTSIDE model-registry.yaml's `models:` list.
@@ -88,11 +95,83 @@ agents:
mcp_servers: [hindsight, openclaw-tools, kanboard, marketplace, agap]
gateway_tools: [cron, nodes, browser] # openclaw.json gateway.tools.allow, live 2026-07-21
vault_access: true # trusted-only per §5 DECIDED; reaches vw_* via the `agap` MCP server
# kb#144 (A2A-12): PER-TOOL scoping, TWO layers — 2026-07-22, second
# pass after a live-verified miss on the first. Each server below
# keeps its own justification comment in its config; this is the
# registry's copy of the same lists (source of truth this side —
# validate_capability_grants.py cross-checks against BOTH).
#
# Layer 1 — OpenClaw's own mcp.servers.*.toolFilter.include in
# adolf/openclaw.json, applied when OpenClaw (the `adolf` container)
# builds ITS OWN tool bundle. Landed first pass, verified schema-valid
# via `openclaw config validate` / `openclaw mcp probe`. Real, correct
# for OpenClaw's own client — but NOT what determines the model's
# actual per-turn context on Adolf's kimi backbone.
#
# Layer 2 — shared-mcp.json's per-server `enabledTools`, which
# adolf-llm/server.js's writeMcpConfig() seeds into each Kimi CLI
# session's project-root .mcp.json (Gate 1). THIS is the layer that
# actually reaches the model: Kimi CLI auto-discovers that file, not
# OpenClaw's config, and applies its own McpServerCommonFields.
# enabledTools/disabledTools via computeEnabledNames (an allowlist
# when only enabledTools is set — confirmed by decompiling the
# installed @moonshot-ai/kimi-code package's dist/main.mjs, both
# copies of the function, packages/agent-core{,-v2}/src/agent/mcp/
# connection-manager.ts). The first kb#144 pass got this backwards —
# see the release comment on kb#144 for the exact wire.jsonl proof
# (tool counts unchanged post-restart) that caught it: Layer 1 alone
# is invisible to Kimi.
#
# Counts (same tool lists both layers, confirmed identical by
# validate_capability_grants.py, exit 0):
# agap 32->24, hindsight 29->9, kanboard 23->14,
# openclaw-tools 5->5 (already minimal, no filter needed).
# marketplace stays UNFILTERED at layer 1 (7/13 kept) but is not in
# shared-mcp.json AT ALL — Kimi's session never had it in the first
# place (pre-existing gap between what OpenClaw offers Adolf and
# what reaches Kimi, out of kb#144's scope to close).
# Reachable-by-Kimi total: hindsight+kanboard+openclaw-tools+agap
# 102-13(marketplace, never reached Kimi)=89 -> 9+14+5+24=52 tools
# (-42%). Byte-measured (chars/4) against each server's real
# tools/list JSON schemas: est. ~7K tokens saved/turn — estimate
# pending the real wire.jsonl number, which needs the adolf-llm
# container restart alvis owns (shared-mcp.json is bind-mounted
# read-only but adolf-llm's server.js caches its content at process
# start, so editing the file alone does not take effect — see
# capability_grant_status below for the confirm-post-restart command).
#
# kb#95 (2026-07-23): added wiki_search/wiki_read/wiki_edit (family
# MediaWiki / РодоВики, family.alogins.net) to agap-mcp and to both
# layers' agap allowlist below — Adolf's persona domain (relatives,
# dates, events), same reasoning as HA/Zabbix/Todoist above. This ages
# the counts comment above (agap 32->24, total 52) by +3/+3; not
# recomputed here since it needs the same live wire.jsonl proof kb#144
# used and this task does not touch the running containers (see
# shared_mcp_kimi_allowlist below for the exact confirm command).
mcp_tool_filter:
hindsight: [recall, retain, reflect, list_memories, get_memory, update_memory, list_directives, create_directive, delete_directive]
kanboard: [kanboard_list_projects, kanboard_get_project, kanboard_list_tasks, kanboard_my_tasks, kanboard_get_task, kanboard_search_tasks, kanboard_list_users, kanboard_project_activity, kanboard_create_task, kanboard_update_task, kanboard_move_task, kanboard_change_task_status, kanboard_assign_task, kanboard_add_comment]
marketplace: [marketplace_find_best, marketplace_search, marketplace_get_product, marketplace_get_recommendations, marketplace_get_reviews, marketplace_compare_prices, marketplace_status]
agap: [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]
openclaw-tools: null # no filter in openclaw.json — already minimal (5/5 kept)
note: >
"scoped core tools" per kb#134's brief — this IS openai/openclaw.json's
live mcp.servers block, not a narrower aspirational allowlist. Per-tool
(not per-server) scoping + LiteLLM virtual-key budgets are kb#147's job;
this field is the input #147 consumes.
"scoped core tools" per kb#134's brief, now REAL at both levels: this
IS openai/openclaw.json's live mcp.servers block (server selection)
plus its per-server toolFilter.include (tool selection, kb#144). Not
a narrower aspirational allowlist — validate_capability_grants.py
cross-checks both against the live config. LiteLLM virtual-key
budgets remain kb#147's separate job; this field is the input #147
consumes for MCP scope (litellm_key_spec() handles the model side).
capability_grant: # kb#147 — the enforcement input for LiteLLM + agap-mcp
litellm_key_alias: adolf
mcp_auth_token_env: AGAP_MCP_TOKEN_ADOLF # secret lives in Vaultwarden + this container's env, never in git; see agap-mcp/docker-compose.yml AGAP_MCP_AGENT_TOKENS
note: >
Reachable model tiers derived at read time from preferred_tier via
agent_registry.py:litellm_key_spec() — not duplicated here. Actual
virtual-key provisioning happens via openai/provision_litellm_keys.py
against the live LiteLLM proxy; NOT run automatically by this
registry (privileged action, requires the LiteLLM master key —
kb#147 handover step, see task comment).
memory:
banks:
- { id: adolf-alvis, role: private, interlocutor: alvis }
@@ -127,6 +206,10 @@ agents:
Widest-scoped agent by design (this session's own tool surface) —
gated by ask-first rules on outward/destructive actions rather than
MCP allowlisting. Per kb#147, a real virtual-key budget still applies.
capability_grant:
litellm_key_alias: claude-coder
mcp_auth_token_env: AGAP_MCP_TOKEN_CLAUDE_CODER
note: "same mechanism as adolf's capability_grant above; see that note."
memory:
banks: []
model: "session (ephemeral, per invocation) + repo state (git history, CLAUDE.md files, kanboard task/comment history) — no persistent Hindsight bank"
@@ -150,6 +233,15 @@ agents:
vault_access: false # sandboxed — hard rule §5, no exceptions
outward_sends: false
note: "scoped to mcp__marketplace__* tools only; no gitea/ha/zabbix/radicale, no kanboard project outside its own."
capability_grant:
litellm_key_alias: torgash
mcp_auth_token_env: AGAP_MCP_TOKEN_TORGASH
note: >
Not provisioned yet (agent not built — see persona.prompt_source
above). litellm_key_spec('torgash') already resolves correctly
against the registry today (preferred_tier: small -> models=[local-
small's litellm_model_name] only, no large/paid-fallback) — verified
by kb#147's provision_litellm_keys.py --dry-run.
memory:
banks: [{ id: torgash, role: private }]
current_state: "bank not yet created — target state, same as the persona itself"
@@ -178,6 +270,10 @@ agents:
native_tools: [WebSearch, WebFetch]
vault_access: false
outward_sends: false
capability_grant:
litellm_key_alias: researcher
mcp_auth_token_env: AGAP_MCP_TOKEN_RESEARCHER
note: "not provisioned yet (agent not built) — same verification status as torgash's capability_grant above."
memory:
banks: [{ id: researcher, role: private }]
current_state: "bank not yet created — target state"
@@ -293,3 +389,84 @@ routing_consumption:
>= required, and current a(t) == 1 (resolved via agent_registry.py's
effective_card(), which dereferences `backbone` into model-registry.yaml
or `runtimes:` above) — no submitter-side hardcoded agent id needed.
# ── kb#147 (A2A-15) implementation status ────────────────────────────────
# What "enforced" means as of this task, per enforcement point (§5 lists
# three: agap-mcp vault tools, OpenClaw per-agent MCP scoping, LiteLLM
# virtual keys). Recorded here — not just in the KB task comment — because
# this file is the grants source of truth the design demands.
capability_grant_status:
agap_mcp_vault_gate: >
IMPLEMENTED, tested, NOT ACTIVATED. agap-mcp/src/trust-gate.js resolves
a bearer token -> agent id -> trust rank (reading THIS file, mounted
read-only) and agap-mcp/src/server.js gates every vw_* tool behind it.
Proven with two real, run-now test suites (no live container touched):
src/trust-gate.test.mjs (pure logic, 10/10) and
src/trust-gate-http.test.mjs (real HTTP request/response, 4/4) — the
latter shows a sandboxed-agent token, no token, and an unknown token all
get "vault access denied" while a trusted-agent token passes. OFF by
default (AGAP_MCP_ENFORCE_VAULT_TRUST=0 in docker-compose.yml) so this
change is zero-impact until an operator: (1) generates real per-agent
bearer tokens, stores them in Vaultwarden, wires them into
AGAP_MCP_AGENT_TOKENS, (2) sets AGAP_MCP_ENFORCE_VAULT_TRUST=1, (3)
restarts the agap-mcp container — deliberately not done by this task
(never restart the live agap-mcp service unattended).
litellm_virtual_keys: >
SPEC'D, NOT PROVISIONED. agent_registry.py:litellm_key_spec() computes
each agent's model allow-list (derived from preferred_tier x
model-registry.yaml routing.tiers, non-metered only unless opted in)
and a default budget from trust_classes[...].default_budget_usd.
openai/provision_litellm_keys.py turns that spec into LiteLLM
/key/generate calls. Verified with --dry-run (prints the exact payload
per agent, no network call) — actually creating keys needs
LITELLM_MASTER_KEY against the live proxy, a privileged write this task
does not perform unattended; see the kb#147 task comment for the exact
command to run once approved.
openclaw_mcp_allowlist: >
STRUCTURAL at both server AND tool level, cross-checked (kb#144 extended
this from server-only). adolf/openclaw.json's mcp.servers block is
adolf's real, live MCP surface (git-controlled): its server set matches
tool_allowlist.mcp_servers, and each server's toolFilter.include (added
kb#144 first pass) matches tool_allowlist.mcp_tool_filter — exactly.
openai/validate_capability_grants.py checks this automatically, read-only,
no live changes. Real and correct for OpenClaw's OWN MCP client surface —
but per the kb#144 first-pass release comment's wire.jsonl proof, this
layer alone does NOT reach the model on Adolf's kimi backbone (see
shared_mcp_kimi_allowlist below, the layer that does). NOT restarted by
this task for this file's change either way (openclaw.json is
bind-mounted read-only as the live config; `docker compose restart adolf`
is the activation step, alvis's call).
shared_mcp_kimi_allowlist: >
STRUCTURAL, cross-checked, NOT YET ACTIVATED — kb#144 SECOND pass
(2026-07-22), added after live verification (restart + one real turn,
wire.jsonl inspection) proved the first pass's openclaw.json-only fix
left Kimi's actual tool bundle unchanged (23/32/29/5, not 14/24/9/5).
Root cause: Kimi CLI auto-discovers a project-root `.mcp.json` that
adolf-llm/server.js's writeMcpConfig() seeds from openai/shared-mcp.json
— a completely separate config from openclaw.json, read by a separate
MCP client (Kimi CLI inside the adolf-llm container, not OpenClaw inside
the adolf container). shared-mcp.json now carries the same per-server
tool lists as `enabledTools` (Kimi's own allowlist field —
McpServerCommonFields.enabledTools, applied via computeEnabledNames;
confirmed by decompiling the installed @moonshot-ai/kimi-code package's
dist/main.mjs, both copies of the function/schema, no live container
touched). openai/validate_capability_grants.py now cross-checks THIS
file too (load_shared_mcp_enabled_tools), same exit-0-or-fail contract
as the openclaw.json check. marketplace is absent from shared-mcp.json
entirely (pre-existing: Kimi's session never had it) — not asserted by
the validator for that server, by design, not a gap this task opened.
NOT YET ACTIVATED: shared-mcp.json IS bind-mounted read-only into
adolf-llm (`./shared-mcp.json:/shared-mcp.json:ro` in
openai/docker-compose.yml) so the file on disk is already what the
container would read — but adolf-llm/server.js loads it ONCE into a
module-level variable at process start (not per-request), so editing the
file alone does not take effect; `docker compose restart adolf-llm` is
the only remaining step, deliberately not run by this task (never
restart a live service unattended). Confirm the real post-restart
per-turn token delta via the adolf-llm container's Kimi session wire
log: `docker exec adolf-llm sh -c "tail -1
/root/.kimi-code/sessions/*/agents/main/wire.jsonl"` (after one real
turn against a NEW session, since existing sessions' .mcp.json is
rewritten on their next turn too) and compare per-server tool counts
against 9/14/5/24 (hindsight/kanboard/openclaw-tools/agap) — the exact
same command the first-pass verification used to catch the miss.