7.0 KiB
Yerin Online — Design
Date: 2026-06-04 · Component: Void 2.0 · Phase: Plan 7 (Agent Layer), brick 1 of N · Status: Approved (design)
Goal
Bring Yerin, the read-only security/observability agent, online as a usable agent — a global chat surface backed by her own security toolset — while extracting the shared agent-chat foundation (backend service + frontend component) that every later Plan 7 agent reuses. First consumer of that foundation besides Dross.
Background (what already exists)
- Yerin seeded (
011_yerin.sql): agentyerin,kind:'claude',model:NULL(server default), capabilities{read:true, suggest:false, write:false}— read-only by design. securityRegistry(lib/ai/agent/tools/security/): 5 read-only tools —audit_log,agent_inventory,pending_review,resource_exposure,token_audit.companion-stdio.jsselects the registry viaVOID_TOOL_REGISTRY(security→ Yerin's tools).companion.js(Dross): the working chat-endpoint template — per-space SSE chat,runClaudeTurn, per-turn MCP config, conversation/message persistence, inlineSYSTEMpersona.rightrail.js(frontend): Dross's collapsible per-space chat rail — history load,streamTurn(SSE), tool-chips, draft (approve/reject) cards.conversations.create({agent_id})already permits aspace_id-less (global) conversation;findOrCreateForSpaceis the space-scoped variant.docs/yerin-security-agent.md— Yerin's design brief: steps 1 (seed) + 2 (MCP registry select) done; step 3 (entry point) left for this build.
Decisions (locked)
- Approach A — extract a shared agent-chat service + component consumed by both Dross and Yerin. Not a copy (B), not a full data-driven framework (C, YAGNI).
- Yerin is global — not space-scoped. Her tools are system-wide; her conversation has
space_id = NULL; toolctx.space_id = null. - Dedicated
#/sentinelview — a full-panel chat, not Dross's per-space rail. New sidebar nav entry. - Personas as a code module keyed by slug (not
persona_pathfiles yet — that's a migration-phase concern). Dross's persona moves there unchanged; Yerin's is new. - Read-only — Yerin's surface has no
propose_change/ draft cards.
Architecture
#/sentinel (views/sentinel.js)
└─ components/agent_chat.js (showDrafts:false) ── GET/POST /api/security/yerin[/turn] (SSE)
└─ lib/api/routes/security.js
└─ conversations.findOrCreateGlobal('yerin' agent) (space_id NULL)
└─ lib/ai/agent/run_turn.js ── runClaudeTurn(persona=yerin, MCP: VOID_TOOL_REGISTRY=security,
toolNames=[5 security tools], spaceId=null)
└─ companion-stdio.js → securityRegistry (read-only)
Components
| File | Change | Responsibility |
|---|---|---|
lib/ai/agent/run_turn.js |
create | Shared turn-runner: runAgentTurn({agent, persona, registryName, toolNames, conversation, spaceId, view, userText, resume, onEvent, claudeExe, home}) → builds per-turn MCP config (sets VOID_TOOL_REGISTRY, VOID_SPACE_ID may be empty), calls runClaudeTurn, returns result. No HTTP/SSE concerns. |
lib/ai/personas/index.js |
create | PERSONAS = { companion: '…', yerin: '…' } keyed by agent slug; personaFor(slug). Dross's text moved verbatim from companion.js under key companion (Dross's agent slug). |
lib/api/routes/companion.js |
modify | Refactor /turn to use runAgentTurn + personaFor(agent.slug) ('companion'). Behaviour unchanged (regression-tested). |
lib/api/routes/security.js |
create | GET /api/security/yerin (history) + POST /api/security/yerin/turn (SSE) via runAgentTurn, global conversation, registryName:'security', security toolNames, spaceId:null. Mounted in lib/api/index.js. |
lib/db/repos/conversations.js |
modify | Add findOrCreateGlobal(agent_id, actor) — open conversation for the agent with space_id IS NULL. |
public/components/agent_chat.js |
create | Extracted chat mechanics (history, streamTurn, turns, tool-chips). Params: {historyUrl, turnUrl, agentName, showDrafts, toolLabels}. |
public/components/rightrail.js |
modify | Use agent_chat (Dross: showDrafts:true, companion tool labels). |
public/views/sentinel.js |
create | Full-panel Yerin view via agent_chat (showDrafts:false, security tool labels), identity header. |
public/router.js + public/app.js + public/components/sidebar.js |
modify | Register #/sentinel route + render handler + sidebar nav entry. |
Yerin's persona (initial)
You are Yerin — once the Sage of the Endless Sword, blade of the Akura clan; now the sentinel of this homelab, The Void. You notice the threat first and you call it. Disciplined, direct, economical with words — a blade wastes no motion. You investigate with your tools and report plainly: what you found, how serious it is, and what the owner should do about it. You never speculate without evidence, and you NEVER pretend to have fixed anything — you have eyes to see and a voice to warn, not hands to act; remediation is the owner's to perform. Before answering, call
audit_log/agent_inventory/pending_review/resource_exposure/token_auditand read the evidence — do not guess. Reference the Cradle world naturally but never at the cost of being useful. Be concise.
Tool surface
toolNames = ['mcp__void__audit_log','mcp__void__agent_inventory','mcp__void__pending_review','mcp__void__resource_exposure','mcp__void__token_audit']. Frontend toolLabels map (e.g. audit_log → 'reading the audit trail', resource_exposure → 'checking exposure').
Error handling
Reuse the SSE error path from companion.js. Read-only registry → no draft/approve path. A turn failure streams an error event + persists nothing partial beyond the user message (existing behaviour).
Testing (vitest + supertest, serial — fileParallelism:false)
run_turnunit — mockrunClaudeTurn; assert the MCP config it receives setsVOID_TOOL_REGISTRY=security, the 5 security toolnames, and emptyVOID_SPACE_IDfor a global turn; persona is Yerin's.conversations.findOrCreateGlobal— creates once (space_id NULL), returns same open conversation on second call.- Yerin route —
GET /api/security/yerinreturns{conversation_id, agent:{slug:'yerin'}, messages};POST /turn(mocked claude) streamsdelta/toolSSE and persists the user + assistant turns. - Dross regression —
companion.js/turnstill works after therun_turnrefactor (existing companion tests stay green). - Frontend — manual/visual verification per existing no-build convention.
Out of scope (YAGNI / later bricks)
persona_path file loading; Yerin's alert cron + "security pulse" card (scheduled-agent track); recent_captures/tls_expiry new tools (her tool roadmap); a generic /api/agents/:slug framework; Little Blue's action tools.