Files
Void-Homelab/docs/plan-5-complete.md
2026-06-01 19:41:46 +10:00

5.4 KiB

Plan 5 — Complete

Date: 2026-06-01 Version: 2.0.0-alpha.5 Tests: full suite green (run cleanly) — companion adds tool/registry/runtime/api unit + integration tests; no network in tests (injected callModel). Branch: plan5-companion-chat (16 tasks, subagent-driven TDD).

Scope delivered (scope B — knowledge assistant + drafting via approval)

Agent runtime + tools

  • lib/ai/secret.jsresolveSecret('env:KEY' | 'file:/path' | raw). The Anthropic key is configured this way; Vaultwarden swap is a pointer change later.
  • lib/ai/anthropic.jsgetAnthropicClient() (key via resolver) + makeCallModel({client,model}) returning a stable callModel({system,messages,tools,onTextDelta}) -> {text,toolUses,stopReason,usage}. Wraps client.messages.stream() + finalMessage() (verified against @anthropic-ai/sdk@0.40.1). DEFAULT_MODEL=claude-sonnet-4-6.
  • lib/ai/agent/registry.jscreateRegistry(){registerTool,getTool,listTools,toAnthropicTools}. Extensible; handlers stripped from the Anthropic schema.
  • Four v1 tools (lib/ai/agent/tools/): search (wraps repos/search.js fts), read (whitelisted table by kind), context (resolves the active view entity), propose_change (capability-gated draft → pending_changes, never applies). Wired in tools/index.js as companionRegistry.
  • lib/ai/agent/runtime.jsrunTurn(...) tool-use loop. Streams deltas, runs tools via the registry, builds the correct Anthropic assistant/tool_result turn structure, collects draft ids, emits one tool event per call (status done/error) + draft events, and stops at maxIterations (guard).

API + persistence

  • Migration 007conversations.space_id (FK, indexed) + seeds the default companion agent (read+suggest, no write).
  • conversations.findOrCreateForSpace — one ambient open conversation per Space+agent.
  • lib/api/routes/companion.jsGET /api/spaces/:id/companion (history) and POST …/companion/turn (SSE). Persists the user message, runs runTurn with the agent actor (so drafts are suggest-tier), streams events as SSE frames, persists one assistant message with {tool_trace, draft_ids, usage} in metadata. callModel from app.locals in tests, real client otherwise. Mounted in lib/api/index.js.

Frontend

  • public/sse.jsstreamTurn() reads an authenticated POST→SSE stream (EventSource can't send the bearer token).
  • public/markdown.jsrenderMarkdown = DOMPurify.sanitize(marked.parse(...)) (reuses existing vendor bundles).
  • public/components/rightrail.js — the chat UI (approved label-led + left/right design). Loads history, streams turns, renders tool chips + inline draft cards (approve/reject → existing /api/pending-changes/:id/{approve,reject}, shared with the Inbox). Safe-DOM throughout; assistant markdown only via the sanitized html: path.
  • public/state.js + public/app.jsstate.spaceId/state.view set in renderView, broadcast via a space-active event so the rail loads the right space on initial load, navigation, and between spaces (same-space re-renders are guarded to preserve the conversation).

Security

  • propose_change never applies — verified by review + test (target table stays empty; only a pending_changes row is written). Capability enforced via canAct; the route passes the agent actor (kind:'agent'), not the owner, so even allow-tier never auto-applies in v1.
  • Prompt-injection containment is structural — untrusted content can at most yield a draft requiring owner approval.
  • XSS — no unsanitized path to the DOM; user text is a text node, assistant markdown is DOMPurify-sanitized (review-confirmed).
  • Cost guardrails — per-turn max_tokens + maxIterations loop guard.

Deviations from the plan (all reviewed)

  • Plan test snippets used ../../lib/pool.js; real path is lib/db/pool.js — corrected.
  • pages uses body_md (+ requires slug); fts returns title_or_snippet — tests adjusted.
  • A plan note string didn't match its own test regex (T7) — reconciled.
  • Runtime emits ONE tool event per call (not running+done) to match the test; the UI renders a chip per event accordingly.
  • Rail initial-load wired through the space-active state event (cleaner than a hashchange listener).

Open items for the user

  • Live smoke + deploy (alpha-5). Standing rule: no production deploy without explicit OK. Needs a real ANTHROPIC_API_KEY in /opt/void-server/.env on CT 311 (the .216 box). Snapshot CT 310 + 311 first, then TARGET=root@192.168.1.216 ./deploy/push.sh, verify /health = alpha-5, and do the manual chat smoke (ask a question → tool chips + streamed answer; "create a task" → inline draft → approve → appears in /api/tasks and clears from Inbox).
  • Rail accent colour — the app's --accent is blackflame orange #ff4f2e, so the rail renders orange, not the mockup's violet. Add a --rail-accent if you want the purple.
  • Branchplan5-companion-chat is ready to merge to main once you've signed off (use finishing-a-development-branch).

What's left after Plan 5

  • Scope C — multi-agent personas + per-persona local (Ollama) models via agents.model.
  • MCP server — re-expose companionRegistry to external agents.
  • Plan 6 — Sacred Valley widgets ported from Void 1.x.
  • Vaultwarden — swap the env/file key for a vault item id.