diff --git a/lib/db/migrations/011_yerin.sql b/lib/db/migrations/011_yerin.sql new file mode 100644 index 0000000..e1b1fac --- /dev/null +++ b/lib/db/migrations/011_yerin.sql @@ -0,0 +1,10 @@ +-- Seed Yerin, the security agent. READ-ONLY by capability (no suggest/write): +-- she investigates and reports; remediation goes through you. Runs on the same +-- claude CLI subscription path as Dross (model NULL = server default); switch to +-- a local Ollama model later by setting agents.model. See docs/yerin-security-agent.md. +INSERT INTO agents (slug, name, kind, model, capabilities) +VALUES ( + 'yerin', 'Yerin', 'claude', NULL, + '{"read":true,"suggest":false,"write":false}'::jsonb +) +ON CONFLICT (slug) DO NOTHING; diff --git a/lib/mcp/companion-stdio.js b/lib/mcp/companion-stdio.js index dc4d0d0..f2c036a 100644 --- a/lib/mcp/companion-stdio.js +++ b/lib/mcp/companion-stdio.js @@ -20,8 +20,16 @@ import { CallToolRequestSchema } from '@modelcontextprotocol/sdk/types.js'; import { companionRegistry } from '../ai/agent/tools/index.js'; +import { securityRegistry } from '../ai/agent/tools/security/index.js'; import { buildCtxFromEnv } from './context.js'; +// Which toolset this stdio server exposes, selected at launch via +// VOID_TOOL_REGISTRY (default: Dross's companion tools; 'security' = Yerin's). +const REGISTRIES = { companion: companionRegistry, security: securityRegistry }; +function resolveRegistry(env = process.env) { + return REGISTRIES[env.VOID_TOOL_REGISTRY] || companionRegistry; +} + // --------------------------------------------------------------------------- // Transport-free helpers (exported for testing) // --------------------------------------------------------------------------- @@ -30,8 +38,8 @@ import { buildCtxFromEnv } from './context.js'; * Returns the list of MCP tool descriptors (no handler) from the companion registry. * @returns {{ name: string, description: string, input_schema: object }[]} */ -export function listMcpTools() { - return companionRegistry.listTools().map(({ name, description, input_schema }) => ({ +export function listMcpTools(env = process.env) { + return resolveRegistry(env).listTools().map(({ name, description, input_schema }) => ({ name, description, input_schema @@ -45,8 +53,8 @@ export function listMcpTools() { * @param {object} ctx – tool context {agent, space_id, view, actor} * @returns {Promise} raw handler result */ -export async function callMcpTool(name, args, ctx) { - const tool = companionRegistry.getTool(name); +export async function callMcpTool(name, args, ctx, env = process.env) { + const tool = resolveRegistry(env).getTool(name); if (!tool) { throw new Error(`Unknown tool: ${name}`); } diff --git a/tests/mcp/registry_select.test.js b/tests/mcp/registry_select.test.js new file mode 100644 index 0000000..74c7ab0 --- /dev/null +++ b/tests/mcp/registry_select.test.js @@ -0,0 +1,40 @@ +import { describe, it, expect, beforeAll } from 'vitest'; +import { pool } from '../../lib/db/pool.js'; +import { resetDb } from '../helpers/db.js'; +import { migrateUp } from '../../lib/db/migrate.js'; +import { listMcpTools, callMcpTool } from '../../lib/mcp/companion-stdio.js'; + +// The stdio MCP server must be able to expose Yerin's securityRegistry instead +// of Dross's companionRegistry, selected by VOID_TOOL_REGISTRY at launch. + +describe('MCP registry selection', () => { + it('defaults to the companion registry when VOID_TOOL_REGISTRY is unset', () => { + const names = listMcpTools({}).map(t => t.name).sort(); + expect(names).toEqual(['context', 'propose_change', 'read', 'search']); + }); + + it('selects the security registry when VOID_TOOL_REGISTRY=security', () => { + const names = listMcpTools({ VOID_TOOL_REGISTRY: 'security' }).map(t => t.name).sort(); + expect(names).toContain('audit_log'); + expect(names).toContain('agent_inventory'); + expect(names).not.toContain('propose_change'); // Yerin cannot propose mutations + }); + + it('callMcpTool routes to the selected registry', async () => { + await resetDb(); await migrateUp(); + const out = await callMcpTool('agent_inventory', {}, {}, { VOID_TOOL_REGISTRY: 'security' }); + expect(Array.isArray(out.agents)).toBe(true); + // an unknown tool for the default registry must not be reachable in security mode + await expect(callMcpTool('propose_change', {}, {}, { VOID_TOOL_REGISTRY: 'security' })) + .rejects.toThrow(/unknown tool/i); + }); +}); + +describe('migration 011 seeds Yerin', () => { + it('creates a read-only yerin agent', async () => { + await resetDb(); await migrateUp(); + const { rows: [y] } = await pool.query(`SELECT * FROM agents WHERE slug='yerin'`); + expect(y).toBeTruthy(); + expect(y.capabilities).toEqual({ read: true, suggest: false, write: false }); + }); +});