# Spec 14 — Contrato de Governança para Agentes Externos (MCP) > **Verificado em 2026-07-18 contra `gov/G6` @ `ddcc511`.** Toda referência > `arquivo:linha` abaixo foi aberta e conferida nesta data/commit. Se o código > mudar, a linha muda — reverifique antes de confiar. > > **Autocontido.** Um agente externo (o **Vendaval** — runtime de agentes de IA > 1:N, fora deste repo) lê SÓ este documento para saber como falar com o > DeskcommCRM via MCP: quais tools existem, o input/output exato de cada uma, a > semântica de governança que ele precisa respeitar e o que ele **nunca** pode > fazer. Estilo edge-contract: refs reais, proibições explícitas, zero remissão > obrigatória a outro doc. > > Complementa a `13-spec-governanca-atendimento.md` (a doutrina de governança) e > a `11-spec-mcp-server-internal.md` (o servidor MCP). Doutrina soberana: > `CLAUDE.md` do repo (multi-tenancy, RLS, RBAC, LGPD, audit, migrations). --- ## 0. Modelo mental (leia primeiro) O DeskcommCRM expõe um **servidor MCP org-scoped**. O agente externo se autentica com um bearer token que carrega **uma organização** e **um conjunto de scopes** (`mcp:read` / `mcp:write`) + um **role** (`viewer`<`agent`<`manager`<`admin`). A partir daí: - **`organization_id` vem SEMPRE do contexto de autenticação (`ctx.organizationId`), NUNCA do input da tool.** As tools de governança carimbam isto explicitamente no cabeçalho do módulo (`lib/mcp/tools/governance.ts:9`). O agente externo não consegue "escolher" a org por parâmetro — ela é fixada pelo token. - **Toda leitura passa pela RLS org-scoped + visibilidade** (§4). Service role bypassa RLS, por isso cada handler ainda filtra `organization_id` manualmente como defesa em profundidade (ex.: `lib/mcp/tools/governance.ts:72-74`). - **Toda escrita relevante gera audit** (`api_audit_log`, fire-and-forget) — o agente externo é auditado como ator não-humano (`actor_type` no metadata, `lib/mcp/tools/governance.ts:21-34`). - **Idempotência** é responsabilidade do contrato: comandos de atribuição são idempotentes sob replay e corrida (§3.1); envio aceita `idempotency_key`. Quando o agente externo assume o dispatch de uma org (§5, `ai_dispatch_mode='external'`), o dispatcher **nativo** deste repo **para de tocar** os eventos daquela org — o Vendaval os consome. O resto do contrato (tools, RLS, proibições) continua valendo igual. --- ## 1. Catálogo de tools (fonte única: `lib/mcp/tools/catalog.ts`) O catálogo estático `TOOL_CATALOG` (`lib/mcp/tools/catalog.ts:19-39`) é a lista canônica. Um **sanity check em runtime** (`lib/mcp/tools/index.ts:62-75`) falha o boot em dev se um handler existir sem entrada no catálogo — ou vice-versa —, garantindo **1:1 entre handler e catálogo**. Nenhuma tool citada aqui existe sem handler; nenhuma tool de governança do catálogo ficou de fora deste contrato. | Tool | Categoria | catalog.ts | handler | Governança de atendimento? | Neste contrato | |---|---|---|---|---|---| | `crm_search_contacts` | read | L21 | `contacts.ts` | não (busca genérica) | fora do escopo | | `crm_get_contact` | read | L22 | `contacts.ts` | não (leitura genérica) | fora do escopo | | `crm_list_conversations` | read | L23 | `conversations.ts:31` | **sim** (assignee_kind, tags, queue_position) | §3.5 | | `crm_get_conversation` | read | L24 | `conversations.ts:94` | **sim** | §3.5 | | `crm_get_conversation_history` | read | L25 | `conversations.ts:148` | adjacente (contexto de mensagens) | §3.5 (menção) | | `crm_get_queue_status` | read | L26 | `governance.ts:236` | **sim** (fila) | §3.3 | | `crm_list_leads` | read | L27 | `leads.ts:82` | **sim** (owner, stage, tags) | §3.6 | | `crm_get_lead` | read | L28 | `leads.ts:124` | **sim** | §3.6 | | `crm_list_pipelines` | read | L29 | `pipelines.ts` | não (metadados de pipeline) | fora do escopo | | `crm_create_lead` | write | L31 | `leads.ts:173` | não (CRUD de lead) | fora do escopo | | `crm_update_lead` | write | L32 | `leads.ts:227` | não (CRUD de lead) | fora do escopo | | `crm_move_lead_stage` | write | L33 | `leads.ts:263` | não (CRUD de lead) | fora do escopo | | `crm_send_whatsapp_message` | write | L34 | `messages.ts:38` | não (envio) — mas tocado pelas proibições (`is_blocked`) | §6.2 | | `crm_assign_conversation` | write | L35 | `governance.ts:54` | **sim** | §3.1 | | `crm_manage_tags` | write | L36 | `governance.ts:174` | **sim** | §3.2 | | `crm_request_human_handoff` | handoff | L38 | `handoff.ts:38` | **sim** | §3.4 | **As 8 tools de governança que o Vendaval consome** (todas documentadas em §3): `crm_assign_conversation`, `crm_manage_tags`, `crm_get_queue_status`, `crm_request_human_handoff`, `crm_get_conversation`, `crm_list_conversations`, `crm_get_lead`, `crm_list_leads`. As tools **fora do escopo** deste contrato de governança são leitura/CRUD genérico (contatos, pipelines, CRUD de lead) e não expõem nem alteram estado de governança de atendimento. `crm_send_whatsapp_message` não é de governança, mas as proibições §6.2 a tocam (guard `is_blocked`), então está documentada lá. --- ## 2. Convenções de todas as tools - **Role/scope** por tool são checados no server core antes do handler. As tools de governança exigem `role>=agent` + o scope da categoria (`mcp:read` para read, `mcp:write` para write/handoff) — ver `requiresRole`/`requiresScope` em cada definição (ex.: `governance.ts:63-64`). As tools de CRUD de lead exigem `role>=manager` (`leads.ts:179-180`). - **Input** é validado por Zod no handler; violação lança erro (o agente recebe a mensagem de erro da tool). Campos com `.default(...)` são opcionais. - **`organization_id` nunca é input.** Não existe em nenhum `inputShape` de governança — é injetado do ctx. - **Erros** são strings de código (ex.: `conversation_not_found`, `assignment_conflict`, `target_not_found`) — sem PII (LGPD). --- ## 3. As tools de governança (input/output exatos) ### 3.1 `crm_assign_conversation` — atribuir / transferir / liberar Handler: `lib/mcp/tools/governance.ts:54`. Categoria `write`, `role>=agent`, scope `mcp:write`. **Input** (`governance.ts:40-45`): | Campo | Tipo | Default | Regra | |---|---|---|---| | `conversation_id` | uuid | — | obrigatório | | `to_user_id` | uuid \| null | `null` | `null` = release (volta à fila) | | `reason` | `"transfer"` \| `"release"` | `"transfer"` | cross-field abaixo | Cross-field (`governance.ts:48-52`): `release ⇔ to_user_id null`; `transfer ⇔ to_user_id preenchido`. Violar isso rejeita antes de qualquer efeito. **Output** — sempre um objeto (`governance.ts:141-147` / `84-90` / `118-124`): ```json { "assigned": true, "conversation_id": "", "assigned_to_user_id": "", "reason": "transfer|release", "idempotent": true|false } ``` **Semântica de idempotência e corrida** (crítica para um runtime 1:N): - **Replay** (comando repetido após já aplicado): a conversa já está no dono alvo ⇒ retorna `idempotent: true` sem novo assignment event (`governance.ts:83-91`). - **Corrida** (dois assigns idênticos concorrentes): optimistic lock via `p_expected_assignee` — o 1º vence com 1 evento; o 2º vê 0 rows, re-lê e, se o dono já é o alvo, devolve `idempotent: true`; senão `assignment_conflict` (`governance.ts:98-127`). - O efeito real (UPDATE + assignment event na mesma transação) é a RPC `fn_conversation_assign` (migration 0031/0032). Recusa cross-org — ver §6.1. - Sucesso não-idempotente grava audit `conversation.transferred|released` (`governance.ts:130-139`). ### 3.2 `crm_manage_tags` — add/remove tags Handler: `lib/mcp/tools/governance.ts:174`. Categoria `write`, `role>=agent`, scope `mcp:write`. **Input** (`governance.ts:167-172`): | Campo | Tipo | Regra | |---|---|---| | `target_kind` | `"conversation"` \| `"contact"` \| `"lead"` | obrigatório | | `target_id` | uuid | obrigatório; alvo tem que ser da org (`governance.ts:196-197`) | | `add` | string[] | opcional | | `remove` | string[] | opcional | Validações (mesma regra G3-05, `governance.ts:185-205`): cada tag é normalizada (**lowercase, trim, ≤40 chars**) por `conversationTagSchema`; o conjunto final é deduplicado e limitado a **≤20 tags** por `conversationTagsSchema` (rejeita se estourar). É obrigatório ao menos um de `add`/`remove` (`governance.ts:187-189`). **Output** (`governance.ts:226`): ```json { "target_kind": "conversation|contact|lead", "target_id": "", "tags": ["..."] } ``` Grava audit `conversation.tags_changed|contact.tags_changed|lead.tags_changed` (`governance.ts:214-224`). ### 3.3 `crm_get_queue_status` — snapshot da fila Handler: `lib/mcp/tools/governance.ts:236` → `getQueueStatus` (`lib/routing/queue.ts:27`). Categoria `read`, `role>=agent`, scope `mcp:read`. **Input:** vazio (`governance.ts:234`). **Output** (`lib/routing/queue.ts:21-25`, `53-57`): ```json { "queue_size": 0, "avg_wait_seconds": 0, "online_eligible_count": 0 } ``` - `queue_size`: conversas na fila = **sem dono ∧ `status='open'`** (`queue.ts:32-37`). - `avg_wait_seconds`: média de `now − last_inbound_at` das conversas na fila, em segundos, arredondada; 0 se fila vazia (`queue.ts:42-49`). - `online_eligible_count`: atendentes elegíveis AGORA (disponível ∧ horário ∧ com folga de capacidade) — quem pode puxar da fila (`queue.ts:51`). ### 3.4 `crm_request_human_handoff` (v2) — bot → humano Handler: `lib/mcp/tools/handoff.ts:38`. Categoria `handoff`, `role>=agent`, scope `mcp:write`. **Input** (`handoff.ts:29-36`): | Campo | Tipo | Default | Nota | |---|---|---|---| | `conversation_id` | uuid | — | obrigatório | | `reason` | string (1..500) | `"requested_human"` | | | `urgency` | `"low"`\|`"normal"`\|`"high"` | `"normal"` | | | `target_user_id` | uuid | — | **opcional**: só atribui se elegível agora; senão rodízio G5 | | `metadata` | record | — | opcional | **Semântica v2 — um algoritmo de destino (roteamento G5)** (`handoff.ts:94-160`): 1. Marca a conversa `pending`, **silencia o bot** (`bot_silenced_until='infinity'`), registra activity + `event_log` (`ai.handoff_triggered`) + audit — via `triggerHandoff` (`handoff.ts:76-88`, cabeçalho `handoff.ts:1-18`). 2. Escolhe o destino entre os **elegíveis** (`loadEligibleAttendants`): usa `target_user_id` se elegível agora, senão **rodízio real** `selectRoundRobin` (`handoff.ts:97-101`). 3. Se escolheu alguém: **reassignment auditado** kind `ai→'user'`, evento `reason='handoff'` na mesma transação (`fn_conversation_assign`, `handoff.ts:104-121`). 4. Sem ninguém elegível: **fila** — limpa `assignee_kind` (`ai→null`), insere evento `reason='handoff'` (from/to null), calcula posição (`handoff.ts:123-160`). **Output estruturado** (`handoff.ts:163-176`) — um dos dois lados é populado: ```json { "handoff_recorded": true, "conversation_id": "", "assigned_to": "", "queued": false, "position": null, "assigned_to_user_id": "", "idempotent": false, "next_action": "Avise o cliente ..." } ``` - Atribuído: `assigned_to` = uuid, `queued=false`, `position=null`. - Enfileirado: `assigned_to=null`, `queued=true`, `position=<1-based>`. - `assigned_to_user_id` é alias de compat com o contrato anterior (`handoff.ts:170-171`). - `idempotent=true` quando o handoff foi suprimido pela janela de 5s (`handoff.ts:172`). ### 3.5 Read tools de conversa (campos de governança expostos — G6-03) **`crm_list_conversations`** (`lib/mcp/tools/conversations.ts:31`) e **`crm_get_conversation`** (`conversations.ts:94`). Categoria `read`, `role>=agent`, scope `mcp:read`. Cada conversa no payload expõe os campos de governança (`conversations.ts:66-83` para list; `117-138` para get): | Campo | Fonte | Semântica | |---|---|---| | `assignee_kind` | `conversations.assignee_kind` | `'user'` (humano atende) \| `'ai'` (IA atende) \| `null` (fila/sem dono) — §3.7 | | `assigned_to_user_id` | coluna | uuid do dono ou null | | `assigned_to_user_name` | resolvido | **só o nome** do atendente, sem email/telefone (LGPD, `conversations.ts:74-76`) | | `tags` | `conversations.tags` | string[] | | `queue_position` | `getQueuePositions` | posição **1-based** na fila do inbox; `null` quando não está na fila (`conversations.ts:19-22`, `78`) | `crm_get_conversation_history` (`conversations.ts:148`) é adjacente (histórico de mensagens para contexto) — não expõe campos de governança novos. > **Como ler `assignee_kind` (uso pelo Vendaval):** antes de responder, o agente > externo lê `assignee_kind`. `'user'` ⇒ **um humano já assumiu** — não intervir > (§6.2). `'ai'` ⇒ a IA é a dona. `null` ⇒ fila/sem dono. ### 3.6 Read tools de lead (campos de governança expostos — G6-03) **`crm_list_leads`** (`lib/mcp/tools/leads.ts:82`) e **`crm_get_lead`** (`leads.ts:124`). Categoria `read`, `role>=agent`, scope `mcp:read`. Enriquecimento aditivo (`enrichLeads`, `leads.ts:34-67`): | Campo | Fonte | Semântica | |---|---|---| | `owner_user_id` | `crm_leads.owner_user_id` | uuid do dono ou null | | `owner_user_name` | resolvido | **só o nome** do dono, sem email/telefone (LGPD, `leads.ts:62-64`) | | `stage` | join `crm_stages` | `{ id, name }` legível (além do `stage_id` cru, `leads.ts:65`) | | `stage_id`, `status`, `tags` | row (`select *`) | inalterados | ### 3.7 `assignee_kind` — semântica canônica Coluna `conversations.assignee_kind` (migration `0032_conversation_assignee_kind`, `supabase/migrations/20260717150000_0032_conversation_assignee_kind.sql:33-34`), com constraint de coerência (`0032:57-62`): - `'user'` ⇔ `assigned_to_user_id is not null` (humano atende); - `'ai'` ⇔ `assigned_to_user_id is null` (IA atende); - `null` (fila/sem dono). **Handoff = reassignment auditado.** IA→humano muda `assignee_kind` de `'ai'` para `'user'` com `reason='handoff'` na RPC `fn_conversation_assign` (§3.4). A conversa com `assignee_kind='user'` **silencia o bot nativo** deterministicamente (`workers/ai-response-worker.ts:287` → `skip("assigned_to_human")`), na mesma família de guard que `force_human` e `bot_silenced_until` (§6.2). --- ## 4. `visibility_mode` — o que o agente externo PODE ler `visibility_mode` vive em `organizations.settings` (jsonb), valores `'all'` | `'own_and_unassigned'` | `'own'`, **default `'own_and_unassigned'`** (migration `0035`, `supabase/migrations/20260717200000_0035_visibility_mode_conversation_rls.sql:1-3`). Restringe **apenas o role `agent`**; `viewer`/`manager`/`admin` veem toda a org. O agente externo lê via MCP **org-scoped e sujeito à RLS**. A visibilidade é imposta no banco, não na aplicação — o agente **não vê além do escopo**: - **Conversas:** policy `conversations_select` usa `fn_can_view_conversation(org, assigned_to_user_id)` (`0035:25-57`). Para `agent`: `'all'` = tudo da org; `'own_and_unassigned'` = suas + fila não-atribuída; `'own'` = só as suas (`0035:36-44`). Mensagens herdam (conversa oculta ⇒ mensagens ocultas, `0035:91-93`). - **Leads:** policy `crm_leads_select` usa `fn_can_view_lead(org, owner_user_id)` (migration `0036`, `supabase/migrations/20260717210000_0036_visibility_mode_lead_rls.sql:32-70`), mesma lógica sobre `owner_user_id`. - **Filhas de lead:** `crm_lead_activities` e `crm_lead_links` só são visíveis se o lead-pai é visível — SELECT via EXISTS na mesma `fn_can_view_lead` (migration `0042`, G6-00/INB-10, `supabase/migrations/20260718160000_0042_lead_children_visibility_rls.sql:41-56`). Fecha o vazamento de herança (activities/links de lead invisível). > **Regra para o Vendaval:** trate o que a tool retorna como o **teto** do que existe > para aquele token. Se uma conversa/lead não veio, para aquele escopo ela **não > existe** — não infira nem tente acessá-la por id direto (a RLS + o filtro > manual de org no handler barram, ex.: `governance.ts:72-74`, `leads.ts:143-146`). --- ## 5. `ai_dispatch_mode` — quem despacha os eventos de IA (G6-02) `organizations.settings.ai_dispatch_mode`, valores `'native'` | `'external'`, **default `'native'`**, com **fail-safe** `.catch("native")` para chave ausente/null/inválida (schema Zod `aiDispatchModeSchema`, `lib/schemas/settings.ts:22-24`). **Semântica** (dispatcher nativo, `lib/ai/dispatcher/index.ts`): - O dispatcher nativo resolve o modo de todas as orgs do batch numa query (`loadDispatchModes`, `index.ts:352-371`), aplicando o mesmo schema Zod (`index.ts:368`). - Org `'external'`: **o dispatcher nativo PULA o evento ANTES do claim** — `skipped_external_dispatch`, sem claim, **sem mudança de status**, `consumed_by` intacto (`index.ts:143-151`). O evento permanece `pending` no `event_log` para o Vendaval consumir. - Org `'native'` (ou default): processa como hoje (claim + dispatch, `index.ts:153-171`). > **Regra para o Vendaval:** nas orgs que ele atende, setar > `settings.ai_dispatch_mode='external'` e consumir os eventos > `ai_agent.dispatch_requested` **pending** do `event_log` (§7). Se esquecer de > setar, o dispatcher nativo processa o evento primeiro (default `'native'`) — > não há dupla-execução, mas o Vendaval não recebe o evento. --- ## 6. Proibições explícitas (o que o agente externo NUNCA pode fazer) ### 6.1 NUNCA cross-org Toda tool é **org-scoped pelo ctx** — `organization_id` vem do token, nunca do input (`governance.ts:9`). Além disso: - `crm_assign_conversation`: a conversa tem que ser da org (filtro manual, `governance.ts:72-74`) **e** o destino tem que ser membro ativo `agent+` da MESMA org — guard **INB-06a** dentro de `fn_conversation_assign` v2, que faz `RAISE` se o destino não é membro (migration `0032`, `supabase/migrations/20260717150000_0032_conversation_assignee_kind.sql:101-145`, comentário `0032:117`). O `membership`/role não é enumerável cross-org (a validação vive DENTRO da função, `0032:15-22`). - `crm_manage_tags`: filtro `organization_id` explícito no fetch e no update (`governance.ts:196-197`, `210-211`). - `crm_get_lead`: defesa em profundidade — 404 se a row não é da org (`leads.ts:143-146`). Um agente externo **nunca** atribui, lê ou etiqueta recurso de outra org. ### 6.2 NUNCA furar `is_blocked` / `force_human` / bot silenciado - **`contacts.is_blocked` guarda o SEND outbound.** `crm_send_whatsapp_message` (`messages.ts:38`) roteia por `sendMessageHandler`, que rejeita com **403 `forbidden` "Contato bloqueou o atendimento."** se o contato está bloqueado (`app/api/v1/messages/_handler.ts:151-159`). O agente externo **não pode enviar a contato bloqueado** — a guarda é server-side, não confie em checar no seu lado. (O STOP inbound seta `is_blocked=true` automaticamente — CLAUDE.md §WAHA.) - **`contacts.force_human`** e **`conversations.assignee_kind='user'`** e **`conversations.bot_silenced_until > now()`** são os guards de "não deixe o bot responder": o worker nativo pula nos três casos (`workers/ai-response-worker.ts:283-297`). Para o Vendaval, a regra equivalente é: se `assignee_kind='user'` (§3.5) um humano assumiu — **não intervenha**; e ele **não pode reativar** um bot silenciado (o `bot_silenced_until='infinity'` do handoff, `lib/ai/handoff/orchestrator.ts:96`, só o desfaz uma ação humana no CRM). ### 6.3 Outras invariantes que o código impõe - **Audit obrigatória em escrita.** Toda mutação de governança grava `api_audit_log` (fire-and-forget) com o ator carimbado (`governance.ts:130-139`, `214-224`). O agente externo não desliga isso. - **Idempotência sob replay/corrida** em `crm_assign_conversation` (§3.1): não conte com "aplicar duas vezes" — o contrato deduplica. - **Tags normalizadas e limitadas** (§3.2): não envie tags fora do formato — são rejeitadas, não truncadas silenciosamente. - **PII fora de payloads:** as read tools devolvem **só o nome** de atendente/dono, nunca email/telefone (`conversations.ts:74-76`, `leads.ts:62-64`) — o agente externo não deve tentar re-derivar PII. --- ## 7. Mudanças requeridas em consumidores (insumo do FG-01) O que o **Vendaval** precisa fazer do seu lado para consumir este contrato: 1. **Habilitar o modo externo por org.** Setar `organizations.settings.ai_dispatch_mode='external'` (§5) nas orgs que o Vendaval atende. Sem isso, o dispatcher nativo processa os eventos (default `'native'`). 2. **Consumir os eventos `pending` do `event_log`.** Para orgs `'external'`, o evento `ai_agent.dispatch_requested` permanece `pending` com `consumed_by` intacto (`lib/ai/dispatcher/index.ts:143-151`). O Vendaval faz o claim/consumo do seu lado (a mesma tabela `event_log`, filtrando `event_type` + `status='pending'`). Deve marcar o evento como consumido para não reprocessar. 3. **Autenticar org-scoped.** Um token MCP por org, com `role>=agent` e scopes `mcp:read`+`mcp:write`. `organization_id` NUNCA vai no input das tools — é o token que fixa a org. 4. **Ler `assignee_kind` antes de responder** (§3.5/§3.7): `'user'` ⇒ humano assumiu, não intervir; `'ai'`/`null` ⇒ pode agir. 5. **Usar `crm_request_human_handoff` v2 para o fallback humano** (§3.4) — deixar o roteamento G5 escolher o destino (passar `target_user_id` só como preferência); tratar os dois formatos de retorno (`assigned_to` OU `queued`+`position`). 6. **Respeitar visibilidade** (§4): tratar o retorno das read tools como o teto do que existe; não tentar acessar id fora do escopo. 7. **Respeitar as proibições §6:** não enviar a `is_blocked`; não intervir em conversa `assignee_kind='user'`; não reativar bot silenciado; nunca cross-org. 8. **Idempotência no envio:** passar `idempotency_key` em `crm_send_whatsapp_message` (TTL 24h, `messages.ts:26-31`) em retries. > **Fora do escopo deste contrato (decisões do FG do Vendaval):** o modelo de claim > do Vendaval sobre o `event_log` (visibilidade/lock do consumo externo) e o > mapeamento de credenciais org→token. Este contrato define **a superfície do > DeskcommCRM**; o lado do Vendaval é o FG-01. --- ## Apêndice A — Refs verificadas em 2026-07-18 @ `ddcc511` | Ref | Confere | |---|---| | `lib/mcp/tools/catalog.ts:19-39` | `TOOL_CATALOG` (16 entradas) | | `lib/mcp/tools/index.ts:62-75` | sanity handler↔catalog 1:1 | | `lib/mcp/tools/governance.ts:9` | org do ctx, nunca do input | | `lib/mcp/tools/governance.ts:40-52` | input `crm_assign_conversation` + cross-field | | `lib/mcp/tools/governance.ts:83-127` | idempotência replay + corrida | | `lib/mcp/tools/governance.ts:167-205,226` | input/validações/output `crm_manage_tags` | | `lib/mcp/tools/governance.ts:234-248` | `crm_get_queue_status` | | `lib/routing/queue.ts:21-57` | shape `QueueStatus` + `getQueueStatus` | | `lib/mcp/tools/handoff.ts:29-176` | input/semântica/output `crm_request_human_handoff` v2 | | `lib/mcp/tools/conversations.ts:66-83,117-138` | campos de governança expostos | | `lib/mcp/tools/leads.ts:34-67` | `enrichLeads` (owner_user_name, stage) | | `supabase/migrations/…0032…:33-62,101-145` | `assignee_kind` + guard INB-06a | | `supabase/migrations/…0035…:1-57` | `visibility_mode` + `fn_can_view_conversation` | | `supabase/migrations/…0036…:32-70` | `fn_can_view_lead` | | `supabase/migrations/…0042…:41-56` | filhas de lead visibility (G6-00) | | `lib/schemas/settings.ts:22-24` | `aiDispatchModeSchema` + `.catch("native")` | | `lib/ai/dispatcher/index.ts:143-151,352-371` | skip antes do claim (org `external`) | | `app/api/v1/messages/_handler.ts:151-159` | guard `is_blocked` no SEND | | `workers/ai-response-worker.ts:283-297` | guards `is_blocked`/`force_human`/`assignee_kind`/`bot_silenced_until` | | `lib/ai/handoff/orchestrator.ts:96` | `bot_silenced_until='infinity'` no handoff |