docs: Yerin online (Plan 7 brick 1) design spec
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
75
docs/superpowers/specs/2026-06-04-yerin-online-design.md
Normal file
75
docs/superpowers/specs/2026-06-04-yerin-online-design.md
Normal file
@@ -0,0 +1,75 @@
|
||||
# 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.
|
||||
Reference in New Issue
Block a user