Skip to content

Audit fixes: /ws/call auth, offline default, STT turn assembly, provider API 500, CI lint gate#84

Open
jaylfc wants to merge 14 commits into
mainfrom
claude/lucid-dirac-k9uagl
Open

Audit fixes: /ws/call auth, offline default, STT turn assembly, provider API 500, CI lint gate#84
jaylfc wants to merge 14 commits into
mainfrom
claude/lucid-dirac-k9uagl

Conversation

@jaylfc

@jaylfc jaylfc commented Jun 11, 2026

Copy link
Copy Markdown
Owner

First batch of fixes from the technical audit (included as AUDIT.md): the critical security gap, the worst correctness bugs, and the CI/dependency quick wins.

Security

Correctness

CI / dependencies / docs

Tests

56 passing (was 30). New coverage: /voice/* TwiML branches (greeting/PIN/lockout/voicemail), stream-token issue/redeem/single-use/TTL, STT turn assembly (interim vs final vs speech_final), /api/providers endpoints, and a fresh-install backend-selection test that asserts the factory's actual choice on a scrubbed env (addresses the core of #77).

Fixes #25, fixes #51, fixes #66, fixes #67, fixes #68, fixes #69, fixes #71, fixes #73, fixes #75, fixes #76, fixes #82.

https://claude.ai/code/session_01R2pGk31EzMvwumYCQ8TFNo


Generated by Claude Code

Summary by CodeRabbit

  • New Features

    • Installer now lets you select the agent backend with a local Ollama default; media WebSocket connections require one‑time stream tokens.
  • Bug Fixes

    • Reduced duplicated/interim STT transcripts, tightened WebSocket receive behavior, improved HTML escaping and delete-button handling, and hardened provider-status handling.
  • Documentation

    • Added a project audit/improvement plan and updated workflow guidance to run linting and tests pre-PR.
  • Tests

    • Expanded end-to-end call flow, provider registry, dashboard auth, offline-defaults, and settings-invalidation tests.
  • Chores

    • CI now installs pinned requirements and runs a Ruff lint step; platform-specific dependency specifiers updated.

claude added 10 commits June 11, 2026 14:16
…plan

Analysis-only deliverable. Documents verified bugs (unauthenticated /ws/call,
/api/providers KeyError 500, default-backend split brain, interim-transcript
duplication), severity-rated audit across security/correctness/architecture/
testing/deps/docs, a 4-theme improvement strategy, and a milestone task plan
with quick wins. No code changes.

https://claude.ai/code/session_01R2pGk31EzMvwumYCQ8TFNo
Add the missing 'backend' field to the agent entries in PROVIDER_DEPS,
make get_provider_status() defensive with .get(), and add endpoint +
registry invariant tests so the provider browser can't silently break
again.

Fixes #68

https://claude.ai/code/session_01R2pGk31EzMvwumYCQ8TFNo
…ke server.py

- agents/__init__.py: single DEFAULT_AGENT_PROVIDER='ollama' constant,
  imported by server.py so the displayed provider and the selected
  backend can't drift; 'auto' (or empty, written by the settings UI)
  keeps auto-detect behaviour; ollama default model aligned to qwen3:8b
- add reset_agent_backend() so tests/settings changes can invalidate
  the singleton
- installers: every wizard choice now writes an explicit AGENT_PROVIDER
  (preserved on re-run; falls back to ollama)
- test_offline_defaults now asserts the backend the factory actually
  builds on a scrubbed env, not the cosmetic constant

Fixes #67

https://claude.ai/code/session_01R2pGk31EzMvwumYCQ8TFNo
…nce timeout

- /ws/call now requires a single-use stream token: each <Connect><Stream>
  embeds a <Parameter> minted at TwiML time (TTL 120s, bound to the
  CallSid, constant-time compare); connections whose start event doesn't
  redeem one are dropped. Closes the PIN-gate bypass on the public port.
- Turn assembly extracted into _accumulate_stt_message(): only finalized
  Deepgram segments are buffered (interims repeated the same words into
  the LLM input), and the turn now ends on speech_final endpointing
  rather than per-segment is_final (kept as fallback when the attribute
  is absent). Buffer shared with the Deepgram callback thread is now
  lock-guarded.
- ws.receive timeout no longer kills the session: silence/hold gaps
  continue the loop; only stop events or a closed connection end it.

Fixes #66, #69, #71
Fixes #25 (turn boundary now speech_final-driven)

https://claude.ai/code/session_01R2pGk31EzMvwumYCQ8TFNo
escHtml now escapes all five dangerous characters (it previously left
quotes intact via the textContent/innerHTML round trip), and the delete
button uses data-sid + addEventListener instead of interpolating the
sid into an inline JS string.

Fixes #73

https://claude.ai/code/session_01R2pGk31EzMvwumYCQ8TFNo
ensure_models() now only checks importability and falls back to cloud
voice with a hint; installation moves to install.sh (which already
pre-installs the MLX packages) or the new 'python local_voice.py
--install'. Running pip at boot blocked startup for up to 300s and
caused a macOS fork-safety crash (Network framework atfork handlers in
pip's forked build children).

Fixes #51

https://claude.ai/code/session_01R2pGk31EzMvwumYCQ8TFNo
rumps/pywebview are macOS-only and audioop-lts only exists for 3.13+,
so 'pip install -r requirements.txt' now works on Linux (matching the
README's claim) and CI stops hand-maintaining a parallel dep list.

Fixes #75

https://claude.ai/code/session_01R2pGk31EzMvwumYCQ8TFNo
- pyproject.toml: ruff config (default E4/E7/E9 + F rules, plus E722
  bare-except; AppKit bulk imports in native_settings.py exempted from
  F401)
- CI gains a 'ruff check .' step before tests
- Fix the 60 existing findings: unused imports, bare excepts (now
  Exception/OSError), f-strings without placeholders, mid-file
  'import time as _time' (top-level time was already imported), dead
  AVSpeech stub code in NativeTTS, unused call_sid in
  voicemail_complete

Fixes #76

https://claude.ai/code/session_01R2pGk31EzMvwumYCQ8TFNo
@coderabbitai

coderabbitai Bot commented Jun 11, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 9252c430-4782-4e82-9742-ca8f07331e01

📥 Commits

Reviewing files that changed from the base of the PR and between 32dbdc0 and 268c1e3.

📒 Files selected for processing (10)
  • .gitignore
  • AUDIT.md
  • install-linux.sh
  • install.sh
  • local_voice.py
  • server.py
  • tests/test_call_flow.py
  • tests/test_dashboard_auth.py
  • tests/test_offline_defaults.py
  • tests/test_settings_overrides.py

📝 Walkthrough

Walkthrough

Implements one-time per-call WebSocket stream tokens and thread-safe STT turn assembly; aligns AGENT_PROVIDER defaults across agents, server, and installers; adds provider registry backend fields; enforces Ruff in CI with platform-marked requirements; and includes tests, docs (AUDIT.md), and multiple small code-quality fixes.

Changes

Agent Provider Configuration & Defaults

Layer / File(s) Summary
Agent provider constant & factory reset
agents/__init__.py
Adds DEFAULT_AGENT_PROVIDER = "ollama", introduces reset_agent_backend() to clear cached singleton for testing, updates get_agent_backend() to default from the constant, and treats literal "auto" as an explicit auto-detect trigger.
Server live AGENT_PROVIDER & health
server.py
AGENT_PROVIDER default is read from DEFAULT_AGENT_PROVIDER; health and settings report live env value; adds AGENT_BACKEND_KEYS and resets cached backend when relevant settings change.
Installers persist AGENT_PROVIDER
install.sh, install-linux.sh
Installer wizards set AGENT_PROVIDER for chosen backend and write it to .env, defaulting to ollama when not present.
Fresh-install & offline defaults tests
tests/test_offline_defaults.py
Adds scrubbed_env fixture, subprocess-backed fresh-process assert that server.AGENT_PROVIDER == agents.DEFAULT_AGENT_PROVIDER, tests that scrubbed env picks local Ollama with qwen3* model, and that explicit "auto" resolves to NoOpAgent.

WebSocket Stream Token Authentication

Layer / File(s) Summary
Stream token issuance & redemption
server.py
Adds _issue_stream_token(call_sid) and _redeem_stream_token(token, call_sid) with TTL, single-use semantics, constant-time call_sid compare, in-memory stream_tokens, and pruning of expired tokens.
TwiML token embedding (inbound & outbound)
server.py
PIN-accepted inbound and outbound TwiML <Connect><Stream> responses include a token custom parameter for the client to present on WebSocket start.
STT accumulation & /ws/call handler
server.py
Introduces _accumulate_stt_message() to fold only finalized Deepgram segments, per-connection lock for transcript buffer, token validation on start (rejects missing/invalid), and receive-loop timeout handling changed to continue on timeouts.
Call flow & WS auth-gating tests
tests/test_call_flow.py
Adds integration/unit tests covering PIN gating, TwiML token embedding, voicemail handling, stream-token issuance/redeem/single-use/TTL/forgery checks, Deepgram turn-assembly behaviors, and WS auth-gating using fake WebSocket/Deepgram clients.

Provider Registry Backend Field & Status Fix

Layer / File(s) Summary
Provider registry backend fields & safe access
provider_registry.py
Adds backend entries (cloud/local) for agent providers and reads backend via info.get("backend", "local") to avoid KeyError in get_provider_status().
Provider registry endpoint tests
tests/test_provider_registry.py
Regression tests asserting get_provider_status() covers all PROVIDER_DEPS entries and that dashboard endpoints return expected data/HTTP codes for known/unknown providers.

Dependency Management & Linting

Layer / File(s) Summary
Platform-conditional dependency markers
requirements.txt
Adds environment markers: audioop-lts; python_version >= "3.13", rumps; sys_platform == "darwin", pywebview>=4.0; sys_platform == "darwin".
Ruff linting configuration
pyproject.toml
Configures Ruff target Python 3.11, excludes prototype, extends-select to include E722, and adds per-file ignore for native_settings.py F401.
CI lint enforcement & README workflow
.github/workflows/ci.yml, README.md
CI now installs from requirements.txt plus pytest/ruff and runs ruff check . before tests. README documents enabling the pre-commit secrets hook and running ruff check . and pytest locally before PRs.

Code Quality & Security Improvements

Layer / File(s) Summary
Dashboard voicemail delete & escHtml
dashboard.html
Replaces inline onclick with data-sid and post-render addEventListener wiring; updates escHtml() to use direct character-to-entity replacement.
Local voice availability & CLI install
local_voice.py
Removes unused import, changes ensure_models() to availability-only (no auto-install), makes NativeTTS return silent PCM, uses find_spec for edge detection, and adds --install CLI.
Exception narrowing & minor server fixes
menubar.py, native_settings.py, server.py
Narrow bare except: to except Exception:, remove unused imports, tighten OSError handling in voicemail cleanup, minor string-format adjustments and import organization.
Tests & small edits
tests/*
Reformat some test assertion messages (avoid f-strings), split .env write for clarity, add backend-invalidation test coverage and dashboard auth/session revocation tests.

Technical Audit & Contributing Workflow

Layer / File(s) Summary
Technical audit documentation
AUDIT.md
Adds a comprehensive audit (2026-06-11) describing security and correctness findings, phased task plan (milestones 0–3), implementation sketches for top tasks (WS auth, backend alignment, transcript handling), and open questions.
.gitignore
.gitignore
Ignores .claude/ transient worktrees.
CI/Docs
README.md, .github/workflows/ci.yml
Documents pre-commit secrets-hook enabling step and local verification (ruff + pytest).

Sequence Diagram(s)

sequenceDiagram
  participant TwiML as Twilio (Call)
  participant Server
  participant Client as Browser/Agent WS
  participant Deepgram
  participant LLM
  TwiML->>Server: generate TwiML <Connect><Stream token=one_time>
  Server->>TwiML: return TwiML with token
  Client->>Server: WebSocket connect -> sends start with customParameters token
  Server->>Server: _redeem_stream_token(token, call_sid)
  Server-->>Client: accept or reject WS (based on token)
  Client->>Deepgram: media forwarded
  Deepgram->>Server: on_message (interim/final)
  Server->>Server: _accumulate_stt_message(msg, transcript_buf) (lock)
  Server->>LLM: when turn finalized -> send transcript
  LLM->>Server: response
  Server->>Client: TTS audio frames
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~75 minutes

Possibly related PRs

  • jaylfc/dialtone#57: Related edits to server settings handling and dashboard-session invalidation; overlaps with live DASHBOARD_TOKEN/session logic in this PR.

🐰 "I mint a token, hop the gate, then fold each final word,
The backends line up tidy now, the CI keeps code bird-heard.
Ruff sniffed the crumbs, tests chased the bugs away,
A tiny rabbit dances—safe calls win the day!"

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 20.21% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the main changes: implementing audit fixes for WebSocket authentication, offline defaults, STT turn assembly, provider API, and CI linting.
Linked Issues check ✅ Passed All nine linked issues (#25, #51, #66, #67, #68, #69, #71, #73, #75, #76, #82) are comprehensively addressed through code changes, tests, and documentation updates.
Out of Scope Changes check ✅ Passed All changes directly support the audit fix objectives; no unrelated modifications were introduced beyond the documented scope.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch claude/lucid-dirac-k9uagl

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 8

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@AUDIT.md`:
- Around line 7-10: Update the EXECUTIVE SUMMARY in AUDIT.md to clarify this is
a pre-fix baseline by either prefixing the section title "Executive Summary"
with "Before this PR" or splitting the report into "Before this PR" and "After
this PR" sections; modify the header and the opening paragraph under the
"Executive Summary" block so it explicitly states the findings reflect the state
before the current PR, and if splitting, create a new "After this PR" subsection
summarizing fixes introduced by the PR (refer to the existing "Executive
Summary" text to edit or duplicate).
- Around line 128-193: Markdown heading lines (e.g., "### Theme 1 — Close the
gap..." and subsequent "### Theme 2/3/4" and "### Milestone 0 — Safety net") are
missing the required blank line above them, which triggers MD022; fix by
inserting a single blank line before each top-level "###" heading in this block
so every heading is surrounded by blank lines; search for the headings "###
Theme 1", "### Theme 2", "### Theme 3", "### Theme 4", "### Milestone 0" and
ensure there is one empty line immediately before each heading to satisfy the
linter.

In `@install-linux.sh`:
- Around line 316-323: The Ollama branches (the case handling that sets
AGENT_PROVIDER="ollama" — see the block that sets LLM_KEY, LLM_BASE_URL,
LLM_PROVIDER and AGENT_PROVIDER) must set a local LLM_MODEL default that matches
the new Ollama/qwen3 default; update both the choice 4 block (where LLM_MODEL is
defaulted to "llama3") and the choice 7 block (where it currently writes
"gpt-4o-mini") so that when AGENT_PROVIDER="ollama" you prompt for LLM_MODEL but
default it to "qwen3" (i.e., change the LLM_MODEL="${LLM_MODEL:-llama3}" and the
other default to use "qwen3" for Ollama paths) ensuring any fresh install points
to a provisioned local Ollama model.

In `@install.sh`:
- Around line 190-196: When AGENT_PROVIDER is set to "ollama" the script must
not default LLM_MODEL to the legacy "llama3"; change the logic around the
LLM_MODEL prompt/assignment so that when AGENT_PROVIDER="ollama" the default
LLM_MODEL uses the previously pulled OLLAMA_MODEL (or leaves it empty) instead
of "llama3". Update the block setting LLM_KEY, LLM_BASE_URL, LLM_PROVIDER,
AGENT_PROVIDER and the LLM_MODEL assignment so
agents._resolve_openai_compat("ollama") receives the correct model (reference
variables LLM_MODEL, AGENT_PROVIDER and OLLAMA_MODEL).

In `@server.py`:
- Line 80: The module currently freezes AGENT_PROVIDER at import time; instead,
stop using the module-level AGENT_PROVIDER constant and read the provider
dynamically via env() wherever used (e.g., in health()/settings handlers and any
code that queries provider), and when api_update_settings() changes
AGENT_PROVIDER or related LLM keys invoke agents.reset_agent_backend() to clear
the cached backend; alternatively, if you don't support live changes, have
api_update_settings() mark the settings as restart-required so the UI/health
reflect that.
- Around line 257-273: The functions _issue_stream_token and
_redeem_stream_token access and mutate the shared stream_tokens dict without
synchronization, causing possible RuntimeError or lost tokens under concurrency;
fix by introducing a threading.Lock (e.g., stream_tokens_lock) and acquire it
around all accesses and mutations of stream_tokens (including the cleanup loop
in _issue_stream_token, the assignment stream_tokens[token]=..., and the pop()
in _redeem_stream_token) to ensure atomic iteration and modifications and
prevent race conditions.

In `@tests/test_call_flow.py`:
- Around line 73-79: The test test_outgoing_stream_carries_token assumes exactly
one token in server.stream_tokens before unpacking; add an explicit assertion
like assert len(server.stream_tokens) == 1 (or assert server.stream_tokens and
len(...) == 1 for clearer failure messaging) immediately before the unpacking
line to produce a clear test assertion if 0 or multiple tokens are present,
referencing server.stream_tokens and the existing unpacking (rec,) to locate
where to insert it.

In `@tests/test_offline_defaults.py`:
- Around line 32-33: The test test_display_constant_matches_factory_default
currently reads server.AGENT_PROVIDER from an already-imported module so it
reflects ambient process state; modify the test to scrub
os.environ["AGENT_PROVIDER"] (or delete it) and then re-import or reload the
server module so server.AGENT_PROVIDER is evaluated against a clean environment,
then assert server.AGENT_PROVIDER == agents.DEFAULT_AGENT_PROVIDER == "ollama";
reference the server module, the AGENT_PROVIDER symbol, and
agents.DEFAULT_AGENT_PROVIDER when making the change.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: efd16e0a-2bc7-48ad-8895-633bd037f1a1

📥 Commits

Reviewing files that changed from the base of the PR and between f9231ff and 32dbdc0.

📒 Files selected for processing (19)
  • .github/workflows/ci.yml
  • AUDIT.md
  • README.md
  • agents/__init__.py
  • dashboard.html
  • install-linux.sh
  • install.sh
  • local_voice.py
  • menubar.py
  • native_settings.py
  • provider_registry.py
  • pyproject.toml
  • requirements.txt
  • server.py
  • tests/test_all_settings_editable.py
  • tests/test_call_flow.py
  • tests/test_offline_defaults.py
  • tests/test_provider_registry.py
  • tests/test_settings_overrides.py

Comment thread AUDIT.md
Comment thread AUDIT.md
Comment thread install-linux.sh
Comment thread install.sh Outdated
Comment thread server.py
Comment thread server.py
Comment thread tests/test_call_flow.py
Comment thread tests/test_offline_defaults.py Outdated
claude added 2 commits June 12, 2026 00:08
- stream_tokens guarded by a lock (webhook threads issue, WS threads
  redeem)
- AGENT_PROVIDER no longer frozen at import for status reporting:
  /health and /api/settings read it live, and updating any
  agent-affecting setting resets the cached backend singleton and
  health cache (with tests)
- installers: Ollama wizard branches now default LLM_MODEL to the
  provisioned model (macOS: $OLLAMA_MODEL RAM tier; Linux: qwen3:8b)
  instead of legacy llama3/gpt-4o-mini; read -r
- AUDIT.md labelled as pre-fix baseline; MD022 heading spacing
- test_offline_defaults: fresh-process subprocess check instead of
  ambient-state constant assertion; test_call_flow asserts token count
  before unpacking

https://claude.ai/code/session_01R2pGk31EzMvwumYCQ8TFNo
@jaylfc

jaylfc commented Jun 12, 2026

Copy link
Copy Markdown
Owner Author

@coderabbitai review


Generated by Claude Code

@coderabbitai

coderabbitai Bot commented Jun 12, 2026

Copy link
Copy Markdown

Tip

For best results, initiate chat on the files or code changes.

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
echo "=== server.py ==="
cat server.py

echo ""
echo "=== agents/__init__.py ==="
cat agents/__init__.py

echo ""
echo "=== dashboard.html ==="
cat dashboard.html

echo ""
echo "=== provider_registry.py ==="
cat provider_registry.py

echo ""
echo "=== requirements.txt ==="
cat requirements.txt

echo ""
echo "=== pyproject.toml ==="
cat pyproject.toml

echo ""
echo "=== local_voice.py ==="
cat local_voice.py

echo ""
echo "=== tests/test_call_flow.py ==="
cat tests/test_call_flow.py

echo ""
echo "=== tests/test_offline_defaults.py ==="
cat tests/test_offline_defaults.py

echo ""
echo "=== tests/test_provider_registry.py ==="
cat tests/test_provider_registry.py

echo ""
echo "=== .github/workflows/ci.yml ==="
cat .github/workflows/ci.yml

echo ""
echo "=== README.md ==="
cat README.md

echo ""
echo "=== menubar.py ==="
cat menubar.py

echo ""
echo "=== native_settings.py ==="
cat native_settings.py

echo ""
echo "=== install.sh ==="
cat install.sh

echo ""
echo "=== install-linux.sh ==="
cat install-linux.sh

Length of output: 242810


🏁 Script executed:

#!/bin/bash
echo "=== conftest.py ==="
cat tests/conftest.py 2>/dev/null || echo "NOT FOUND"

echo ""
echo "=== tests/ directory listing ==="
ls -la tests/

echo ""
echo "=== Check for AGENT_PROVIDER in case 5 of install.sh ==="
grep -n "AGENT_PROVIDER\|openai-compat\|LLM_CHOICE" install.sh | head -30

echo ""
echo "=== Check media handling in handle_ws before auth ==="
grep -n "event.*media\|dg_conn.send\|_redeem\|stream_sid\|call_sid" server.py | head -40

echo ""
echo "=== Check test_all_settings_editable ==="
cat tests/test_all_settings_editable.py

Length of output: 8504



@jaylfc This is a solid batch of audit fixes — security, correctness, CI hygiene, and test coverage all improved together. 56 passing tests vs 30 previously is a meaningful jump. Here are the findings from the review:


Summary

Area Finding Severity
server.py Audio forwarded to Deepgram before stream token validation 🔴 Medium
local_voice.py NativeTTS docstring misleading ("Not implemented") 🟡 Minor
server.py / tests/ Everything else looks well-implemented

🔴 Pre-auth audio leaks to Deepgram in handle_ws

The stream token is validated in the "start" event branch, but "media" events are forwarded to Deepgram unconditionally — no auth check guards dg_conn.send_media():

# server.py — handle_ws receive loop
elif msg["event"] == "media":
    audio = base64.b64decode(msg["media"]["payload"])
    if dg_conn:
        dg_conn.send_media(audio)   # ← runs before start/token has been seen

Twilio always sends start before media, so legitimate calls are unaffected. However, an attacker connecting directly to the public WebSocket (port 5050, no auth on upgrades) can craft a sequence that sends media frames without a start event, burning Deepgram credits without ever presenting a token. The LLM and TTS are safely gated on stream_sid being set, but Deepgram is not.

Suggested fix — add an authenticated flag, set it on successful token redemption, and gate Deepgram forwarding behind it:

authenticated = False

while True:
    data = ws.receive(timeout=30)
    if data is None:
        continue
    msg = json.loads(data)
    if msg["event"] == "start":
        stream_sid = msg["start"]["streamSid"]
        call_sid = msg["start"]["callSid"]
        params = msg["start"].get("customParameters") or {}
        if not _redeem_stream_token(params.get("token"), call_sid):
            print(f"⛔ /ws/call rejected: missing/invalid stream token ({call_sid})")
            break
        authenticated = True          # ← set only after successful redemption
    elif msg["event"] == "media":
        if authenticated and dg_conn:  # ← gate on auth
            audio = base64.b64decode(msg["media"]["payload"])
            dg_conn.send_media(audio)
    elif msg["event"] == "stop":
        break

🟡 NativeTTS docstring says "Not implemented"

The method exists and deliberately returns silence — the docstring is misleading:

# local_voice.py
def synthesize_for_twilio(self, text: str) -> bytes:
    """Synthesize using macOS native TTS. Returns PCM16 at 8kHz.

    Not implemented: AVSpeechSynthesizer has no straightforward
    bytes-output path, so this engine always falls back to silence.
    """
    return b""

The phrase "Not implemented" could be confused with a stub that will be filled in later. Consider:

    """Always returns silence.

    AVSpeechSynthesizer has no direct PCM-bytes output path without a
    subprocess; this engine exists only as a last-resort fallback so the
    VoiceEngine initialisation doesn't crash on non-Apple-Silicon Macs.
    """

✅ What's well done

  • Stream token design (_issue_stream_token / _redeem_stream_token): single-use, TTL-bound, constant-time compare, lock-guarded — solid.
  • STT turn assembly (_accumulate_stt_message): correctly distinguishes speech_final (end-of-utterance) from is_final (per-segment), with a documented fallback when the SDK omits speech_final. All five turn-assembly cases are tested.
  • XSS fix in dashboard.html: escHtml now uses a proper character-to-entity map (closes the " and ' gap), and the onclickdata-sid+addEventListener refactor removes the injection vector cleanly.
  • Provider registry: info.get("backend", "local") instead of info["backend"] correctly closes the KeyError that caused #68.
  • ensure_models() non-installing: boot-time pip installs eliminated; the --install CLI is a clean alternative.
  • DEFAULT_AGENT_PROVIDER unification: server and factory share the same constant — the split-brain issue in #67 is properly closed.
  • CI: requirements.txt-driven install + ruff check . before pytest is the right ordering.
  • Platform markers: audioop-lts; python_version >= "3.13", rumps; sys_platform == "darwin", and pywebview marker are all correct.
  • Test coverage: TestStreamTokens covers all six token edge cases (correct SID, single-use, wrong SID, missing/forged, expired, prune-on-issue). TestTurnAssembly covers all five transcript assembly behaviours.

CodeRabbit caught that media frames were forwarded to Deepgram before
the start event's token validation, so a direct connection to the
public WS could burn STT quota without authenticating. The handler now
sets an authenticated flag only after successful token redemption and
gates send_media on it; stream_sid/call_sid are likewise only assigned
post-auth.

Also: register /ws/call via call instead of decorator (flask-sock's
route decorator returns None, which unbound handle_ws and made it
untestable), add WS-level regression tests driving handle_ws with a
fake socket + fake Deepgram client (media-before-start, media-after-
invalid-start, media-after-valid-start), and reword the NativeTTS
docstring per review.

https://claude.ai/code/session_01R2pGk31EzMvwumYCQ8TFNo
Comment thread server.py Outdated
Comment thread server.py
@kilo-code-bot

kilo-code-bot Bot commented Jun 12, 2026

Copy link
Copy Markdown

Code Review Summary

Status: 7 Issues Found | Recommendation: Address before merge

Overview

Severity Count
CRITICAL 0
WARNING 7
SUGGESTION 0
Issue Details (click to expand)

CRITICAL

File Line Issue
N/A N/A No critical issues found.

WARNING

File Line Issue
server.py 294 check_auth() can raise TypeError on non-string tokens.
server.py 1234 JSON null values are stringified as None.
Other Observations (not in diff)

Issues found in unchanged code that cannot receive inline comments:

File Line Issue
server.py 206 twilio_validator is built from import-time TWILIO_TOKEN; changing TWILIO_AUTH_TOKEN through settings will not update signature verification until restart.
server.py 670 VOICEMAIL_PIN is an import-time constant; changing it through settings will not affect PIN validation until restart, but the schema does not mark it restart-required.
server.py 1129 .env quote escaping can corrupt values because load_env()/get_all_settings() only strip outer quotes and do not understand backslash escapes.
server.py 1005/1019/1046 Provider IDs are inconsistent across UI and registry: STT whisper vs registry whisper-api, TTS openai vs registry openai-tts, and settings exposes openai-compat without a registry entry.
install.sh 301 Port-forward setup writes an http:// webhook URL while Twilio Media Streams require wss://, which can make the AI stream fail after PIN success.
Files Reviewed (3 files)
  • server.py - 2 issues
  • tests/test_call_flow.py - 0 issues
  • tests/test_dashboard_auth.py - 0 issues

Fix these issues in Kilo Cloud


Reviewed by nex-n2-pro:free · 1,176,849 tokens

…rmed payloads

Kilo review findings on PR #84:

- check_auth/require_dashboard_auth now read the dashboard token live
  via _dashboard_token() (env first, import-time fallback), and changing
  or clearing DASHBOARD_TOKEN through /api/settings revokes all
  existing dashboard sessions — making the schema hint's promise true.
  Old token stops working and the new one works without a restart;
  covered by tests.
- _accumulate_stt_message guards against Deepgram payloads with no
  alternatives so an edge-case message can't raise IndexError on the
  callback thread and silently kill transcript processing for the call.

Partially addresses #72 (DASHBOARD_TOKEN portion).

https://claude.ai/code/session_01R2pGk31EzMvwumYCQ8TFNo
@jaylfc

jaylfc commented Jun 12, 2026

Copy link
Copy Markdown
Owner Author

Kilo findings triaged:

Fixed in the latest commit — the two in-diff items: DASHBOARD_TOKEN now applies live (check_auth reads it live; changing/clearing it via /api/settings revokes all existing sessions, with regression tests), and _accumulate_stt_message guards empty alternatives so edge-case Deepgram payloads can't kill the callback thread.

Already tracked as issues — the five "other observations" in unchanged code: import-time twilio_validator and VOICEMAIL_PIN#72 (config live/restart semantics), .env quote round-trip → #72, provider ID mismatches across UI/registry → #70, port-forward http:// vs required wss://#22 (with installer detail in comments). These are scoped for follow-up PRs rather than this one.


Generated by Claude Code

Comment thread server.py
if not expected:
return True # No token set = no auth (first run)
return hmac.compare_digest(token, DASHBOARD_TOKEN)
return hmac.compare_digest(token, expected)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

WARNING: check_auth() can raise TypeError on non-string tokens

request.json may contain {"token": null} or another non-string value, which makes hmac.compare_digest(None, expected) raise TypeError and break the login flow. Guard the input before comparing.

Suggested change
return hmac.compare_digest(token, expected)
return isinstance(token, (str, bytes)) and hmac.compare_digest(token, expected)

Reply with @kilocode-bot fix it to have Kilo Code address this issue.

Comment thread server.py
@@ -1150,6 +1233,13 @@ def api_update_settings():
else:
update_setting(key, str(value))

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

WARNING: JSON null values are stringified as "None"

api_update_settings() accepts arbitrary JSON values and calls str(value) for every setting. If a client sends DASHBOARD_TOKEN: null, the token becomes the literal string "None" instead of being cleared or rejected, which can leave the dashboard with an unintended password.


Reply with @kilocode-bot fix it to have Kilo Code address this issue.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment