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>
92 lines
4.0 KiB
JavaScript
92 lines
4.0 KiB
JavaScript
// 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;
|
|
}
|