5.4 KiB
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.js—resolveSecret('env:KEY' | 'file:/path' | raw). The Anthropic key is configured this way; Vaultwarden swap is a pointer change later.lib/ai/anthropic.js—getAnthropicClient()(key via resolver) +makeCallModel({client,model})returning a stablecallModel({system,messages,tools,onTextDelta}) -> {text,toolUses,stopReason,usage}. Wrapsclient.messages.stream()+finalMessage()(verified against@anthropic-ai/sdk@0.40.1).DEFAULT_MODEL=claude-sonnet-4-6.lib/ai/agent/registry.js—createRegistry()→{registerTool,getTool,listTools,toAnthropicTools}. Extensible; handlers stripped from the Anthropic schema.- Four v1 tools (
lib/ai/agent/tools/):search(wrapsrepos/search.js fts),read(whitelisted table by kind),context(resolves the active view entity),propose_change(capability-gated draft →pending_changes, never applies). Wired intools/index.jsascompanionRegistry. lib/ai/agent/runtime.js—runTurn(...)tool-use loop. Streamsdeltas, runs tools via the registry, builds the correct Anthropic assistant/tool_result turn structure, collects draft ids, emits onetoolevent per call (statusdone/error) +draftevents, and stops atmaxIterations(guard).
API + persistence
- Migration 007 —
conversations.space_id(FK, indexed) + seeds the defaultcompanionagent (read+suggest, no write). conversations.findOrCreateForSpace— one ambient open conversation per Space+agent.lib/api/routes/companion.js—GET /api/spaces/:id/companion(history) andPOST …/companion/turn(SSE). Persists the user message, runsrunTurnwith 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.callModelfromapp.localsin tests, real client otherwise. Mounted inlib/api/index.js.
Frontend
public/sse.js—streamTurn()reads an authenticated POST→SSE stream (EventSource can't send the bearer token).public/markdown.js—renderMarkdown=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 sanitizedhtml:path.public/state.js+public/app.js—state.spaceId/state.viewset inrenderView, broadcast via aspace-activeevent 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_changenever applies — verified by review + test (target table stays empty; only apending_changesrow is written). Capability enforced viacanAct; 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+maxIterationsloop guard.
Deviations from the plan (all reviewed)
- Plan test snippets used
../../lib/pool.js; real path islib/db/pool.js— corrected. pagesusesbody_md(+ requiresslug);ftsreturnstitle_or_snippet— tests adjusted.- A plan note string didn't match its own test regex (T7) — reconciled.
- Runtime emits ONE
toolevent per call (not running+done) to match the test; the UI renders a chip per event accordingly. - Rail initial-load wired through the
space-activestate 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_KEYin/opt/void-server/.envon CT 311 (the.216box). Snapshot CT 310 + 311 first, thenTARGET=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/tasksand clears from Inbox). - Rail accent colour — the app's
--accentis blackflame orange#ff4f2e, so the rail renders orange, not the mockup's violet. Add a--rail-accentif you want the purple. - Branch —
plan5-companion-chatis ready to merge tomainonce 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
companionRegistryto external agents. - Plan 6 — Sacred Valley widgets ported from Void 1.x.
- Vaultwarden — swap the env/file key for a vault item id.