# Dialplan Agente.md secao 43-44. Editor estruturado (não textarea) + versionamento com ativação/rollback explícitos. ## Modelo - `dialplan_extensions` (tenant-scoped, RLS): fonte editável — `context`, `name`, `conditionField`/`conditionExpr` (uma condição por extension, simplificação deliberada em relação ao FreeSWITCH puro, que permite várias `` por extension), `actions`/`antiActions` (JSON, array de `{application, data}`), `continueOnFalse`, `order`, `enabled`. - `dialplan_versions` (tenant-scoped, RLS): snapshot gerado — `context`, `version` (incremental por tenant+context), `generatedXml`, `status` (`DRAFT`/`ACTIVE`/`SUPERSEDED`), `createdByUserId`, `activatedAt`. ## Fluxo (agente.md secao 44) ``` POST /dialplan/extensions -- edita as linhas (não afeta chamadas) POST /dialplan/versions/generate -- gera XML a partir das linhas atuais, valida bem-formação (fast-xml-parser), salva como nova versão DRAFT POST /dialplan/versions/:id/activate -- marca ACTIVE, a anterior vira SUPERSEDED (transação atômica) ``` **Reativar uma versão antiga é o próprio rollback** — não existe endpoint separado. `GET /dialplan/versions` mostra o histórico completo. ## Dialplan padrão pra tenant novo (PHASE 57) Achado real reportado pelo usuário: "a ligação entre ramais não está funcionando" — nenhum tenant nascia com nenhuma regra de dialplan, então nenhum tenant novo conseguia discar entre os próprios ramais até alguém configurar manualmente em Telefonia > Dialplan. `DEFAULT_DIALPLAN_EXTENSIONS` (`packages/telephony/src/default-dialplan.ts`) tem as 2 regras mínimas (discagem interna com pickup de grupo + `*8`, já testadas ponta a ponta com chamada real na PHASE 53) — `TenantsController.create()` semeia essas regras e já ativa a versão 1 do contexto `default`, dentro da MESMA transação de criação do tenant. Um tenant criado ANTES desta fase precisa de reparo manual (inserir as mesmas 2 linhas + gerar/ativar uma versão). ## Mecanismo: dinâmico por chamada, não arquivo+rescan Ao contrário de Trunks (gateways carregados uma vez no boot/rescan), dialplan é resolvido pelo FreeSWITCH **a cada chamada** via `mod_xml_curl` — então não precisa de sincronização por arquivo nem de avisar o FreeSWITCH quando uma versão é ativada. `b2bcall-fs-config` serve `generatedXml` da versão `ACTIVE` diretamente na resposta HTTP; "ativar" uma versão tem efeito imediato na próxima chamada. Resolução de tenant é feita pelo channel variable `variable_b2bcall_tenant_id` — que já vem setado desde o registro/ autenticação do ramal (`buildDirectoryUserXml`, fase Extensions). Diferente do directory (resolvido por `domain`, hoje um valor fixo compartilhado por todos os tenants — limitação conhecida), o dialplan não sofre dessa ambiguidade porque a variável já está no contexto da própria chamada. ## Segurança: allowlist de applications (agente.md secao 180) `ALLOWED_DIALPLAN_APPLICATIONS` (`packages/telephony/src/dialplan-xml.ts`) restringe `action.application` a um conjunto seguro (`answer`, `bridge`, `playback`, `hangup`, `set`, `export`, ...) — de propósito **sem** `system`, `exec`, `socket`/Lua-eval ou qualquer application capaz de rodar comandos no host. Sem isso, um tenant_admin malicioso ou comprometido poderia configurar uma "rota de dialplan" que executa comandos arbitrários no servidor FreeSWITCH. ## Achado crítico durante os testes desta fase Ao testar deliberadamente que a allowlist rejeitava `application: "system"`, a API **aceitou** com 201 — a validação inteira de `apps/api` (todo `@Body()`, em todos os controllers, desde que a API foi criada) estava silenciosamente inoperante rodando via `tsx`. Causa raiz, correção e o que foi limpo: ver **docs/VALIDATION_PIPE_BUG.md** (achado grande o suficiente pra merecer documento próprio). Resumo da correção: `apps/api` agora builda com `tsc` de verdade antes de rodar — nunca mais `tsx src/main.ts` direto. ## Verificado ponta a ponta (já com a validação corrigida) ``` POST /dialplan/extensions {"conditionExpr":"^7000$","actions":[answer,playback,hangup]} POST /dialplan/versions/generate?context=default → v1 DRAFT POST /dialplan/versions/:id/activate → v1 ACTIVE originate {b2bcall_tenant_id=}null/_test_ 7000 XML default → fs-events: CALL_CREATED → CALL_ANSWERED → CALL_ENDED (tenantId correto) ``` Criei uma segunda versão (v2, rota diferente), ativei — v1 virou `SUPERSEDED`. Reativei v1 (rollback) — v2 virou `SUPERSEDED`, v1 voltou pra `ACTIVE`. Confirmado com `GET /dialplan/versions`. ## O que falta - Só uma `` por extension (simplificação) — FreeSWITCH suporta múltiplas em sequência; revisitar se algum caso de uso real precisar. - `dialplan.view`/`dialplan.manage` não existem como permissions próprias — reusei `freeswitch.view`/`freeswitch.configure` (já existentes na seção 145 do agente.md), que cobrem semanticamente "configuração do FreeSWITCH". Se precisar de granularidade maior no futuro, criar permissions dedicadas. - Sem UI ainda (fase Frontend).