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:
101
agap-mcp/src/trust-gate.test.mjs
Normal file
101
agap-mcp/src/trust-gate.test.mjs
Normal 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`);
|
||||
Reference in New Issue
Block a user