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

@@ -0,0 +1,91 @@
// trust-gate — kb#147 (A2A-15): vault access = trusted-only, enforced here.
//
// agap-mcp has no per-caller identity today (every MCP client hits the same
// unauthenticated /mcp endpoint on :3100) — that's the gap DESIGN-a2a-agents.md
// v2.1 §5 flags as the crux of "vault access = trusted only" (DECIDED, alvis).
// This module is the enforcement point: it maps a bearer token off the request
// to an agent id (via AGAP_MCP_AGENT_TOKENS, a secret never committed to git),
// then looks up that agent's trust class in agent-registry.yaml (the
// version-controlled source of truth for grants, kb#134) to decide whether
// vault tools (vw_*) may run.
//
// FAIL-CLOSED PRINCIPLE (once enforcement is turned on): no token, an
// unrecognized token, or an agent below `trusted` rank all resolve to "no
// vault access" — there is no default-allow path once AGAP_MCP_ENFORCE_VAULT_TRUST
// is on. See server.js for the off-by-default activation gate: merging this
// module changes zero live behavior until an operator deliberately flips that
// flag AND supplies real per-agent tokens (kb#147 handover step).
import { readFileSync } from 'fs';
import yaml from 'js-yaml';
const VAULT_TOOL_PREFIX = 'vw_';
const DEFAULT_REGISTRY_PATH = '/agent-registry.yaml';
const DEFAULT_TRUSTED_RANK = 2; // matches agent-registry.yaml trust_classes.trusted.rank; used only if the registry can't be read
let _registryCache = null;
export function loadRegistry(path = process.env.AGENT_REGISTRY_PATH || DEFAULT_REGISTRY_PATH) {
if (_registryCache) return _registryCache;
try {
_registryCache = yaml.load(readFileSync(path, 'utf8'));
} catch (e) {
// Fail closed, not fail crash: no registry readable means no agent can be
// proven trusted, so vaultAllowed() below returns false for everyone
// rather than the process refusing to start (agap-mcp serves gitea/ha/
// zabbix/radicale/todoist tools too, which don't depend on this file).
console.error(`trust-gate: could not load agent registry from ${path}: ${e.message}`);
_registryCache = { agents: [], trust_classes: {} };
}
return _registryCache;
}
// Test-only: let unit tests inject a registry object instead of touching the
// filesystem, and let the CLI/tests reset the module-level cache between runs.
export function _resetRegistryCacheForTests(registry = null) {
_registryCache = registry;
}
export function trustedRankThreshold(registry = loadRegistry()) {
return registry.trust_classes?.trusted?.rank ?? DEFAULT_TRUSTED_RANK;
}
export function trustRankOf(agentId, registry = loadRegistry()) {
if (!agentId) return -1; // unauthenticated caller: rank below every real trust class
const agent = (registry.agents || []).find(a => a.id === agentId);
if (!agent) return -1; // unknown agent id: fail closed, not "assume trusted"
const cls = registry.trust_classes?.[agent.trust_class];
return cls ? cls.rank : -1;
}
// tokenMap: { "<bearer-token>": "<agent-id>" } — parsed once at startup from
// AGAP_MCP_AGENT_TOKENS (JSON), itself sourced from per-agent tokens stored in
// Vaultwarden and injected via this container's .env, never inlined in git.
export function loadTokenMap(raw = process.env.AGAP_MCP_AGENT_TOKENS) {
if (!raw) return {};
try {
const parsed = JSON.parse(raw);
return (parsed && typeof parsed === 'object') ? parsed : {};
} catch (e) {
console.error(`trust-gate: AGAP_MCP_AGENT_TOKENS is not valid JSON: ${e.message}`);
return {};
}
}
export function resolveCallerAgent(bearerToken, tokenMap) {
if (!bearerToken) return null;
return tokenMap[bearerToken] || null;
}
export function isVaultTool(toolName) {
return toolName.startsWith(VAULT_TOOL_PREFIX);
}
export function vaultAllowed(agentId, registry = loadRegistry()) {
return trustRankOf(agentId, registry) >= trustedRankThreshold(registry);
}
export function authHeaderToken(req) {
const header = req.headers?.['authorization'] || req.headers?.['Authorization'] || '';
return header.startsWith('Bearer ') ? header.slice(7).trim() : null;
}