Achado real reportado pelo usuário: "a ligacao entre ramais nao esta funcionando". Não era regressão de nada desta sessão — o tenant onde o usuário registrou os próprios ramais de teste tinha ZERO regras de dialplan e ZERO versão ativa. Nenhum tenant nascia com a regra de "discagem interna": só existia nos tenants onde eu mesmo configurei manualmente durante testes anteriores (Acme). Todo tenant novo nascia incapaz de ligar entre os próprios ramais até alguém configurar isso à mão em Telefonia > Dialplan — um gap real de produto. `DEFAULT_DIALPLAN_EXTENSIONS` (novo, packages/telephony) tem as mesmas 2 regras já testadas ponta a ponta com chamada real (PHASE 53): discagem interna com pickup de grupo + `*8`. `TenantsController.create()` agora semeia essas regras e já ativa a versão 1 do contexto default, na mesma transação de criação do tenant. Reparo pontual aplicado no tenant existente do usuário (mesmas 2 regras, via SQL direto — não existe endpoint de "reparar tenant já criado"). Testado com uma chamada real: originate de um ramal pro outro tocou de verdade no aparelho do usuário (RINGING -> NO_ANSWER, não mais dialplan not found). Auto-seed testado criando um tenant novo de verdade via POST /tenants — confirmado no banco que a versão 1 já nasce ACTIVE. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BFaBaBSQGhyXGEgtTYZGV8
5.2 KiB
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<condition>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=<tenant>}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
<condition>por extension (simplificação) — FreeSWITCH suporta múltiplas em sequência; revisitar se algum caso de uso real precisar. dialplan.view/dialplan.managenão existem como permissions próprias — reuseifreeswitch.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).