# 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`): agent `yerin`, `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.js`** selects the registry via `VOID_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, inline `SYSTEM` persona. - **`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 a `space_id`-less (global) conversation; `findOrCreateForSpace` is 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) 1. **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). 2. **Yerin is global** — not space-scoped. Her tools are system-wide; her conversation has `space_id = NULL`; tool `ctx.space_id = null`. 3. **Dedicated `#/sentinel` view** — a full-panel chat, not Dross's per-space rail. New sidebar nav entry. 4. **Personas as a code module** keyed by slug (not `persona_path` files yet — that's a migration-phase concern). Dross's persona moves there unchanged; Yerin's is new. 5. **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_audit` and 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`) 1. **`run_turn` unit** — mock `runClaudeTurn`; assert the MCP config it receives sets `VOID_TOOL_REGISTRY=security`, the 5 security toolnames, and empty `VOID_SPACE_ID` for a global turn; persona is Yerin's. 2. **`conversations.findOrCreateGlobal`** — creates once (`space_id NULL`), returns same open conversation on second call. 3. **Yerin route** — `GET /api/security/yerin` returns `{conversation_id, agent:{slug:'yerin'}, messages}`; `POST /turn` (mocked claude) streams `delta`/`tool` SSE and persists the user + assistant turns. 4. **Dross regression** — `companion.js` `/turn` still works after the `run_turn` refactor (existing companion tests stay green). 5. 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.