--- description: emergency-number guard, concurrent-call cap, dial-path discipline — the outbound safety invariants paths: - "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.