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,101 @@
// Proof-of-enforcement for kb#147, run with: node src/trust-gate.test.mjs
//
// Deliberately does NOT touch the live agap-mcp container, LiteLLM, or
// Vaultwarden — it exercises the exact exported functions server.js calls
// (trustRankOf/vaultAllowed/resolveCallerAgent/isVaultTool) against a
// synthetic registry + token map, so this is a real test of the enforcement
// logic itself, not a mock of it.
import assert from 'node:assert/strict';
import {
trustRankOf,
vaultAllowed,
resolveCallerAgent,
isVaultTool,
trustedRankThreshold,
_resetRegistryCacheForTests,
} from './trust-gate.js';
const registry = {
trust_classes: {
human: { rank: 3 },
trusted: { rank: 2 },
sandboxed: { rank: 1 },
untrusted: { rank: 0 },
},
agents: [
{ id: 'adolf', trust_class: 'trusted' },
{ id: 'claude-coder', trust_class: 'trusted' },
{ id: 'torgash', trust_class: 'sandboxed' },
{ id: 'researcher', trust_class: 'sandboxed' },
{ id: 'kimi-endpoint', trust_class: 'untrusted' },
],
};
const tokenMap = {
'tok-adolf': 'adolf',
'tok-claude-coder': 'claude-coder',
'tok-torgash': 'torgash',
'tok-researcher': 'researcher',
};
let passed = 0;
function check(label, fn) {
fn();
passed++;
console.log(`ok - ${label}`);
}
check('trusted rank threshold resolves from registry', () => {
assert.equal(trustedRankThreshold(registry), 2);
});
check('trusted agents (adolf, claude-coder) can reach vault', () => {
assert.equal(vaultAllowed('adolf', registry), true);
assert.equal(vaultAllowed('claude-coder', registry), true);
});
check('sandboxed agents (torgash, researcher) CANNOT reach vault', () => {
assert.equal(vaultAllowed('torgash', registry), false);
assert.equal(vaultAllowed('researcher', registry), false);
});
check('untrusted agent cannot reach vault', () => {
assert.equal(vaultAllowed('kimi-endpoint', registry), false);
});
check('unauthenticated caller (no token resolved) cannot reach vault', () => {
assert.equal(vaultAllowed(null, registry), false);
assert.equal(trustRankOf(null, registry), -1);
});
check('unknown/unregistered agent id fails closed, not open', () => {
assert.equal(vaultAllowed('some-new-agent-nobody-declared', registry), false);
});
check('resolveCallerAgent maps bearer token -> agent id, else null', () => {
assert.equal(resolveCallerAgent('tok-torgash', tokenMap), 'torgash');
assert.equal(resolveCallerAgent('tok-adolf', tokenMap), 'adolf');
assert.equal(resolveCallerAgent('not-a-real-token', tokenMap), null);
assert.equal(resolveCallerAgent(null, tokenMap), null);
});
check('end-to-end: a sandboxed agent\'s token provably cannot reach vw_* tools', () => {
const callerAgentId = resolveCallerAgent('tok-torgash', tokenMap);
assert.equal(callerAgentId, 'torgash');
assert.equal(isVaultTool('vw_get_password'), true);
assert.equal(vaultAllowed(callerAgentId, registry), false); // <- the acceptance bar
});
check('end-to-end: a trusted agent\'s token can reach vw_* tools', () => {
const callerAgentId = resolveCallerAgent('tok-adolf', tokenMap);
assert.equal(vaultAllowed(callerAgentId, registry), true);
});
check('non-vault tool name is unaffected by the gate', () => {
assert.equal(isVaultTool('gitea_read_file'), false);
assert.equal(isVaultTool('zabbix_get_problems'), false);
});
_resetRegistryCacheForTests(null);
console.log(`\n${passed} passed`);