Skip to content

P0 production hardening: Twilio auth, voice-call metrics, Docker/CI fixes - #13

Open
teetangh wants to merge 7 commits into
mainfrom
hardening/p0-voice-prod
Open

P0 production hardening: Twilio auth, voice-call metrics, Docker/CI fixes#13
teetangh wants to merge 7 commits into
mainfrom
hardening/p0-voice-prod

Conversation

@teetangh

@teetangh teetangh commented Jul 10, 2026

Copy link
Copy Markdown
Owner

P0 production-hardening pass from the production-readiness roadmap. No behavior changes to the conversation flow — this PR closes the gaps that block everything else: unauthenticated Twilio surface, images that can't run the voice path, and a metrics stream that was generated and dropped.

What's in here (one commit per item)

  1. fix(bugs) — natural prompt-version sort (v10 ordered before v2), Offer.short() no longer renders /Noned, stale OpenRouter/MarkerStripper docstrings corrected.
  2. fix(secrets)pipecat_provider now reads config via the Secrets settings object with actionable errors instead of bare os.environ[...] KeyErrors, validated before the heavyweight pipecat imports. .env.example gains the missing GROQ_API_KEY plus TWILIO_WEBHOOK_AUTH / WEBHOOK_BASE_URL docs.
  3. feat(security) — every Twilio-called webhook validates X-Twilio-Signature (against the public WEBHOOK_BASE_URL Twilio actually signed); the Media Streams WS rejects connections pre-accept without a valid stateless HMAC token carried on <Stream url>. TWILIO_WEBHOOK_AUTH=auto|on|off keeps dev/CI hermetic; both simulators work under enforcement (the TwiML simulator signs its requests, the streaming simulator uses the ws_token from dial-init). Operator endpoints (dial-init, state) intentionally stay open — operator auth is a follow-up.
  4. feat(metrics) — new VoiceCallMetricsObserver consumes the Pipecat MetricsFrames the pipeline already generates: per-stage TTFB/processing stats, LLM tokens, TTS chars → per-call summary in data/eval_runs/voice_call_metrics.jsonl + voice_llm/voice_stt/voice_tts rows in the CostMeter ledger. Also fixes COST_LOG being frozen at import time. This is the measurement baseline for the latency issues (Latency: config-driven TTS — Rime WebSocket (interim) → Cartesia Sonic (end state) #1, Latency: config-driven voice LLM — Groq Llama-3.3-70B / Claude Haiku 4.5 with tool-call re-validation #2, Latency: prompt diet + prompt caching + turn-taking tuning + pre-synthesized opener #3).
  5. fix(docker) — both images previously failed to build (sources copied after uv pip install ., which hatchling needs present) and lacked voice extras. Verified: images build, import apps.api.main (full pipecat + Smart Turn chain) and PipecatProvider import succeed in-container.
  6. civoice-smoke.yml (fully disabled stub) → ci.yml: unit job on every push/PR with no secrets; live smokes (~$0.06/run) reachable via manual dispatch, gated on unit.

Verification

  • pytest -q: 123 passed (30 new tests: signature validation matrix, WS token accept/reject, metrics observer dedup + cost math, CostMeter regression, prompt-loader sort, secrets errors).
  • Live server booted twice: TWILIO_WEBHOOK_AUTH=on → unsigned webhook 403, operator dial-init open and returning ws_token; off → unsigned webhook passes the gate (404 unknown session).
  • docker build + in-container smoke imports for both images.

Related issues

Unblocks #1, #2, #3 (latency measurement), #5, #6, #7, #8 (metrics/cost plumbing). Roadmap: docs/architecture/10-production-readiness.md.

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features

    • Added optional authentication for voice webhooks and streaming connections.
    • Added voice-call usage, performance, and cost tracking.
    • Added support for OpenAI voice pipeline pricing and provider configuration.
    • Added simulator support for authenticated Twilio requests and streams.
  • Bug Fixes

    • Prompt versions now sort numerically.
    • Offer summaries no longer display “None” for missing expiry dates.
  • Chores

    • Continuous integration now runs unit tests on pushes and pull requests, with voice smoke tests available manually.
    • Updated environment configuration guidance for voice services.

teetangh and others added 7 commits July 10, 2026 18:04
… docstrings

- list_versions sorted lexically by filename so v10 ordered before v2;
  now sorts numerically by the declared version field
- Offer.short() rendered '/Noned' when expiry_days was unset
- streaming.py + place_streaming_voice_call.py docstrings still said
  OpenRouter LLM / MarkerStripperProcessor; actual wiring is OpenAI
  gpt-4o-mini + the end_call tool

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…in .env.example

- pipecat_provider read six secrets via bare os.environ[...] which
  crashed with KeyError; now uses the existing Secrets settings object
  with a _require() helper raising actionable RuntimeErrors
- secrets are validated before the pipecat imports so misconfiguration
  fails deterministically with the most useful message first
- .env.example gains GROQ_API_KEY (required by settings.yaml
  llm.agent_voice but previously undocumented), TWILIO_WEBHOOK_AUTH,
  and WEBHOOK_BASE_URL

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…S token

Previously every /voice/* webhook and the streaming WebSocket were
unauthenticated — anyone who found the tunnel URL could drive calls or
attach a pipeline.

- apps/api/twilio_security.py: X-Twilio-Signature validation as a
  FastAPI dependency (validates against the public WEBHOOK_BASE_URL
  Twilio actually signed) + stateless HMAC stream token for the WS
- enforcement on Twilio-called routes only (twiml/gather/wait/status/
  streaming-twiml); operator endpoints (dial-init/state) remain open —
  operator auth tracked separately
- WS endpoint rejects bad tokens pre-accept (403 handshake) before any
  pipeline resources are built; <Stream url> carries ?token=...
- TWILIO_WEBHOOK_AUTH=auto|on|off (auto: enforced iff TWILIO_AUTH_TOKEN
  is set) keeps local dev and CI hermetic
- simulators keep working under enforcement: the streaming simulator
  uses the ws_token from dial-init; the TwiML simulator signs its
  requests with the documented HMAC-SHA1 algorithm

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ge, COST_LOG fix

The streaming pipeline generated Pipecat MetricsFrames
(enable_metrics/enable_usage_metrics) and dropped them; streaming calls
were also invisible to the cost ledger.

- apps/voice/call_metrics.py: VoiceCallMetricsObserver collects per-stage
  TTFB/processing stats + LLM token and TTS character usage (deduped on
  frame.id — observers see a frame once per pipeline edge) and writes a
  per-call summary to data/eval_runs/voice_call_metrics.jsonl
- finalize() mirrors voice_llm/voice_stt/voice_tts costs into CostMeter
  so scripts/cost_report.py covers voice calls; STT priced by wall-clock
  audio-minutes (documented approximation), TTS by characters
- CostMeter.record_usage(): generic entry point for non-LLMResponse
  usage; PRICES_USD gains gpt-4o-mini; new VOICE_PRICES_USD table
  (Deepgram nova-3 $0.0077/min, Rime mistv2 $0.03/1k chars)
- fix: COST_LOG was resolved at import into a ClassVar, so configuring
  it later silently wrote to the default path; now resolved per call

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…worker images

The previous order (COPY pyproject.toml -> uv pip install . -> COPY
sources) failed the hatchling wheel build outright — packages =
[agents, apps, learning] didn't exist at install time — and even a
successful base install lacked pipecat/twilio, so apps.api.main (which
imports the streaming voice router) could not start in-container.

Verified: both images build; `import apps.api.main` (full pipecat +
Smart Turn chain) and PipecatProvider import succeed in-container.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
voice-smoke.yml was a fully disabled stub (if: false). Renamed to ci.yml:
- unit job runs on every push/PR with no secrets (installs .[voice,dev]
  since the unit suite imports apps.voice.streaming)
- smoke job (live STT/LLM/TTS, ~$0.06/run) unchanged but now reachable
  via manual dispatch, gated on unit passing

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Jul 10, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

Adds Twilio webhook and Media Streams authentication, voice-call metrics and cost logging, validated provider secrets, voice dependency installation, CI changes, prompt ordering correction, and optional offer expiry formatting with focused tests and simulator updates.

Changes

Voice platform changes

Layer / File(s) Summary
Voice runtime and installation wiring
.env.example, .github/workflows/ci.yml, Dockerfile.*
Adds voice and webhook environment settings, separates unit and manual smoke CI jobs, and installs the voice package extra in both images.
Validated provider secrets
apps/voice/pipecat_provider.py, tests/test_pipecat_provider_secrets.py
Validates required provider credentials through get_secrets() and uses the validated values for call setup and Pipecat services.
Twilio webhook and stream authentication
apps/api/twilio_security.py, apps/api/voice_twiml.py, apps/voice/streaming.py, scripts/simulate_*, tests/test_twilio_security.py
Adds configurable HTTP signature checks, session-based WebSocket tokens, protected voice routes, tokenized stream URLs, simulator signing, and authentication coverage.
Voice metrics and cost recording
agents/cost_meter.py, apps/voice/call_metrics.py, apps/voice/streaming.py, tests/test_cost_meter.py, tests/test_call_metrics_observer.py
Adds voice pricing, generic usage records, per-call latency and usage aggregation, JSONL summaries, teardown finalization, and cost-metering tests.

Data and prompt correctness

Layer / File(s) Summary
Numeric prompt version ordering
agents/prompt_loader.py, tests/test_prompt_loader.py
Sorts prompt versions by declared numeric version and tests ordering, champion selection, and missing directories.
Optional offer expiry rendering
agents/schemas.py, tests/test_schemas.py
Omits the expiry suffix when expiry_days is unset and tests both formatting cases.

Estimated code review effort: 4 (Complex) | ~60 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Twilio
  participant API
  participant Streaming
  participant Metrics
  participant CostMeter
  Twilio->>API: Send signed webhook
  API->>API: Validate X-Twilio-Signature
  API-->>Twilio: Return tokenized TwiML
  Twilio->>Streaming: Connect with session token
  Streaming->>Streaming: Verify stream token
  Streaming->>Metrics: Emit pipeline metrics
  Streaming->>Metrics: Finalize on teardown
  Metrics->>CostMeter: Record LLM, STT, and TTS usage
Loading
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 19.40% 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: Twilio auth hardening, voice-call metrics, and Docker/CI fixes.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ 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 hardening/p0-voice-prod

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.

@gemini-code-assist gemini-code-assist 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.

Code Review

This pull request introduces robust Twilio request authentication for webhook and streaming endpoints, adds a comprehensive latency and cost tracking system for streaming voice calls via a new Pipecat metrics observer, and fixes several minor bugs including numerical prompt version sorting and environment variable resolution in CostMeter. The review feedback identifies three key areas for improvement: defensively handling unconfigured Twilio auth tokens in stream_token to avoid an AttributeError, protecting the metrics _stats helper against empty lists to prevent ZeroDivisionError and IndexError, and raising an HTTPException instead of closing the WebSocket to properly return an HTTP 403 status code during handshake rejection.

Important

The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.

Comment on lines +91 to +96
def stream_token(session_id: str) -> str:
"""Stateless auth token for the Media Streams WS endpoint."""
secret = get_secrets().TWILIO_AUTH_TOKEN
return hmac.new(
secret.encode(), f"stream:{session_id}".encode(), hashlib.sha256,
).hexdigest()[:32]

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

medium

If TWILIO_WEBHOOK_AUTH is set to 'on' but TWILIO_AUTH_TOKEN is not configured in the environment, get_secrets().TWILIO_AUTH_TOKEN will be None. In this scenario, calling secret.encode() will raise an AttributeError: 'NoneType' object has no attribute 'encode'. Adding a check to ensure secret is configured before calling .encode() prevents this potential crash.

Suggested change
def stream_token(session_id: str) -> str:
"""Stateless auth token for the Media Streams WS endpoint."""
secret = get_secrets().TWILIO_AUTH_TOKEN
return hmac.new(
secret.encode(), f"stream:{session_id}".encode(), hashlib.sha256,
).hexdigest()[:32]
def stream_token(session_id: str) -> str:
"""Stateless auth token for the Media Streams WS endpoint."""
secret = get_secrets().TWILIO_AUTH_TOKEN
if not secret:
raise RuntimeError("TWILIO_AUTH_TOKEN is not configured")
return hmac.new(
secret.encode(), f"stream:{session_id}".encode(), hashlib.sha256,
).hexdigest()[:32]

Comment on lines +48 to +56
def _stats(values: list[float]) -> dict:
ordered = sorted(values)
p95_index = max(0, round(0.95 * (len(ordered) - 1)))
return {
"avg": round(sum(ordered) / len(ordered), 4),
"p95": round(ordered[p95_index], 4),
"max": round(ordered[-1], 4),
"n": len(ordered),
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

medium

If values is empty (e.g., if a processor did not record any metrics due to an early call termination or error), len(ordered) will be 0, leading to a ZeroDivisionError when calculating the average, and an IndexError when accessing ordered[p95_index]. Adding a defensive check for empty values ensures the observer finalizes successfully in all edge cases.

Suggested change
def _stats(values: list[float]) -> dict:
ordered = sorted(values)
p95_index = max(0, round(0.95 * (len(ordered) - 1)))
return {
"avg": round(sum(ordered) / len(ordered), 4),
"p95": round(ordered[p95_index], 4),
"max": round(ordered[-1], 4),
"n": len(ordered),
}
def _stats(values: list[float]) -> dict:
if not values:
return {"avg": 0.0, "p95": 0.0, "max": 0.0, "n": 0}
ordered = sorted(values)
p95_index = max(0, round(0.95 * (len(ordered) - 1)))
return {
"avg": round(sum(ordered) / len(ordered), 4),
"p95": round(ordered[p95_index], 4),
"max": round(ordered[-1], 4),
"n": len(ordered),
}

Comment thread apps/voice/streaming.py
Comment on lines +424 to +428
# Pre-accept close rejects the handshake (HTTP 403) before any
# pipeline resources are constructed.
logger.warning("Streaming WS rejected for session_id=%s: bad token", session_id)
await websocket.close(code=1008)
return

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

medium

Calling websocket.close(code=1008) on an unaccepted WebSocket connection does not return a proper HTTP 403 Forbidden response during the handshake. In FastAPI, raising an HTTPException(status_code=403) before calling await websocket.accept() is the standard and clean way to reject the handshake with a 403 status code, preventing any pipeline resources from being constructed.

Suggested change
# Pre-accept close rejects the handshake (HTTP 403) before any
# pipeline resources are constructed.
logger.warning("Streaming WS rejected for session_id=%s: bad token", session_id)
await websocket.close(code=1008)
return
logger.warning("Streaming WS rejected for session_id=%s: bad token", session_id)
from fastapi import HTTPException
raise HTTPException(status_code=403, detail="Invalid stream token")

@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: 3

🤖 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 @.github/workflows/ci.yml:
- Line 60: Update the checkout step in the smoke job to set persist-credentials:
false on actions/checkout@v4, preventing unnecessary GITHUB_TOKEN credentials
from being stored in the repository configuration.

In `@agents/cost_meter.py`:
- Around line 39-49: Update the VOICE_PRICES_USD constants for
“deepgram/nova-3-general” to 0.0078 per audio-minute and “rime/mistv2” to 0.05 /
1000 per character, replacing the stale values.

In `@apps/api/twilio_security.py`:
- Around line 91-100: Update stream_token() to validate that TWILIO_AUTH_TOKEN
is present before creating the HMAC, mirroring the existing webhook
authentication guard; reject missing or empty credentials rather than signing
with an empty secret, and ensure verify_stream_token() propagates that
rejection.
🪄 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: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 985d2643-c252-4de0-a93d-546c2eb9e1e7

📥 Commits

Reviewing files that changed from the base of the PR and between bab31c3 and 33cb5bb.

📒 Files selected for processing (21)
  • .env.example
  • .github/workflows/ci.yml
  • Dockerfile.api
  • Dockerfile.worker
  • agents/cost_meter.py
  • agents/prompt_loader.py
  • agents/schemas.py
  • apps/api/twilio_security.py
  • apps/api/voice_twiml.py
  • apps/voice/call_metrics.py
  • apps/voice/pipecat_provider.py
  • apps/voice/streaming.py
  • scripts/place_streaming_voice_call.py
  • scripts/simulate_twilio_stream.py
  • scripts/simulate_twiml_call.py
  • tests/test_call_metrics_observer.py
  • tests/test_cost_meter.py
  • tests/test_pipecat_provider_secrets.py
  • tests/test_prompt_loader.py
  • tests/test_schemas.py
  • tests/test_twilio_security.py

Comment thread .github/workflows/ci.yml
timeout-minutes: 15

steps:
- uses: actions/checkout@v4

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🔒 Security & Privacy | 🟡 Minor | ⚡ Quick win

Set persist-credentials: false on checkout.

The smoke job doesn't push, so the persisted GITHUB_TOKEN on the checked-out repo is unnecessary attack surface. Flagged by zizmor (artipacked).

🔒 Proposed hardening
-      - uses: actions/checkout@v4
+      - uses: actions/checkout@v4
+        with:
+          persist-credentials: false
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
- uses: actions/checkout@v4
- uses: actions/checkout@v4
with:
persist-credentials: false
🧰 Tools
🪛 zizmor (1.26.1)

[warning] 60-60: credential persistence through GitHub Actions artifacts (artipacked): does not set persist-credentials: false

(artipacked)

🤖 Prompt for 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.

In @.github/workflows/ci.yml at line 60, Update the checkout step in the smoke
job to set persist-credentials: false on actions/checkout@v4, preventing
unnecessary GITHUB_TOKEN credentials from being stored in the repository
configuration.

Source: Linters/SAST tools

Comment thread agents/cost_meter.py
Comment on lines +39 to +49

# OpenAI direct — the streaming voice path's LLM (apps/voice/streaming.py)
"gpt-4o-mini": (0.15, 0.075, 0.60),
}

# Non-token voice pricing (July 2026 published pay-as-you-go rates).
VOICE_PRICES_USD: dict[str, float] = {
# Deepgram streaming STT — USD per audio-minute
"deepgram/nova-3-general": 0.0077,
# Rime Mist v2 TTS — USD per character ($0.03 / 1k chars)
"rime/mistv2": 0.03 / 1000,

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🌐 Web query:

What is the current Deepgram nova-3 pay-as-you-go pricing per minute?

💡 Result:

As of July 10, 2026, the pay-as-you-go pricing for Deepgram Nova-3 is as follows: * Nova-3 (Monolingual): $0.0077 per minute [1][2][3][4]. * Nova-3 (Multilingual): $0.0092 per minute [4]. These rates apply to both real-time streaming and batch processing [1][4]. Deepgram utilizes per-second billing precision [4]. Pricing can vary based on factors such as the number of audio channels being processed, as costs multiply by the channel count [3]. Additional information regarding billing and plans can be found on Deepgram's official pricing page [5][6].

Citations:


🌐 Web query:

What is the current Rime TTS mist v2 pricing per character?

💡 Result:

As of July 10, 2026, Rime AI does not publish a single flat rate per character for the Mist model, as its pricing is structured primarily through tiered monthly subscription plans rather than a universal pay-as-you-go rate [1][2]. While older documentation and third-party cost trackers have historically cited various pay-as-you-go rates (such as $20 to $30 per million characters) [3][4][2], Rime's current official pricing model is built around the following subscription tiers: - Starter: $5/month for 100,000 characters - Developer: $19/month for 500,000 characters - Pro: $99/month for 3,000,000 characters - Business: $249/month for 10,000,000 characters - Enterprise: Custom pricing for high-volume requirements [1][2]. For users requiring specific per-character rates, particularly those exceeding standard tiers, Rime provides custom packages through their Enterprise and high-volume sales channels [5][1]. You can find the most current and account-specific details by visiting the official Rime pricing page at rime.ai/pricing [5].

Citations:


🌐 Web query:

site:rime.ai/pricing Rime pricing mist per character official

💡 Result:

Rime AI's pricing for its text-to-speech services starts at $0.05 per 1,000 characters [1]. The platform also offers volume pricing for larger organizations operating at scale [1]. Users can begin with a free plan that includes 3,000 minutes of usage [1].

Citations:


🌐 Web query:

site:deepgram.com/pricing Nova-3 monolingual $0.0077 per minute official

💡 Result:

The official pricing for the Deepgram Nova-3 Monolingual model is $0.0078 per minute [1]. Your reference to $0.0077 per minute does not match the current official pricing listed on the Deepgram website [1].

Citations:


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Inspect the relevant file and nearby references to the pricing constants.
git ls-files agents/cost_meter.py
wc -l agents/cost_meter.py
sed -n '1,140p' agents/cost_meter.py

printf '\n--- references ---\n'
rg -n "VOICE_PRICES_USD|deepgram/nova-3-general|rime/mistv2|gpt-4o-mini|cost_meter" agents -n

Repository: teetangh/defaultline

Length of output: 5463


Update the voice pricing constants in agents/cost_meter.py:45-49

  • deepgram/nova-3-general should be 0.0078 per minute.
  • rime/mistv2 should use 0.05 / 1000 per character; 0.03 / 1000 is stale.
🤖 Prompt for 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.

In `@agents/cost_meter.py` around lines 39 - 49, Update the VOICE_PRICES_USD
constants for “deepgram/nova-3-general” to 0.0078 per audio-minute and
“rime/mistv2” to 0.05 / 1000 per character, replacing the stale values.

Comment on lines +91 to +100
def stream_token(session_id: str) -> str:
"""Stateless auth token for the Media Streams WS endpoint."""
secret = get_secrets().TWILIO_AUTH_TOKEN
return hmac.new(
secret.encode(), f"stream:{session_id}".encode(), hashlib.sha256,
).hexdigest()[:32]


def verify_stream_token(session_id: str, token: str) -> bool:
return hmac.compare_digest(stream_token(session_id), token or "")

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Locate the relevant files and inspect the surrounding code.
git ls-files | rg 'apps/api/(twilio_security\.py|voice_twiml\.py|streaming\.py)$'

echo
echo '--- apps/api/twilio_security.py ---'
wc -l apps/api/twilio_security.py
cat -n apps/api/twilio_security.py | sed -n '1,180p'

echo
echo '--- apps/api/voice_twiml.py ---'
wc -l apps/api/voice_twiml.py
cat -n apps/api/voice_twiml.py | sed -n '1,220p'

echo
echo '--- apps/api/streaming.py ---'
wc -l apps/api/streaming.py
cat -n apps/api/streaming.py | sed -n '1,240p'

Repository: teetangh/defaultline

Length of output: 15160


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Find every use of the stream token helpers and the streaming endpoints.
rg -n "stream_token|verify_stream_token|TWILIO_AUTH_TOKEN|TWILIO_WEBHOOK_AUTH|streaming_ws|streaming_dial_init|streaming_twiml" apps api . -g '!**/.git/**'

echo
echo '--- apps/api/voice_twiml.py around streaming routes ---'
sed -n '220,420p' apps/api/voice_twiml.py | cat -n

echo
echo '--- any file named *stream* or *twilio* in apps/api ---'
find apps/api -maxdepth 1 -type f | rg 'stream|twilio'

Repository: teetangh/defaultline

Length of output: 7473


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Inspect the streaming call sites and the settings default for TWILIO_AUTH_TOKEN.
sed -n '380,460p' apps/api/voice_twiml.py | cat -n
echo
sed -n '400,500p' apps/voice/streaming.py | cat -n
echo
sed -n '90,130p' agents/settings.py | cat -n
echo
sed -n '1,220p' tests/test_twilio_security.py | cat -n

Repository: teetangh/defaultline

Length of output: 17392


Guard stream_token() when TWILIO_AUTH_TOKEN is missing.
With TWILIO_WEBHOOK_AUTH=on and no auth token configured, this still signs with the empty string, so the WS token is predictable and verify_stream_token() accepts forgeable tokens. Mirror the webhook guard here and reject missing TWILIO_AUTH_TOKEN before signing.

🤖 Prompt for 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.

In `@apps/api/twilio_security.py` around lines 91 - 100, Update stream_token() to
validate that TWILIO_AUTH_TOKEN is present before creating the HMAC, mirroring
the existing webhook authentication guard; reject missing or empty credentials
rather than signing with an empty secret, and ensure verify_stream_token()
propagates that rejection.

@teetangh teetangh self-assigned this Jul 10, 2026
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