Files
hold-slayer/.claude/rules/call-safety.md
Robert Helewka 4a3c14d4af
All checks were successful
CVE Scan & Docker Build / security-scan (push) Successful in 45s
CVE Scan & Docker Build / build-and-push (push) Successful in 1m53s
docs: add Claude AI assistant rules and configuration
Add comprehensive rule documentation for AI-assisted development covering
authentication surfaces, outbound-call safety invariants, and other project
conventions to guide Claude's understanding of critical system behaviors.
2026-07-28 19:01:38 -04:00

2.3 KiB
Raw Blame History

description, paths
description paths
emergency-number guard, concurrent-call cap, dial-path discipline — the outbound safety invariants
core/dial_plan.py
core/gateway.py
mcp_server/server.py
api/calls.py

Outbound-call safety

This is the safety core. A defect here means an unwanted real phone call, a runaway telephony bill, or — the one that matters most — an AI-initiated emergency call that should have been impossible.

  • is_emergency_number() is the single source of truth for refusal. It lives in core/dial_plan.py, blocks 911/9911/112 and their E.164 mappings (_BLOCKED = keys values), and normalises the input (strips spaces, dashes, dots) before comparing. If you learn of another dialled form that reaches emergency services, add it to EMERGENCY_NUMBERS — never work around the guard.

  • Every outbound path goes through gateway.make_call, and the guard is its first check — before the concurrency cap, before create_call, before any SIP action. REST make_call, MCP make_call, receptionist ring-back, and any transfer to an external number must funnel through it. Do not introduce a dial path that reaches sip_engine.make_call without passing the guard first. If a new feature needs to place a call, it calls gateway.make_call.

  • The concurrency cap is spend control, not decoration. max_concurrent_calls (default 4) is checked in make_call after the emergency guard and before call creation, using len(call_manager.active_calls). Keep the ordering: refuse-emergency, then cap, then create. Don't move the count to after creation (it would off-by-one) and don't remove it.

  • Refusals raise ValueError at the gateway; surfaces translate it. make_call raises ValueError for both refusals; the MCP tool converts it to ToolError, and the REST layer maps it to a 4xx. Keep refusals as exceptions from the gateway — a refused call must never look like a placed one.

  • The README's [!CAUTION] block is a contract, not decoration. If you change refusal behaviour, the README caution and this rule must stay true. A system that quietly stops refusing emergency numbers is a serious regression even if every test still passes — add/keep a test that asserts each blocked form is refused.