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:
91
agap-mcp/src/trust-gate.js
Normal file
91
agap-mcp/src/trust-gate.js
Normal 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;
|
||||
}
|
||||
Reference in New Issue
Block a user