Files
Void-Homelab/docs/superpowers/specs/2026-06-04-yerin-online-design.md
2026-06-04 20:59:12 +10:00

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): 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 routeGET /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 regressioncompanion.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.