Skip to content

feat(voice): reserve the Crow voice bot, so it can't be deleted, edited through generic saves, peer-managed, delegated to, scheduled or given cards - #528

Merged
kh0pper merged 4 commits into
mainfrom
feat/crow-voice-pr0-guards
Oct 20, 2026
Merged

kh0pper merged 4 commits into
mainfrom
feat/crow-voice-pr0-guards

Conversation

@kh0pper

@kh0pper kh0pper commented Oct 20, 2026

Copy link
Copy Markdown
Owner

Why

Crow's voice will become one reserved bot, crow.voice, stored in pi_bot_defs with definition.kind = "voice". A later PR creates that row at boot. This PR ships first and adds no row of its own. It adds the definition module and every guard the row needs, so the row is never unguarded: not when it first appears, and not if the later PR is reverted.

What changes

servers/shared/voice-bot.js (new, no I/O)

  • CROW_VOICE_BOT_ID = "crow.voice", isVoiceBotId, isVoiceBotDef.
  • The two presets: fast_local (the local voice model) and cloud (needs a cloud model; turns escalation off).
  • DEFAULT_PERSONA.
  • voiceBotDefinition({ preset, fastKey, cloudKey, migratedFrom }) builds a definition that can never do pi work: no gateways, no tracker, no skills, no pi built-ins, a short tool list (memory, media, music), and a permission policy of bash deny, drafts only, no self-authoring and no skill learning.
  • normalizeVoiceSettings(def) reads the voice settings and falls back to the strict default for anything missing or malformed. It never falls back to something looser.

Guards, one check each

  • Bot Builder: every POST action on crow.voice is refused with voice_bot_readonly. Delete reaches handleDeleteConfirm, which answers voice_bot_permanent. sanitizeBotId is exported, and create uses it with the same rule as before.
  • Federation: botPeerManageable is false for the voice bot, and the peer catalog (localBots) leaves it out.
  • Listings: the Bot Board panel, the crow-messages roster, and Perch /bots and /roost leave it out.
  • Work:
    • crow_delegate and the bot-schedule tool refuse it with "cannot take background jobs".
    • Perch "Give a task" (POST /bots/:id/tasks) gives 400 bot_unavailable.
    • The Bot Board card assign and execute routes, and proposed-skill approve, give 400 bot_unavailable.
    • Bot cron advances a voice schedule without queueing a job.
    • The job runner fails a voice job before any pi spawn.
  • The artifacts-companions boot migration skips the voice row, including one whose JSON doesn't parse.

No schema change, no SCHEMA_GENERATION bump, no DELETE or DROP. No bundle code changes, so there are no manifest bumps.

Tests

  • tests/voice-bot-def.test.js: presets, normalization of malformed input, the reserved id, and sanitizeBotId("crow.voice") === "crow-voice".
  • tests/voice-bot-guards.test.js: a scratch DB from init-db with a crow.voice row and two ordinary bots, using the real handlers and routers.
    • Each guard is checked by its side effect, not only by its status: the row and its definition are byte-identical, no invite is minted, no bot_jobs or schedules row is created, and the card is unchanged.
    • Ordinary bots still work through the same paths.
  • Full suite: 10178 tests, 10143 pass, 0 fail, 35 skipped.

Deploy

Merge, then pull main on each host and restart the gateway(s) and the pi-bot gateway runner. Nothing changes for ordinary bots, and nothing is visible until the voice bot row exists.

Rollback

Revert this PR only after any later voice PR has been reverted and the voice row has been disabled (UPDATE pi_bot_defs SET enabled = 0 WHERE bot_id = 'crow.voice', a disable, never a delete).

https://claude.ai/code/session_01N9fXzvNy8rCEArj1y5p6fp

…gated to or given cards

The reserved crow.voice row is refused by Bot Builder delete (voice_bot_permanent) and every generic Bot Builder action (voice_bot_readonly), is never peer-manageable, is left out of the peer catalog, the Bot Board list, the crow-messages roster and the Perch /bots and /roost listings, cannot take crow_delegate or crow_schedule_bot jobs, cannot be given cards (400 bot_unavailable), and is skipped by the artifacts-companions migration. Bot Builder's create rule is exported as sanitizeBotId.

Claude-Session: https://claude.ai/code/session_01N9fXzvNy8rCEArj1y5p6fp
…e Crow voice bot; Bot Builder delete shows voice_bot_permanent

POST /card/:id (assign), /card/:id/execute and /bot/:botId/proposed-skill/approve refuse crow.voice with 400 bot_unavailable before any card, job or definition write. handleBotBuilderPost lets delete_confirm through to handleDeleteConfirm's own refusal; VOICE_BOT_ACTIONS stays the voice editor allow-list.

Claude-Session: https://claude.ai/code/session_01N9fXzvNy8rCEArj1y5p6fp
… the Crow voice bot

tickBotSchedules advances a pipeline:botcron:crow.voice schedule like any it cannot enqueue (no bot_jobs row, the schedule is kept). runJob fails any job whose bot id is crow.voice or whose definition is kind voice, before any bot load or pi spawn, so every producer is covered.

Claude-Session: https://claude.ai/code/session_01N9fXzvNy8rCEArj1y5p6fp
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant