Adds GET /api/health/services returning registry services grouped by
category with merged cached status and per-group healthy counts, and
POST /api/health/check (owner-only) that enqueues a health.check
pg-boss job. Registers the health_check worker in the jobs index.
Adds HTML5 drag-to-reorder for .sv-card elements in Sacred Valley. The
pure moveId helper is unit-tested. Drop calls PUT /api/dashboard/layout
to persist the new card_order; DOM reflects the new order immediately.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Adds the Plan 6 card framework: svCard() chrome factory, pure orderCards()
ordering helper with unit tests, three stub card modules (clock/weather/host-perf),
and rewrites sacred_valley.js with the two-band layout that mounts ordered cards.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
23 TDD tasks across 4 phases: grid framework + data cards, reuse cards,
speedtest, Little Blue health band. Verified against repo patterns (validate,
api.put, requireOwner, pg-boss, migrations).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Server-side icon cache (lib/health/icons.js + GET /api/icons/:slug.png) fetches
each icon once from dashboard-icons into a persistent dir and serves it from the
LAN. Browser never contacts the CDN; slug sanitized; first-letter fallback.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Two-band dashboard: draggable data cards (clock/weather/host-perf/speedtest/
jobs/inbox/search) + Little Blue read-only Health band (config registry +
pg-boss health engine + grouped service tiles w/ auto-icons). Refined-B chrome,
server-side layout persistence, polling refresh. Fix-it agent deferred.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- migration 011_yerin.sql: seed read-only 'yerin' agent ({read:true}, kind claude,
model NULL = server default; switch to local Ollama via agents.model anytime)
- companion-stdio.js: select the toolset from VOID_TOOL_REGISTRY ('security' →
Yerin's securityRegistry; default → Dross's companionRegistry)
- tests/mcp/registry_select.test.js
Remaining for Yerin (left for review): an entry point (route or cron) + persona
prompt — see docs/yerin-security-agent.md.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Three more read-only tools on securityRegistry:
- pending_review: agent-proposed changes awaiting approval (injection surface)
- resource_exposure: host/url/status attack-surface inventory (resources.listExposure,
scalar cols only — no monitoring/metadata/credentials)
- token_audit: token label/last_used/revoked, never the hash
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
verifyToken loaded every non-revoked token and bcrypt-compared each (O(n) per
request — auth-latency DoS + linear scaling). New token format
vk_<selector>.<verifier>: the non-secret selector is indexed and locates exactly
one row; only the verifier is bcrypt-hashed. Legacy NULL-selector tokens still
verify via a fallback scan. Dropped the useless idx_agent_tokens_hash.
- migration 010_token_selector.sql (adds selector col + unique partial index)
- createToken/verifyToken reworked; also adds listTokenMeta (read for Yerin's
token_audit tool)
- tests/repos/token_selector.test.js
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- pool.js: add pool.on('error') handler — an idle-client error (DB restart /
.215 failover) previously crashed the process (no 'error' listener → throw)
- context tool: project a SAFE_COLUMNS allow-list for resources (never the
monitoring/metadata JSON blobs); also add 'resource' to TABLE (was unhandled)
- applyPendingChange: guard the 'upsert' arm so a non-upsertable entity_type
fails with a clear ValidationError instead of a bare TypeError
Tests: pool_error, context (resource case), pending_extended_actions (guard).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
New securityRegistry (separate from companionRegistry) with two read-only,
secret-free tools for the Yerin security agent:
- audit_log: query the redacted audit trail by actor_kind/actor_id
- agent_inventory: list agents + capabilities/scopes (explicit projection,
never SELECT *, no token material)
Follows the existing createRegistry() pattern. Design + wiring roadmap in
docs/yerin-security-agent.md. Not yet seeded/exposed over MCP (left for review).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Owner bearer token was compared with === / !==, which short-circuits on the
first differing byte and leaks token length+prefix via response timing
(security-sweep-2026-06-01.md). New timingSafeStrEqual (crypto.timingSafeEqual
with a length pre-check so it never throws on length mismatch); wired into both
owner.js and agent_auth.js.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The pending_changes.action CHECK only permitted create/update/delete, so a
suggest-tier agent hitting POST /api/refs/upsert (or the resource dependency
routes) 500'd on the INSERT (docs/security-followups.md HIGH finding).
- migration 009: widen CHECK to include 'upsert'
- applyPendingChange: dispatch 'upsert' -> refsRepo.upsertByExternal on approve
- resources.js: add_dependency/remove_dependency are now owner-only (requireOwner),
infra wiring is never diverted to pending_changes
- tests/api/pending_extended_actions.test.js: regression coverage
Full suite green (278 pass / 1 skip).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- system prompt = Dross (Ozriel's construct fragment, per Void 1.0), with tool guidance
- migration 008 renames the seeded agent 'companion' → display name 'Dross'
- removed lib/ai/anthropic.js + lib/ai/agent/runtime.js + tests + @anthropic-ai/sdk dep (companion now runs via the claude CLI; kept lib/ai/secret.js for the Vaultwarden roadmap)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
resetDb() DROPs schema; dev DATABASE_URL pointed at the shared prod void DB on
.215. setup.js now forces a dedicated void_test DB (TEST_DATABASE_URL or derived)
and throws if it would target prod. Created void_test + pg_hba rule on CT 310.
Verified: full suite green, prod void space count unchanged (2→2).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- driver: tool_results arrive as type:'user' content blocks (not bare); parse them
- route: tool_result content is a JSON string; parse it for pending_change_id → draft event
- propose_change: inject ctx.space_id into create payloads (model can't know the uuid; tables require it)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
claude resolves the MCP server command against the child env (no PATH), so a
bare 'node' failed to spawn (status:failed). Use process.execPath. Also pass
--tools to drop claude's built-ins (Bash/Read/Write/…) — companion gets only
the four mcp__void__* tools.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Replaces the runTurn/callModel/Anthropic-API-key path in POST /turn with
runClaudeTurn (claude CLI) backed by a per-turn MCP config that spawns
companion-stdio.js. Extracts pending_change_id from tool_result events
defensively (structuredContent → text-JSON fallback). Rewrites companion
test to inject fake-claude-draft.js via app.locals.claudeExe.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Implements runClaudeTurn() — spawns the claude CLI for a single companion
turn using subscription/OAuth auth (strips ANTHROPIC_API_KEY +
ANTHROPIC_AUTH_TOKEN from child env), streaming normalised events (delta,
tool, tool_result, result, error) via onEvent callback.
Includes hermetic test + fake-claude.js fixture that mimics real 2.1.159
stream-json output; zero network/CLI calls in the test suite.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>