Files
skills/hub/templates/voice.js
T
Kevin Veen-BirkenbachandClaude Opus 5.5 8ba45efd04 feat(hub): kvb-hub review hub with passkey-signed approvals
The kvb-hub is a local review hub in a container bound to 127.0.0.1: one page, JSON APIs and an MCP endpoint for the operator's inbox, asks, decisions, staged-commit reviews, the radio stream, the agent graph, the worktree tree, push-to-talk and read-aloud radio (Piper, whisper.cpp) and an optional LLM command line. The hub is only a relay. The host executors in hub/client (the runner user service, the PermissionRequest hook and the approval CLI the commit skills call) verify a WebAuthn passkey signature against keys pinned outside every sandbox, refuse replays and fail closed. scripts/hub.sh with make hub-install, hub-check, hub-pin, hub-runner and hub-hook installs and checks the hub; hub-hook writes ~/.claude/settings.json after a backup. make watch-check shows a stuck watch hook. The new kvb-git-diff skill and the commit skills route reviews through the hub; the test_hub_* suites replace test_hub.py.

Security review (kvb-think-criticize, three skeptics) fixed four holes: signed rules auto-approved chained shell commands; the client and LLM openers followed redirects, the LLM one with its bearer key; the worktree scan ran git fsmonitor and filter commands from agent-writable repos; compose set no memory, pids or health limits. Documented until core requirement 093, on the operator's word: the hub page is trusted while signing and pinning, local processes can forge the unsigned operator actions, and the container reads the host /tmp. Committed on the operator's word despite the gate: the passkey path is not yet verified live.

Accepted MEDUSA false positives (137 findings on 115 lines, rows in security-accepted.jsonl): EXA002 x17, MEDUSA-MM-SCAN-058 x12, B603 x8, MEDUSA-FTSC-SCAN-089 x7, MEDUSA-PRIV-SCAN-762 x6, AIC-EXFIL-005 x5, EXA006 x5, MEDUSA-FTSCAN-231 x5, WEB-SSRF x5, MEDUSA-INF-SCAN-2057 x3, RH005 x3, TC001 x3, LO002 x2, MEDUSA-AEXP-SCAN-096 x2, MEDUSA-CGS-SCAN-028 x2, MEDUSA-INF-SCAN-1350 x2, MEDUSA-INF-SCAN-2369 x2, MEDUSA-INF-SCAN-2440 x2, MEDUSA-INF-SCAN-2638 x2, MLSC-SERIAL-002 x2, PL002 x2, PQC005 x2, SAST-PATH-001 x2, TC004 x2, TC005 x2, TC006 x2, TC007 x2, AA-146, EXA007, FINE-050, GB-ENCODE-001, GB-ENCODE-003, MEDUSA-CGS-SCAN-029, MEDUSA-CGS-SCAN-103, MEDUSA-DP-SCAN-1034, MEDUSA-DP-SCAN-1916, MEDUSA-DP-SCAN-954, MEDUSA-EXF26-TOOL-004, MEDUSA-FT-TRIGGER-005, MEDUSA-INF-SCAN-2206, MEDUSA-INF-SCAN-2457, MEDUSA-INF-WEB-003, MEDUSA-INFRA-SCAN-030, MEDUSA-MP-SCAN-1135, MEDUSA-RLHF-POISON-002, MEDUSA-RT-SCAN-006, MM-ENCODE-001, MP-124, PQC001, PQC002, PQC006, RP001, TC009, TC010, WEB-FLASK-004.

🎯 Fix: 80 %
💥 Break: 4 %
💸 Cost: 129 %
⏱️ Time: –
🛡️ Security: 100 %
💰 Value: ?
🏆 Score: 60
🚦 Gate: ⛔ fix 80 % < 90; cost 129 % > 100: ask the operator before the commit

```
/home/kevinveenbirkenbach/Repositories/github.com/kevinveenbirkenbach/skills/.worktrees/feature/kvb-hub
```

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-06 18:46:40 +00:00

72 lines
2.3 KiB
JavaScript

const Voice = (() => {
"use strict";
const RANKS = {mayday: 0, "pan-pan": 1, security: 2};
const ALARMS = new Set(["mayday", "pan-pan"]);
const ROUTINE = 3;
function toneOf(row, human) {
if (ALARMS.has(row.class)) {
return "mayday";
}
return row.class === "security" || row.to.includes(human) ? "direct" : "radio";
}
function loudness(tone, settings) {
if (tone !== "mayday") {
return settings.sounds[tone].volume;
}
return Math.max(...Object.values(settings.sounds).map((sound) => sound.volume), settings.music.volume);
}
function speakable(row, speech) {
if (!speech.on || !row.speech || !row.voice) {
return false;
}
return row.class === "mayday" || (speech.classes.includes(row.class) && !speech.muted.includes(row.source));
}
function rank(item) {
return Object.hasOwn(RANKS, item.cls) ? RANKS[item.cls] : ROUTINE;
}
function enqueue(pending, item, limit) {
const at = pending.findIndex((other) => rank(other) > rank(item));
const next = at < 0 ? [...pending, item] : [...pending.slice(0, at), item, ...pending.slice(at)];
return next.slice(0, limit);
}
function frames(duration, rate, seconds) {
return Math.max(Math.min(Math.ceil(duration * rate), rate * seconds), 0);
}
function wav(samples, rate) {
const size = samples.length * 2;
const buffer = new ArrayBuffer(44 + size);
const view = new DataView(buffer);
const text = (offset, value) => [...value].forEach((char, index) => view.setUint8(offset + index, char.charCodeAt(0)));
text(0, "RIFF");
view.setUint32(4, 36 + size, true);
text(8, "WAVE");
text(12, "fmt ");
view.setUint32(16, 16, true);
view.setUint16(20, 1, true);
view.setUint16(22, 1, true);
view.setUint32(24, rate, true);
view.setUint32(28, rate * 2, true);
view.setUint16(32, 2, true);
view.setUint16(34, 16, true);
text(36, "data");
view.setUint32(40, size, true);
samples.forEach((sample, index) => {
const level = Math.max(-1, Math.min(1, sample));
view.setInt16(44 + index * 2, level < 0 ? level * 0x8000 : level * 0x7fff, true);
});
return buffer;
}
return {toneOf, loudness, speakable, enqueue, frames, wav};
})();
if (typeof module === "object" && module.exports) {
module.exports = Voice;
}