@fazer-ai/agents 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (83) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +112 -0
  3. package/dist/agents/claude.js +152 -0
  4. package/dist/agents/codex.js +155 -0
  5. package/dist/agents/detect.js +15 -0
  6. package/dist/agents/handoff.js +22 -0
  7. package/dist/agents/hermes-skills.js +177 -0
  8. package/dist/agents/hermes.js +474 -0
  9. package/dist/agents/index.js +57 -0
  10. package/dist/agents/manual.js +22 -0
  11. package/dist/agents/other.js +39 -0
  12. package/dist/agents/shell.js +15 -0
  13. package/dist/agents/types.js +2 -0
  14. package/dist/config.js +48 -0
  15. package/dist/exec.js +279 -0
  16. package/dist/hostinger.js +75 -0
  17. package/dist/hub-command.js +144 -0
  18. package/dist/index.js +726 -0
  19. package/dist/licenses.js +93 -0
  20. package/dist/mcp.js +100 -0
  21. package/dist/oauth.js +578 -0
  22. package/dist/onboarding-marker.js +48 -0
  23. package/dist/preferences.js +40 -0
  24. package/dist/skills/agents-dev/SKILL.md +37 -0
  25. package/dist/skills/agents-dev/gotchas.md +6 -0
  26. package/dist/skills/agents-dev/guardrails.md +6 -0
  27. package/dist/skills/agents-dev/references/00-get-the-code.md +28 -0
  28. package/dist/skills/agents-dev/references/01-layout-and-bun-check.md +29 -0
  29. package/dist/skills/agents-dev/references/02-free-full-and-invariants.md +7 -0
  30. package/dist/skills/agents-dev/references/03-implement.md +9 -0
  31. package/dist/skills/agents-dev/references/04-own-image-and-deploy.md +13 -0
  32. package/dist/skills/agents-onboarding/SKILL.md +80 -0
  33. package/dist/skills/agents-onboarding/gotchas.md +157 -0
  34. package/dist/skills/agents-onboarding/guardrails.md +65 -0
  35. package/dist/skills/agents-onboarding/references/00-prereqs-and-access.md +37 -0
  36. package/dist/skills/agents-onboarding/references/01-vps-dns-ssh.md +67 -0
  37. package/dist/skills/agents-onboarding/references/01b-brownfield.md +70 -0
  38. package/dist/skills/agents-onboarding/references/01c-pick-tier.md +61 -0
  39. package/dist/skills/agents-onboarding/references/02-coolify.md +109 -0
  40. package/dist/skills/agents-onboarding/references/03-chatwoot-pro.md +61 -0
  41. package/dist/skills/agents-onboarding/references/04-agents-image.md +46 -0
  42. package/dist/skills/agents-onboarding/references/05-langfuse.md +45 -0
  43. package/dist/skills/agents-onboarding/references/06-setup-and-mcp.md +47 -0
  44. package/dist/skills/agents-onboarding/references/08-agent-import.md +55 -0
  45. package/dist/skills/agents-onboarding/references/09-chatwoot-bind.md +41 -0
  46. package/dist/skills/agents-onboarding/references/10-validate-e2e.md +34 -0
  47. package/dist/skills/agents-onboarding/references/agent-features.md +61 -0
  48. package/dist/skills/agents-onboarding/references/chatwoot-hub-register.md +69 -0
  49. package/dist/skills/agents-onboarding/references/deploy-b-portainer.md +138 -0
  50. package/dist/skills/agents-onboarding/references/deploy-c-compose.md +64 -0
  51. package/dist/skills/agents-onboarding/samples/agents/README.md +23 -0
  52. package/dist/skills/agents-onboarding/samples/agents/maria-clinica-moreira.json +313 -0
  53. package/dist/skills/agents-onboarding/scripts/chatwoot-admin.py +248 -0
  54. package/dist/skills/agents-onboarding/scripts/coolify.py +552 -0
  55. package/dist/skills/agents-onboarding/scripts/docker-status.py +129 -0
  56. package/dist/skills/agents-onboarding/scripts/gen-onboarding-env.ts +187 -0
  57. package/dist/skills/agents-onboarding/scripts/harbor-login.py +118 -0
  58. package/dist/skills/agents-onboarding/scripts/langfuse-verify.py +118 -0
  59. package/dist/skills/agents-onboarding/scripts/portainer-brownfield.py +115 -0
  60. package/dist/skills/agents-onboarding/scripts/remote.py +198 -0
  61. package/dist/skills/agents-onboarding/scripts/sshkey.py +140 -0
  62. package/dist/skills/agents-onboarding/templates/chatwoot/.env.example +30 -0
  63. package/dist/skills/agents-onboarding/templates/chatwoot/README.md +65 -0
  64. package/dist/skills/agents-onboarding/templates/chatwoot/docker-compose.coolify.yml +136 -0
  65. package/dist/skills/agents-onboarding/templates/chatwoot/docker-compose.yml +139 -0
  66. package/dist/skills/agents-onboarding/templates/docker-compose.coolify.yml +73 -0
  67. package/dist/skills/agents-onboarding/templates/docker-compose.portainer.yml +132 -0
  68. package/dist/skills/agents-onboarding/templates/docker-compose.prod.yml +85 -0
  69. package/dist/skills/agents-onboarding/templates/langfuse/.env.example +59 -0
  70. package/dist/skills/agents-onboarding/templates/langfuse/README.md +132 -0
  71. package/dist/skills/agents-onboarding/templates/langfuse/docker-compose.coolify.yml +189 -0
  72. package/dist/skills/agents-onboarding/templates/langfuse/docker-compose.yml +185 -0
  73. package/dist/skills/agents-operation/SKILL.md +42 -0
  74. package/dist/skills/agents-operation/gotchas.md +61 -0
  75. package/dist/skills/agents-operation/guardrails.md +26 -0
  76. package/dist/skills/agents-operation/references/00-production-safety.md +24 -0
  77. package/dist/skills/agents-operation/references/01-diagnose.md +34 -0
  78. package/dist/skills/agents-operation/references/02-reproduce.md +22 -0
  79. package/dist/skills/agents-operation/references/03-adjust.md +36 -0
  80. package/dist/skills/agents-operation/references/04-validate-and-apply.md +31 -0
  81. package/dist/ui-select.js +279 -0
  82. package/dist/ui.js +167 -0
  83. package/package.json +53 -0
@@ -0,0 +1,70 @@
1
+ # 01b: Inventário brownfield (sondar antes de instalar)
2
+
3
+ A VPS pode chegar vazia (**greenfield**) ou já com Coolify e/ou Chatwoot e/ou Langfuse e/ou a própria agents, em **qualquer combinação** (**brownfield**). Antes de instalar qualquer coisa (etapas 2 a 5), **sonde** o que já existe e decida **por serviço**: reusar, instalar, ou sinalizar incompatibilidade. É isso que torna as etapas de deploy **idempotentes** (só provisionam o que falta) e **não-destrutivas** (nunca apagam o que o usuário já tem).
4
+
5
+ ## 1. Sondagem (read-only, não muta nada)
6
+
7
+ Rode na VPS via SSH (etapa 0). Tudo aqui é leitura (`docker ps/inspect`, `ss`, `curl`, `df/free`). O probe tem `{{…}}`, `$()`, aspas aninhadas e múltiplas linhas: **não monte isso inline no `ssh <host> '…'`** (no PowerShell as aspas são comidas e a here-string ganha BOM → o bash quebra; ver `gotchas.md`). **Escreva o probe num arquivo `recon.sh`** e rode pelo `scripts/remote.py`, que entrega byte a byte em qualquer SO:
8
+
9
+ `recon.sh`:
10
+ ```sh
11
+ sec(){ printf '\n### %s\n' "$1"; }
12
+ sec OS; ( . /etc/os-release && echo "$PRETTY_NAME" )
13
+ sec RESOURCES; free -h | awk 'NR==2{print "mem "$2"/"$7" avail"}'; df -h / | awk 'NR==2{print "disk "$2"/"$4" free"}'; echo "cpu $(nproc)"
14
+ sec DOCKER; docker --version || echo absent
15
+ sec CONTAINERS; docker ps -a --format '{{.Names}} {{.Image}} {{.Status}} [{{.Label "com.docker.compose.project"}}]'
16
+ sec PORTS; ss -tlnp | awk 'NR>1{n=split($4,a,":");print a[n]}' | sort -un | tr '\n' ' '; echo
17
+ sec COOLIFY; curl -s -m5 -o /dev/null -w 'api8000=%{http_code}\n' http://localhost:8000/api/health
18
+ sec IMAGES; docker ps -a --format '{{.Image}}' | sort -u | grep -iE 'coolify|chatwoot|langfuse|agents|pgvector|clickhouse|minio|traefik|caddy|nginx'
19
+ ```
20
+
21
+ ```sh
22
+ python3 scripts/remote.py --ssh root@<VPS_IP> --ssh-opts "-i <chave>" --script-file recon.sh
23
+ ```
24
+
25
+ > **Tier B (Portainer):** quando a plataforma é Portainer, a sondagem é **via API do Portainer** (`GET /api/stacks`, `GET /api/endpoints/{id}/docker/containers/json`), não `coolify-db`. A lógica é a mesma (fingerprint por imagem + matriz da seção 3); use `scripts/portainer-brownfield.py` (já detecta quem ocupa 80/443 → se há ingress, o Caddy bundled conflita, reusar ou ir de `templates/docker-compose.prod.yml` BYO-proxy). Ver [`deploy-b-portainer.md`](deploy-b-portainer.md).
26
+
27
+ ## 2. Ler os sinais
28
+
29
+ **Identifique o serviço pela IMAGEM, não pelo nome do projeto** (é um UUID opaco). Fingerprints:
30
+
31
+ | Serviço | Imagem (fingerprint) | Saúde = todos healthy | Versão |
32
+ |---|---|---|---|
33
+ | **Coolify** | `coollabsio/coolify` (+ `coolify-db`/`-redis`/`-realtime`, `-proxy`=`traefik`) | container `coolify` + API `:8000`=200 | tag da imagem (ex. `:4.1.2`) |
34
+ | **Chatwoot** | imagem com `chatwoot` (+ `sidekiq`, e `baileys-api` pro WhatsApp) | `chatwoot` + `sidekiq` Up | tag (`:latest` → ver via `/version`) |
35
+ | **Langfuse** | `langfuse/langfuse` (+ `-worker`, `clickhouse`, **`minio`**) | web+worker+clickhouse+minio Up | tag (ex. `:3`) |
36
+ | **fazer.ai agents** | `ghcr.io/fazer-ai/agents` (+ `pgvector`) | container Up + `/api/health` | tag |
37
+
38
+ As portas das apps **não** ficam expostas no host (atrás do Traefik); só Coolify (`:8000`) e o proxy (`:80`/`:443`) escutam. `curl localhost:80` sem o Host certo dá 404/503 (esperado). Pra health de uma app, use o FQDN dela.
39
+
40
+ ## 3. Matriz de decisão (por serviço)
41
+
42
+ - **Ausente** (nenhum container com o fingerprint) → **instala** do zero (etapa do serviço).
43
+ - **Presente + saudável + compatível** → **reaproveita**: capture endpoint/UUID/FQDN pro state, NÃO recrie (a etapa do serviço vira no-op + captura).
44
+ - **Presente + não-saudável** (container existe mas não Up/healthy) → **pare e sinalize**: investigar/consertar antes de prosseguir; nunca instalar por cima.
45
+ - **Presente + incompatível** → **pare e sinalize ao usuário** (atualizar / migrar / instalar em paralelo, decisão dele). Ver compatibilidade abaixo.
46
+
47
+ Greenfield = tudo ausente = instala tudo. O resultado é um inventário por serviço (`ausente | reusar | sinalizar`) que dirige as etapas 2 a 5.
48
+
49
+ ## 4. Compatibilidade (o que torna "presente" em "incompatível")
50
+
51
+ - **Chatwoot OSS vs Pro:** a imagem revela. `harbor.fazer.ai/chatwoot/fazer-ai/chatwoot-pro` = **Pro** (Kanban + features fazer-ai). `ghcr.io/fazer-ai/chatwoot` (nosso fork OSS), ou o `chatwoot/chatwoot` oficial do Docker Hub num brownfield de terceiro, = **OSS**: o core do agente funciona (Agent Bot é padrão), mas **sem** Kanban/features Pro. Se o usuário quer essas features, sinalize a migração pra Pro.
52
+ - **Langfuse v3 vs v2:** o fazer.ai agents fala com a v3 (arquitetura `clickhouse` + **`minio` obrigatório**, ver `references/05-langfuse.md`). Tag `:2`, ou ausência de `clickhouse`/`minio`, → incompatível/parcial: sinalize.
53
+ - **Coolify:** validado em `4.x`. Versões muito antigas têm API diferente; confirme `:8000/api/health`=200 e cheque a versão pela tag.
54
+ - **Postgres reusado (fora do Coolify, Tier B/C):** o fazer.ai agents exige **pgvector** (extensão `vector`) e um **superuser** pro bootstrap das 2 roles (ver `references/04-agents-image.md`). Um Postgres compartilhado sem pgvector ou sem acesso superuser → sinalize.
55
+
56
+ ## 5. Reaproveitar (capturar pro state, sem recriar)
57
+
58
+ Pra um serviço que vai reusar, capture o que as etapas seguintes precisam:
59
+ - **No Coolify, do container ao FQDN:** cruze o label `com.docker.compose.project` (= `uuid` do serviço) com o `coolify-db` pra pegar o endpoint público (sub-componentes como `sidekiq`/`minio`/`clickhouse` têm `fqdn` vazio):
60
+
61
+ ```sh
62
+ docker exec -i coolify-db psql -U coolify -d coolify -c \
63
+ "SELECT s.uuid, s.name, sa.fqdn FROM services s
64
+ JOIN service_applications sa ON sa.service_id=s.id
65
+ WHERE sa.fqdn IS NOT NULL AND sa.fqdn<>'' ORDER BY s.id;"
66
+ ```
67
+
68
+ Ou via API: `GET /api/v1/services` (etapa 2). Preserve a porta do FQDN quando houver (ex. Langfuse `:3000`).
69
+ - **Endpoints/creds:** FQDN público + credenciais já existentes (admin token do Chatwoot via Rails runner; chaves do Langfuse) buscadas **transitoriamente** (ver `guardrails.md`), nunca persistidas.
70
+ - **Nunca** recrie um serviço saudável só pra "padronizar": isso destrói dados do usuário. Em brownfield, reusar > reinstalar.
@@ -0,0 +1,61 @@
1
+ # 01c: Selecionar o tier de deploy + o contrato
2
+
3
+ Depois do inventário brownfield (1b) você sabe **o que já existe** na VPS. Agora escolha **a trilha de
4
+ deploy** (qual orquestrador sobe Chatwoot + fazer.ai agents + Langfuse com TLS) e siga só ela. As etapas 6 a 10 (a
5
+ **espinha**: `/setup` → MCP → import → bind → E2E) são **idênticas em qualquer tier**: elas
6
+ consomem o *resultado* do deploy, não se importam com *como* ele foi feito.
7
+
8
+ ## Qual tier
9
+
10
+ O probe do 1b já revela o orquestrador instalado (fingerprint por imagem). Mapeie:
11
+
12
+ | Sinal na VPS (1b) | Tier | Trilha de deploy |
13
+ |---|---|---|
14
+ | **Coolify** saudável (ou usuário quer Coolify) | **A** | etapas 2-5 ([`02-coolify.md`](02-coolify.md) → `03` → `04` → `05`) |
15
+ | **Portainer** (ou usuário quer Portainer) | **B** | [`deploy-b-portainer.md`](deploy-b-portainer.md) |
16
+ | **Tudo o mais**: VM crua, ou um painel sem trilha dedicada (Easypanel, Dokploy, CapRover, …) | **C** | [`deploy-c-compose.md`](deploy-c-compose.md) |
17
+
18
+ Coolify ou Portainer presentes e saudáveis ganham (reusar > instalar; o 1b decide por serviço); **qualquer
19
+ outro painel cai no Tier C** (compose genérico, adaptado ao painel com seu conhecimento dele). Nenhum sinal
20
+ claro → **pergunte ao usuário**, não adivinhe.
21
+
22
+ > A trilha C (compose genérico) é mais nova que A/B; trate-a como primeira run guiada.
23
+
24
+ ## O contrato de deploy (o que a trilha entrega à espinha)
25
+
26
+ Qualquer que seja o tier, o segmento de deploy termina quando **entrega exatamente isto**. A espinha
27
+ (6-10) não pede mais nada e não olha pra dentro do orquestrador:
28
+
29
+ 1. **Três URLs HTTPS públicas**, com **DNS resolvido + TLS válido** (Let's Encrypt):
30
+ - `agentes.<domínio>` → agents (`GET /api/health` = 200).
31
+ - `chatwoot.<domínio>` → Chatwoot (Pro ou OSS).
32
+ - `langfuse.<domínio>` → Langfuse (recomendado; o E2E valida traces).
33
+ 2. **agents subida com as duas roles de DB** (superuser p/ bootstrap+migrate, runtime **não-superuser**) e o
34
+ env mínimo: `PUBLIC_URL`, `JWT_SECRET`, `ENCRYPTION_KEY`, os dois pares de URL
35
+ (`MIGRATION_DATABASE_URL` = superuser; `DATABASE_URL` + `LANGGRAPH_DATABASE_URL` = runtime),
36
+ `BRANDING_STORAGE_DIR`/`QUOTES_STORAGE_DIR` no volume persistente, **réplica única**. O
37
+ `scripts/gen-onboarding-env.ts` gera os secrets + URLs; os composes
38
+ do repo já trazem o resto.
39
+ 3. **agents acessível + token do `/setup`** legível nos logs de boot (`${PUBLIC_URL}/setup?token=…`).
40
+ 4. **admin token do Chatwoot** obtível (via Rails runner) pro `deployment_connect`/bind da etapa 9.
41
+ 5. **Langfuse com MinIO** (a ingestion v3 exige blob storage) + as chaves (public/secret) obtíveis.
42
+
43
+ Entregou os 5 → vá direto pra **etapa 6** (a mesma pra todos os tiers).
44
+
45
+ > **Chatwoot existente (`chatwootSource: existing` no marcador):** só **fazer.ai agents + Langfuse** a provisionar: `chatwoot.<domínio>` é a instância que **já está no ar** (não a crie nem lhe mexa; o item 1 e o deploy do Chatwoot da trilha do tier são pulados). O admin token (item 4) vem via **Rails runner** se a instância é on-box/alcançável por SSH; se for **off-box** (Chatwoot Cloud / outro host), o **usuário fornece** um admin API token (Chatwoot → Profile → Access Token). O bind (etapa 9) usa a URL pública + esse token.
46
+
47
+ ## Invariantes (valem em todos os tiers)
48
+
49
+ - **`pgvector/pgvector:pg17`**, nunca Postgres puro: o schema roda `CREATE EXTENSION vector`.
50
+ - **Réplica única** do fazer.ai agents: os workers (scheduler/debounce/outbound) assumem um único líder; não escale o
51
+ serviço `agents` pra >1 (ver o aviso no `templates/docker-compose.prod.yml`).
52
+ - **DNS antes do ACME**: o cert só emite com o A-record já resolvendo pro IP da VPS. Crie os A-records
53
+ (etapa 1) e confirme a resolução **antes** de anexar o domínio no painel / subir o Caddy.
54
+ - **Quem ocupa 80/443**: se já há um proxy/ingress (Traefik do painel, nginx, um Caddy), o Caddy
55
+ *bundled* do `templates/docker-compose.portainer.yml` **conflita**:
56
+ reuse o proxy existente com `templates/docker-compose.prod.yml` (BYO-proxy).
57
+ O 1b já sinaliza quem detém as portas.
58
+ - **Não sobrescreva `command:`** no serviço do fazer.ai agents: o CMD da imagem faz `bootstrap → migrate deploy →
59
+ serve`. Um `command:` próprio quebra o boot.
60
+ - **agents → Chatwoot pela URL pública**: o `deployment_connect` funciona contra a URL **pública** do Chatwoot,
61
+ sem gambiarra de rede interna (detalhe na gotcha de [`deploy-b-portainer.md`](deploy-b-portainer.md)).
@@ -0,0 +1,109 @@
1
+ # 02: Coolify (reusar/instalar, API, Instance Domain)
2
+
3
+ > **Avise o usuário onde você está:** o Coolify é o **1º serviço** do deploy (o painel que gerencia tudo; depois vêm Chatwoot, fazer.ai agents e Langfuse). Diga que vai começar por ele, e a instalação leva alguns minutos. Dê sinal de vida durante a espera longa (Docker + imagens baixando) em vez de sumir; ao terminar, confirme e anuncie o próximo. Ver o princípio de narração (e a contagem que se ajusta ao caso real) em `SKILL.md`.
4
+
5
+ ## Brownfield: reusar se já existe e está saudável
6
+
7
+ A VPS pode já vir com Coolify (brownfield). Nesse caso, reaproveite o existente: **nunca** destrua dados do usuário. O inventário brownfield completo (todos os serviços, com a matriz reusar/instalar/sinalizar) está na etapa 1b (`references/01b-brownfield.md`); aqui é só a parte do Coolify.
8
+
9
+ ### Greenfield: instalar o Coolify se a VPS vier sem ele
10
+
11
+ VPS nova sem Coolify → rode o instalador oficial. **Como você invoca importa mais que o instalador** (é o passo que mais trava): ele puxa Docker + imagens (minutos), lê o próprio stdin e faz job control. Dois anti-padrões fazem ele sair com exit 1/127 fingindo estar "interativo (Y/N)" (e aí um modelo fraco entra num loop de tweaks):
12
+
13
+ - **Não** jogue a saída do `curl` direto num shell (baixar-e-canalizar): o `bash` passa a ler o SCRIPT do stdin e colide com os prompts do instalador.
14
+ - **Não** o passe pelo stdin do `remote.py`/`bash -s` (que já usa o stdin pro próprio script): o instalador herda um stdin ocupado/EOF.
15
+
16
+ Em vez disso, **baixe o instalador pra um arquivo no remoto e rode o arquivo com stdin limpo (`< /dev/null`), detached**, depois faça poll (não bloqueie o terminal por minutos, a chamada pode ser cortada). O `remote.py --script-file` roda este wrapper, que faz o download+run NO remoto (sem canalizar):
17
+
18
+ `install-coolify.sh`:
19
+ ```sh
20
+ curl -fsSL https://cdn.coollabs.io/coolify/install.sh -o /tmp/coolify-install.sh
21
+ setsid bash /tmp/coolify-install.sh < /dev/null > /tmp/coolify-install.log 2>&1 &
22
+ echo "coolify install iniciado (pid $!); log em /tmp/coolify-install.log"
23
+ ```
24
+
25
+ Depois faça **poll não-bloqueante** até ficar pronto: `curl -s -o /dev/null -w '%{http_code}' http://localhost:8000/api/health` = `200` e os containers core `Up (healthy)`. Resultado esperado: `/data/coolify` criado, "Your instance is ready to use!" no log, **6 containers core** Up+healthy (`coolify`, `coolify-db`, `coolify-redis`, `coolify-realtime`, `coolify-proxy`, `coolify-sentinel`). Em brownfield (Coolify já presente e saudável) **reaproveite** (etapa 1b); nunca reinstale por cima de dados do usuário.
26
+
27
+ ## Verifique que o Coolify alcança o próprio servidor (localhost): auto-repare a chave
28
+
29
+ Assim que o Coolify está de pé (greenfield recém-instalado OU brownfield reaproveitado), **confirme que ele consegue se conectar ao próprio host** antes de qualquer deploy. O Coolify gerencia o servidor `localhost` por SSH, do container `coolify` para o host (`root@host.docker.internal`), com uma chave que o instalador adiciona às **chaves autorizadas do root**. É uma falha silenciosa e recorrente: se a última linha desse arquivo **não terminava em newline** (uma chave colada pelo painel da VPS costuma chegar assim), o `cat >>` do instalador **cola** a chave do Coolify no fim da linha anterior: ela deixa de ser uma entrada válida, o servidor `localhost` fica **Unreachable** e **todo deploy falha depois** (sem erro óbvio; a UI só mostra o servidor inacessível).
30
+
31
+ Não conserte na mão (editar as chaves autorizadas por SSH→PowerShell é a classe de quoting que mais quebra). Rode o helper: ele normaliza o arquivo (uma chave por linha, sem linhas grudadas, newline no fim, permissões), garante a chave do Coolify como linha própria, e **verifica** o SSH container→host:
32
+ ```sh
33
+ python3 scripts/coolify.py heal-localhost --ssh root@<VPS_IP>
34
+ ```
35
+ `reachable:true` → o Coolify alcança o próprio host; siga. **Idempotente**: rode sempre (greenfield ou brownfield); se já estava são, não muda nada e confirma `reachable:true`. O flag interno do Coolify (`is_reachable`) fica em cache e **não** revalida sozinho de forma confiável (o job periódico costuma ser pulado por lock); ele só reflete o conserto após um `docker restart coolify` (que o passo do **Instance Domain** já faz antes dos deploys), então um "Unreachable" na UI até lá é cosmético. Se **não** for setar o Instance Domain antes do 1º deploy, rode `docker restart coolify` depois do heal pra revalidar. `reachable:false` → confira que o container `coolify` está `Up` e que o host aceita login root por chave, e re-rode.
36
+
37
+ ## 1º admin (o ÚNICO passo do usuário no Tier A)
38
+
39
+ O **usuário cria** o 1º admin pelo browser em `http://<VPS_IP>:8000` (gate de conta): **esse é o único passo manual do Tier A**; você entrega o link e **espera** (`wait-admin`), nunca cria por conta própria. Depois do admin, **NÃO peça mais nada ao usuário**: o token e o Instance Domain você faz por SSH (abaixo). **Nunca** mande o usuário abrir "Settings → …".
40
+
41
+ Em vez de pedir "responda quando criar", **aguarde** o admin aparecer (poll no banco via psql, não trava o operador). **Rode em background, não em foreground**: o poll bloqueia por minutos e não há nada a fazer no meio, então dispare non-blocking e retome quando o comando sair (nunca deixe o poll travar o seu turno). Como o gate depende do usuário criar a conta no browser, dê uma janela larga (`--attempts 120`, ~10 min):
42
+ ```sh
43
+ python3 scripts/coolify.py wait-admin --ssh root@<VPS_IP> --attempts 120 # em background, non-blocking
44
+ ```
45
+ `ok:true` (com `users>0`) → siga pro token. `ok:false` (timeout) → re-lance ou pergunte ao operador. **Não avance pro token antes do `ok:true`.** Brownfield: se já existe admin, detecta na 1ª tentativa e segue. (O detector é psql, não Tinker: não dá o falso "sem admin" que o Tinker dava ao ecoar o payload.)
46
+
47
+ ## API Access (token): você faz por SSH, não pela UI
48
+
49
+ Dois passos, ambos por SSH, **sem o usuário**. Os dois (e toda chamada à API daqui pra frente) saem do `scripts/coolify.py` (Python stdlib, embutido nesta skill): ele base64-pipa o payload por SSH, semeia o `currentTeam`, e **mantém o token Sanctum `<id>|<token>` fora de qualquer shell** (o `|` só vive num arquivo `0600` e no header HTTP). Foi um `|` num comando montado à mão que já derrubou uma run. Rode via Bash com sandbox desligado, como todo ssh/curl (ver `00`).
50
+
51
+ **1. Habilite a API**: vem **desabilitada** por padrão; sem isto todo request dá `403 {"message":"API is disabled."}`:
52
+ ```sh
53
+ python3 scripts/coolify.py enable-api --ssh root@<VPS_IP>
54
+ ```
55
+ Pega na hora, sem restart, idempotente. `allowed_ips` vazio (default) = sem restrição de origem; **não mexa** (o agente acessa de fora).
56
+
57
+ **2. Gere o token root.** O `createToken` cru falha com `team_id null`, então o script **semeia a sessão** (`currentTeam`) antes de gerar, extrai o `<id>|<token>` e grava num arquivo `0600` (ability `*` = root; o segredo **não** é impresso):
58
+ ```sh
59
+ python3 scripts/coolify.py token --ssh root@<VPS_IP> --out coolify.token # arquivo no scratchpad, transitório
60
+ ```
61
+ Daí toda chamada autenticada lê o token do arquivo, você **nunca** digita o token num `curl`. Valide a API:
62
+ ```sh
63
+ python3 scripts/coolify.py api-get --base-url http://<VPS_IP>:8000 --token-file coolify.token --path /servers # → 200
64
+ ```
65
+ `create-service` (deploy de serviço), `api-post` (qualquer POST autenticado) e `set-fqdn` usam o mesmo `--token-file`. O arquivo é transitório (scratchpad); **nunca** em repo/log/commit. **Windows/PowerShell:** no `api-post`, prefira `--json-file` a `--json-stdin`: o pipe do PowerShell manda o stdin em UTF-16 e quebra o JSON (o helper tolera BOM/UTF-16 como rede de segurança, mas `--json-file` é determinístico).
66
+
67
+ ## Instance Domain: você seta por SSH (faça ANTES de deployar os serviços)
68
+
69
+ Troca o acesso ao **painel** de `http://<VPS_IP>:8000` para `https://coolify.<seu-dominio>` (TLS). O deploy dos serviços não depende disto (rodam pelo IP cru), mas **faça**: é parte do contrato do painel, não item descartável. Duas regras de ordem/confiabilidade que já queimaram runs:
70
+
71
+ 1. **Faça ANTES de deployar os serviços.** O passo termina com `docker restart coolify`, que **zera a fila de deploy** do Coolify (`gotchas.md`: "o `start` da API pode não materializar"). Reiniciar o `coolify` no meio do deploy faz os serviços não subirem. Ordem: token → **Instance Domain** → deploy dos serviços.
72
+ 2. **NÃO monte o psql + restart inline no `ssh <host> '…'`**: o `UPDATE …` com aspas e o `; docker restart` quebram no PowerShell (aspas comidas, `\`/BOM; é exatamente onde uma run real falhou e o agente desistiu chamando de "cosmético"). Escreva um `.sh` e rode pelo `remote.py` (entrega byte-exato):
73
+
74
+ `set-instance-domain.sh`:
75
+ ```sh
76
+ docker exec -i coolify-db psql -U coolify -d coolify -c "UPDATE instance_settings SET fqdn='https://coolify.<seu-dominio>';"
77
+ docker restart coolify
78
+ ```
79
+ ```sh
80
+ python3 scripts/remote.py --ssh root@<VPS_IP> --ssh-opts "-i <chave>" --script-file set-instance-domain.sh
81
+ ```
82
+ O `UPDATE` **sozinho não regenera** o proxy; é o **`restart coolify`** (o app, **NÃO** `coolify-proxy`) que reescreve a rota do painel. Derruba painel+API ~30-40s (o token já gerado **sobrevive**). Exige o A-record `coolify.` (etapa 1). Depois **valide**:
83
+ ```sh
84
+ curl -so /dev/null -w "%{http_code} ssl=%{ssl_verify_result}\n" https://coolify.<seu-dominio>
85
+ ```
86
+ → `200`/`302` com `ssl=0` (cert válido; o ACME emite quando o A-record resolve). Só siga sem o domínio se ele **falhar de verdade** após este caminho (não como atalho).
87
+
88
+ ## DB do Coolify (acesso direto; ver gotchas)
89
+
90
+ ```sh
91
+ docker exec -i coolify-db psql -U coolify -d coolify
92
+ ```
93
+
94
+ ## Projeto + ambiente
95
+
96
+ Crie (ou reaproveite) um projeto com o **nome de exibição do usuário** (ex.: `clinica-moreira`) e o ambiente `production`. Os UUIDs (server/projeto/env/serviços) são **gerados a cada instalação**: descubra-os pela API/DB; nunca chumbe UUIDs de outra instalação.
97
+
98
+ ## Registry privado do Harbor (só Pro)
99
+
100
+ Imagens **Pro** (Chatwoot `chatwoot-pro`; fazer.ai agents no projeto `agents`) são privadas no Harbor: o Coolify precisa da credencial registrada **antes** de puxar, senão o deploy falha (pull denied / 401). Só no caminho Pro:
101
+
102
+ 1. Provisione a credencial **per-user** pelo **proxy do hub no CLI** (não há hub MCP na sessão; o CLI tem o OAuth do bootstrap):
103
+ ```sh
104
+ bunx @fazer-ai/agents hub registry-credential --apply --out harbor.secret
105
+ ```
106
+ Grava o secret em `harbor.secret` (`0600`) e imprime só `username` + caminho (o secret **nunca** sai no output). Idempotente (garante o robot per-user, sem rotação).
107
+ 2. Registre no Coolify (Servers → Registries, ou via API) apontando pra `harbor.fazer.ai` com o `username` (do passo 1) e o secret de `harbor.secret`. **Nunca** logue o secret.
108
+
109
+ No caminho **OSS** (imagem pública), pule isto inteiro.
@@ -0,0 +1,61 @@
1
+ # 03: Deploy do Chatwoot (Pro ou OSS)
2
+
3
+ ## Antes de tudo: `chatwootSource` (novo vs. existente/BYO)
4
+
5
+ Leia `~/.fazer-ai/onboarding.json` → `chatwootSource`. Se **`existing`** (Chatwoot BYO), **PULE este doc inteiro**: não há Chatwoot a provisionar, `chatwoot.<seu-dominio>` é a instância que **já está no ar** (não a crie nem lhe mexa). Detecte Pro/OSS pela imagem (etapa 1b), vá direto ao **bind (etapa 9)** e trate a **etapa 9b (licenciar)** como opcional (só se for um Pro sem Kanban e o usuário quiser). Todo o resto abaixo é só pra **`new`**.
6
+
7
+ > **Avise o usuário onde você está** (quando o Chatwoot é `new`): o Chatwoot é o **2º serviço** do deploy (a plataforma de atendimento onde as conversas acontecem; vem depois do painel, antes do fazer.ai agents e do Langfuse). Diga que vai subir o Chatwoot agora e que leva alguns minutos; dê sinal de vida durante a espera longa (imagem baixando) e confirme ao terminar, anunciando o próximo. Ver o princípio de narração em `SKILL.md`.
8
+
9
+ ## Primeiro (source `new`): leia o marcador e ramifique (Pro vs OSS)
10
+
11
+ Leia `~/.fazer-ai/onboarding.json` → `chatwootTier`. Eixo **independente** da edição do fazer.ai agents (`edition`, etapa 4). Marcador ausente → fallback pelo hub (`bunx @fazer-ai/agents hub licenses`): licença CHATWOOT disponível → Pro; senão OSS.
12
+
13
+ - **`community` (OSS)** → imagem **pública** `ghcr.io/fazer-ai/chatwoot:latest` (nosso fork), `COMPOSE_PROFILES` vazio (sem `baileys-api`). **NÃO** rode `docker login` nem provisione credencial do Harbor (não há licença e o pull é público). Deploy pelo compose genérico (`templates/chatwoot/`, ver `templates/chatwoot/README.md`); no Coolify, setar `CHATWOOT_IMAGE=ghcr.io/fazer-ai/chatwoot:latest` no `templates/chatwoot/docker-compose.coolify.yml` e **remover** o `baileys-api`. **Pule a etapa 9b** (licenciar). O resto deste doc é **só Pro**.
14
+ - **`pro`** → siga abaixo (Harbor + Coolify API + `docker login` + etapa 9b).
15
+
16
+ ## Imagem privada (Harbor): credencial per-user via proxy do CLI
17
+
18
+ Este é passo **seu** de execução, não uma pergunta: a edição (Pro/OSS) já foi decidida no início, então baixar a versão Pro é automático. **Se** você mencionar ao usuário o que está fazendo, diga em linguagem dele ("vou liberar o acesso à versão Pro pra baixar os programas no servidor; usa a sua assinatura, nenhuma senha passa por mim"), **nunca** "provisionar a registry credential per-user do Harbor" nem os comandos. Frases boas × ruins em `guardrails.md`.
19
+
20
+ `harbor.fazer.ai/chatwoot/fazer-ai/chatwoot-pro:latest`.
21
+ - Credencial do Harbor pelo **proxy do hub no CLI** (não há hub MCP na sessão do agente; o CLI tem o OAuth do bootstrap):
22
+ ```sh
23
+ bunx @fazer-ai/agents hub registry-credential --apply --out harbor.secret
24
+ ```
25
+ Robot **per-user** (a MESMA cred cobre Chatwoot Pro e fazer.ai agents Pro), idempotente; grava o secret em `harbor.secret` (`0600`) e imprime só o `username`; o secret **nunca** sai no output. **Nunca** logar o secret.
26
+ - O compose é o vendorado `templates/chatwoot/docker-compose.coolify.yml` (não precisa extrair do hub).
27
+
28
+ ## Deploy via API do Coolify
29
+
30
+ O `scripts/coolify.py create-service` lê o compose, faz o **base64** (raw → 422 "should be base64 encoded") e POSTa em `/api/v1/services` com `instant_deploy:false`; depois você deploya:
31
+ ```sh
32
+ python3 scripts/coolify.py create-service --base-url http://<VPS_IP>:8000 --token-file coolify.token \
33
+ --name chatwoot --project-uuid <PROJ_UUID> --server-uuid <SRV_UUID> --environment-name production \
34
+ --compose-file templates/chatwoot/docker-compose.coolify.yml # → {uuid}
35
+ python3 scripts/coolify.py api-post --base-url http://<VPS_IP>:8000 --token-file coolify.token --path /services/<uuid>/start
36
+ ```
37
+ - Logue no Harbor com `scripts/harbor-login.py login` **antes** do `start` (o pull da privada precisa do login): roda `docker login --password-stdin` por SSH (secret fora do argv) e protege o `$` do usuário robot. O `username` vem do `hub registry-credential` (acima); o secret está em `harbor.secret` (`0600`):
38
+ ```sh
39
+ python3 scripts/harbor-login.py login --ssh root@<VPS_IP> --username '<robot-user>' --secret-file harbor.secret
40
+ ```
41
+
42
+ ## Admin + token (Rails runner via SSH)
43
+
44
+ O **usuário** cria o 1º admin do Chatwoot na própria tela de onboarding do Chatwoot (`https://chatwoot.<seu-dominio>`): você entrega o link e **espera** ele criar a conta + o admin. Quando ele voltar, pergunte **uma vez** qual e-mail ele usou nesse admin (você precisa dele pra ler o token) e **guarde esse e-mail para o resto da run**: é o e-mail do operador em todo o onboarding, inclusive o login do Langfuse (etapa 5). **Não volte a perguntar e-mail depois disso** (nem "qual e-mail pro Langfuse/OWNER?"): reuse este. Com o admin já criado, `scripts/chatwoot-admin.py provision` **lê** esse admin (pelo email) e devolve o `api_access_token` dele: roda o Rails runner **dentro** do container (base64-piped por SSH, então o email e as aspas do script não tocam o shell) e **nunca** cria conta nem usuário. O token é o `AccessToken` polimórfico do usuário (idempotente: reusa o existente ou minta um pelo `AccessToken` do owner, `find_or_create_by!`).
45
+ ```sh
46
+ python3 scripts/chatwoot-admin.py provision --ssh root@<VPS_IP> --container <chatwoot-rails-container> \
47
+ --email <email-do-admin> --out chatwoot-admin.json
48
+ ```
49
+ Grava `api_access_token` num arquivo `0600`; só metadados são impressos. Se o email ainda não existe, o helper erra claro (`the user must create the admin … first`) → espere o usuário criar e re-rode. Esse `api_access_token` vai no header `api-access-token: <token>` (hífen: sobrevive a proxies, ver `deploy-b-portainer.md`) das chamadas REST do Chatwoot **e** no `deployment_connect` da etapa 9 (transitório, nunca persistido em repo/log).
50
+
51
+ ## FQDN + 503
52
+
53
+ Ver `gotchas.md`: setar `service_applications.fqdn` no `coolify-db` + restart (o `SERVICE_FQDN_*` env **não** dirige o Traefik).
54
+
55
+ ## Inbox API (pro E2E)
56
+
57
+ `POST https://chatwoot.<seu-dominio>/api/v1/accounts/1/inboxes` (header `api-access-token`) body:
58
+ ```json
59
+ {"name":"Validação (API)","channel":{"type":"api","webhook_url":""}}
60
+ ```
61
+ → inbox `Channel::Api`. O bind do agente (etapa 9) provisiona o webhook do bot; **não** precisa setar `webhook_url` à mão.
@@ -0,0 +1,46 @@
1
+ # 04: Deploy do fazer.ai agents
2
+
3
+ > **Avise o usuário onde você está:** o fazer.ai agents vem **depois do painel e do Chatwoot**; em seguida só falta o Langfuse. Diga que vai subir o fazer.ai agents agora e que leva alguns minutos; dê sinal de vida durante a espera longa e confirme ao terminar. Ver o princípio de narração (com a contagem que se ajusta ao caso real) em `SKILL.md`.
4
+
5
+ ## Edição: Free ou Pro (lê o marcador PRIMEIRO)
6
+
7
+ Leia `~/.fazer-ai/onboarding.json` → `edition` (`free` | `pro`; ausente = `free`). É a escolha **explícita** do CLI; respeite-a. Eixo **independente** do `chatwootTier` (etapa 3).
8
+
9
+ - **`free`** → imagem **pública** (default do compose). **Sem** `docker login`, não seta `AGENTS_IMAGE`. (Hoje o default é o placeholder `ghcr.io/fazer-ai/agents:latest`; a imagem pública Free ainda não foi publicada.)
10
+ - **`pro`** → imagem **privada** no Harbor: `harbor.fazer.ai/agents/fazer-ai/agents-pro:latest`. Provisione a credencial **per-user** pelo **proxy do CLI** (`bunx @fazer-ai/agents hub registry-credential --apply --out harbor.secret`, robot per-user, grava o secret `0600` e imprime só o `username`), logue com `scripts/harbor-login.py login` (secret via `--secret-file harbor.secret`; protege o `$` do robot), e setar `AGENTS_IMAGE` pra esse path. **Nunca** logar o secret.
11
+ - **Reuso (per-user):** se o Chatwoot também for Pro (etapa 3), é o **mesmo** `docker login`, não logar duas vezes.
12
+ - **Tier A (Coolify):** setar a env `AGENTS_IMAGE` no serviço + registrar a Harbor registry credential no Coolify (igual ao Chatwoot Pro).
13
+ - **Tier B/C (compose):** `export AGENTS_IMAGE=<imagem>` (ou no `.env`) antes do `docker compose up`.
14
+
15
+ ## Compose
16
+
17
+ Use o `templates/docker-compose.coolify.yml` do repo via `scripts/coolify.py create-service` (lê o compose, base64-encoda, POSTa em `/api/v1/services`). Topologia: `agents` (imagem conforme a **edição** acima; o compose default é a Free) + `postgres` (`pgvector/pgvector:pg17`: NÃO postgres puro: o schema precisa de `CREATE EXTENSION vector`). Volume `storage:/app/storage`. Healthcheck `wget -qO- http://localhost:3000/api/health`.
18
+
19
+ ## Magic vars (Coolify gera; NÃO setar à mão)
20
+
21
+ - `SERVICE_URL_AGENTS` → `PUBLIC_URL` e `CDN_URL`.
22
+ - `SERVICE_USER_DBUSER` / `SERVICE_PASSWORD_64_DBPASSWORD` → **superuser** (dono do Postgres) → `MIGRATION_DATABASE_URL`.
23
+ - `SERVICE_USER_APPDBUSER` / `SERVICE_PASSWORD_64_APPDBPASSWORD` → **app role** (não-superuser) → `DATABASE_URL` + `LANGGRAPH_DATABASE_URL`.
24
+ - `SERVICE_PASSWORD_64_JWTSECRET` → `JWT_SECRET`; `SERVICE_PASSWORD_64_ENCRYPTIONKEY` → `ENCRYPTION_KEY`.
25
+
26
+ ## Persistência de branding/quotes (fix: já no compose)
27
+
28
+ ```
29
+ BRANDING_STORAGE_DIR=/app/storage/branding
30
+ QUOTES_STORAGE_DIR=/app/storage/quotes
31
+ ```
32
+ Sem isso caem em `./data/*` (FS efêmero do container) e logo/favicon (+ PDFs de quote) somem no redeploy. Já corrigido no `templates/docker-compose.coolify.yml`; **confira que está lá** (branding é refino manual opcional depois, em `/admin/branding`, mas a persistência precisa já estar no lugar).
33
+
34
+ ## Boot = CMD da imagem (NÃO sobrescrever `command`)
35
+
36
+ A sequência `bootstrap → migrate deploy → serve` é o CMD do Dockerfile. **Não** declare `command:` no compose (sobrescrever crash-loopa). Detalhe em `gotchas.md`.
37
+
38
+ ## FQDN + 503 + verificação
39
+
40
+ O `SERVICE_FQDN_*` não dirige o Traefik; quem roteia é a linha em `service_applications` (ver `gotchas.md`). Ache o id e seta o FQDN:
41
+ ```sh
42
+ python3 scripts/coolify.py list-apps --ssh root@<VPS_IP> # ache o id do agents
43
+ python3 scripts/coolify.py set-fqdn --ssh root@<VPS_IP> --app-id <id> --fqdn https://agentes.<seu-dominio>
44
+ python3 scripts/coolify.py api-post --base-url http://<VPS_IP>:8000 --token-file coolify.token --path /services/<uuid>/restart
45
+ ```
46
+ Antes do DNS resolver, verifique o routing por sslip.io: `curl http://agents-<service-uuid>.<VPS_IP>.sslip.io/api/health`. Depois do `/setup` (etapa 6) o app responde em `https://agentes.<seu-dominio>`.
@@ -0,0 +1,45 @@
1
+ # 05: Deploy do Langfuse (com MinIO obrigatório)
2
+
3
+ > **Avise o usuário onde você está:** o Langfuse é o **último serviço** do deploy (o painel que registra as conversas do agente). Diga que vai subir o Langfuse agora, é o último; ao terminar, confirme que o deploy dos serviços acabou e que agora vem a configuração do agente. Ver o princípio de narração em `SKILL.md`.
4
+
5
+ ## NÃO use o one-click do Coolify
6
+
7
+ O template one-click declara os `LANGFUSE_S3_*` mas sobe **sem MinIO e com creds vazias**. Resultado: `POST /api/public/ingestion` dá **HTTP 500** (`Could not load credentials from any providers` → `Failed to upload events to blob storage`) e os traces **somem em silêncio**. Pior: `GET /api/public/projects` lê só o Postgres e retorna 200, então um "test connection" ingênuo **passa** e mascara a ingestion quebrada.
8
+
9
+ ## Use o compose vendorado do repo
10
+
11
+ `templates/langfuse/docker-compose.coolify.yml`: topologia `langfuse` (web) + `langfuse-worker` + `postgres` + `redis` + `clickhouse` + **`minio`**, com as 3 famílias S3 (`EVENT_UPLOAD`/`MEDIA_UPLOAD`/`BATCH_EXPORT`) apontando pra `http://minio:9000` via as magic vars `SERVICE_USER_MINIO`/`SERVICE_PASSWORD_MINIO`. Deploy via `scripts/coolify.py create-service` (base64) + `set-fqdn` (abaixo). Detalhes e mapa magic-var↔env genérico: `templates/langfuse/README.md`.
12
+
13
+ ## Fluxo headless-seed (você provisiona TUDO num deploy; o usuário só faz login)
14
+
15
+ Padrão oficial de headless-init do Langfuse, **validado empiricamente** (stack local com o compose deste template: o `LANGFUSE_INIT_USER` vira **OWNER** da org, o signup fica fechado desde o boot, o user semeado loga com `role:OWNER`, e as keys ingerem `207`). **Não deixe o signup aberto**: o Langfuse **não tem** o gate "primeiro-admin-depois-fecha" do Coolify/agents (`AUTH_DISABLE_SIGNUP=true` devolve `422` sempre, sem exceção pro 1º usuário), então signup aberto seria uma janela real pra qualquer um se cadastrar na instância exposta. Semeie tudo de uma vez:
16
+
17
+ 1. **Gere os valores do seed.** Um par de keys `pk-lf-…`/`sk-lf-…`, um id de org e um de projeto (strings únicas), e uma **senha temporária forte com um símbolo** pro login do operador (a política do Langfuse exige um caractere não-alfanumérico em signup/troca; o seed e o login aceitam sem, mas gere com). O **e-mail é o do operador, e você já o tem: NÃO pergunte.** Reuse **exatamente o mesmo e-mail do admin do Chatwoot** (etapa 3), pra ele ter um login só entre as ferramentas. **Nunca faça uma pergunta do tipo "qual e-mail você quer usar pro Langfuse/pro OWNER?"**: o usuário já criou o admin do Chatwoot com um e-mail, e é esse que vale aqui; perguntar de novo é retrabalho e confunde. Se por algum motivo você ainda não tem o e-mail do admin do Chatwoot em mãos, pegue-o de lá (etapa 3), não do usuário.
18
+ 2. **Semeie TUDO num deploy só** (o signup já nasce fechado: `AUTH_DISABLE_SIGNUP=true` é o default do template). Set na env do serviço (Coolify) ou no `.env` (genérico) e **deploy uma vez**:
19
+ - `LANGFUSE_INIT_USER_EMAIL` (operador), `LANGFUSE_INIT_USER_NAME`, `LANGFUSE_INIT_USER_PASSWORD` (a senha gerada)
20
+ - `LANGFUSE_INIT_ORG_ID`, `LANGFUSE_INIT_ORG_NAME`
21
+ - `LANGFUSE_INIT_PROJECT_ID`, `LANGFUSE_INIT_PROJECT_NAME`, `LANGFUSE_INIT_PROJECT_PUBLIC_KEY` (`pk-lf-…`), `LANGFUSE_INIT_PROJECT_SECRET_KEY` (`sk-lf-…`)
22
+
23
+ No boot o Langfuse cria o **usuário (OWNER da org) + org + projeto + keys**. O USER exige a ORG (por isso vão juntos); upsert **por id**, então re-deploy não duplica.
24
+ 3. **Entregue o login e mostre a senha temporária.** A URL do Langfuse em **`/auth/sign-in`** (login, **não** signup), o **e-mail dele + a senha gerada** (mostre a senha, é o único jeito de ele entrar), e peça pra **trocar no 1º acesso** pela UI. O seed é **create-if-not-exists** (validado: a troca de senha do operador **sobrevive** a redeploys, o `LANGFUSE_INIT_USER_PASSWORD` fica inerte), então a senha definitiva é a dele e nunca passou por você. Ele nunca abre "Settings → API Keys" nem copia key nenhuma.
25
+
26
+ Como **você gerou** as keys no passo 1, elas já estão na sua mão pra ligar no fazer.ai agents (abaixo). Um deploy, sem redeploy, sem ler `org_id` no Postgres, e o signup **nunca** ficou aberto.
27
+
28
+ ## FQDN (preserve a porta)
29
+
30
+ `scripts/coolify.py set-fqdn --ssh root@<VPS_IP> --app-id <id> --fqdn https://langfuse.<seu-dominio>:3000` (ache o id com `list-apps`). O template mapeia o FQDN pra porta 3000 do container; **dropar o `:3000` quebra o routing**. Ver `gotchas.md`.
31
+
32
+ ## Verifique a ingestion (health verde NÃO basta)
33
+
34
+ `scripts/langfuse-verify.py` POSTa um batch em `/api/public/ingestion` e exige **207/200** (não 500); as chaves são o par que você semeou, lidas de um arquivo `0600` (a secret key fora do argv):
35
+ ```sh
36
+ echo '{"publicKey":"<pk-lf>","secretKey":"<sk-lf>"}' > langfuse.keys && chmod 600 langfuse.keys
37
+ python3 scripts/langfuse-verify.py ingestion --base-url https://langfuse.<seu-dominio>:3000 --keys-file langfuse.keys
38
+ ```
39
+ Status 500 = quase sempre MinIO/S3 ausente.
40
+
41
+ ## Ligue no fazer.ai agents (por MCP, `langfuse_connect`)
42
+
43
+ O wiring é **por MCP**, num tool só: `langfuse_connect` recebe `public_key`/`secret_key`/`base_url` **inline** (as keys que você semeou), cria a credencial no vault **já preenchida** (`kind:"langfuse"`, `{publicKey, secretKey}` + `baseUrl`) e liga o tracing no tenant-settings. É dry-run por padrão: revise o preview (keys redigidas) e reenvie com `dry_run:false` pra aplicar. Mesmo padrão do `deployment_connect` do Chatwoot (segredo de infra inline). Como as keys já existem, a credencial nasce **preenchida** (NÃO `pending`): uma entry pending não resolve o segredo e o tenant-settings rejeita com `credential ref not found`. (No vault o campo é `baseUrl` camelCase, ver `gotchas.md`; doc do tool em `docs/mcp.md`.)
44
+
45
+ > **Ao pedir o OK do usuário** pra aplicar, fale do benefício, não do mecanismo: "vou ligar o painel que registra as conversas do agente, pra você acompanhar e depurar depois". **Não** cite `langfuse_connect`/"tracing"/"tenant-settings"/keys. Frases boas × ruins em `guardrails.md`.
@@ -0,0 +1,47 @@
1
+ # 06: `/setup` do fazer.ai agents + conectar o MCP
2
+
3
+ ## `/setup` (cria o 1º admin = SUPER_ADMIN)
4
+
5
+ - Quando o banco está sem usuários, o fazer.ai agents abre o `/setup`. No boot ela loga um token único e a URL pronta `${PUBLIC_URL}/setup?token=...` (a menos que `SETUP_TOKEN_REQUIRED=false`).
6
+ - O 1º admin é criado como **SUPER_ADMIN** (`tenant_id` NULL) via `POST /api/auth/setup`.
7
+ - O **usuário** abre a URL `/setup` (com o token do boot) e cria o 1º admin. Você entrega a URL e **espera**; não cria por conta própria.
8
+ - Config de boot relevante (defaults): `setupTokenRequired:true`, `signupEnabled:false`.
9
+
10
+ ### O tenant nasce do `companyName` do `/setup`: confira depois
11
+
12
+ O `/setup` cria **um** tenant a partir do `companyName` que quem preenche o form digita. No **real**, é o usuário que digita: pode sair diferente do nome combinado (numa run real saiu `fazer.ai`/`fazer-ai` em vez de `Clínica Moreira`). Depois de conectar o MCP, rode **`tenant_list`** e **confira** o `name`/`slug`:
13
+ - bate com o escolhido → siga.
14
+ - divergiu → **NÃO crie outro tenant** (`tenant_create` é proibido, ver abaixo): siga com o que existe e **avise o usuário** da divergência. Renomear, se ele quiser, é `tenant_update` (não um tenant novo).
15
+
16
+ ## Conectar o MCP do fazer.ai agents (OAuth). GATE: sem as tools, PARE, não contorne
17
+
18
+ Toda a config do fazer.ai agents (import do agente, vault, tenant-settings, KB, deployment/bind) é **exclusivamente via MCP tools**: elas carregam dry-run + audit + o fence de tenant. As tools de MCP só carregam no **boot** da sessão, e a **ordem do reinício muda por harness**: o Claude autentica na TUI (`/mcp`), que exige o server já carregado no boot, então reinicia **antes** de autenticar; Codex/Hermes autenticam por comando de CLI, então reiniciam **depois**. Endpoint do fazer.ai agents: `https://agentes.<seu-dominio>` (discovery/caminho exato em `docs/mcp.md`).
19
+
20
+ **Claude Code** (reinicie ANTES de autenticar):
21
+ 1. **Adicione:** `claude mcp add` (transport HTTP) pro endpoint. O server entra no config, mas **não** aparece na sessão atual nem no `/mcp` (a sessão leu o config no boot).
22
+ 2. **Reinicie a sessão** (feche e reabra o `claude` no mesmo dir). Só agora o `/mcp` lista `fazer-ai` como **"Needs authentication"** (esperado, não é falha).
23
+ 3. **Autentique:** `/mcp` → `fazer-ai` → **Authenticate** → browser; o usuário loga com o admin do `/setup` (SUPER_ADMIN) e aprova os escopos (`mcp:read/write/admin`). Ao voltar **"Connected"**, as tools carregam **na mesma sessão, sem 2º reinício**.
24
+
25
+ **Codex / Hermes** (autentique por CLI, depois reinicie):
26
+ 1. **Adicione + logue:** `codex mcp add` + `codex mcp login` (ou o equivalente do Hermes), que abre o browser pro mesmo login SUPER_ADMIN.
27
+ 2. **Reinicie a sessão.** As tools carregam no boot seguinte.
28
+
29
+ O access token fica no store de MCP do harness, não conosco (`guardrails.md`).
30
+
31
+ **GATE DURO. Se as tools `fazer-ai` (`whoami`, `tenant_list`, `agent_import`, …) NÃO estão expostas nesta sessão:**
32
+
33
+ - **PARE e peça ao usuário pra completar o passo do harness dele** (Claude: **reiniciar → `/mcp` Authenticate**; Codex/Hermes: **`mcp login` → reiniciar**), confirmando o Authenticate/login **e** o reinício. Espere ele voltar. Esse é o **único** caminho.
34
+ - **NUNCA contorne.** É **proibido**, para qualquer config do fazer.ai agents: chamar a **API REST direto** (mintar API key, cookie + `x-tenant-id`); fazer requisições ao endpoint `/mcp` **por fora do harness**; **ler o código-fonte/bundle do fazer.ai agents** (`/app/src`, `/app/dist`) pra descobrir endpoints internos; montar **OAuth manual**. Esses bypasses pulam dry-run/audit/fence, são frágeis, e **não provam o MCP**, que é o produto que esta run existe pra validar.
35
+ - **Sinal de que você entrou no anti-padrão:** se você se pegou grepando `agents.controller.ts`, procurando `POST /api/v1/agents/import`, ou mintando uma API key pra "equivalente REST" porque "a tool não apareceu" → **PARE imediatamente** e peça o reinício. Não existe "fallback REST transitório" para config do fazer.ai agents. **Idem pra achar uma rota/deeplink do console:** não baixe+grepe o bundle da SPA; as rotas que a skill usa estão nas refs (ex.: o deeplink de credencial em `08-agent-import.md` §2), e o bundle é minificado/hasheado (frágil).
36
+
37
+ ## Alvo de tenant nas MCP tools (SUPER_ADMIN)
38
+
39
+ O admin do `/setup` é **SUPER_ADMIN** (`tenant_id` NULL), então o token MCP é **fleet-level**: `whoami` mostra `tenantId: null`. Ele **não** carrega um tenant embutido; você escolhe o tenant **por chamada**:
40
+
41
+ 1. Logo após conectar, rode **`tenant_list`**: há **um** tenant (o criado pelo `/setup`, a partir do `companyName`). Anote o **slug** (ou o id).
42
+ 2. Em **toda tool per-tenant** (`agent_import`, `agent_*`, `vault_*`/`credential_create`, `tenant_settings_*`, `deployment_connect`/`inbox_bind`, `knowledge_*`, …) passe o argumento **`tenant`** com esse slug (ou id). O campo só aparece para tokens SUPER_ADMIN; para um token de tenant (API key) ele nem existe e o tenant é implícito.
43
+ 3. **NUNCA chame `tenant_create`.** O tenant já existe (o do `/setup`); criar outro gera um tenant **órfão**, e o agente/credenciais importados cairiam no lugar errado. Se uma per-tenant tool reclamar de *"fleet-level … pass `tenant`"* ou *"no tenant target"*, a causa é **faltar o argumento `tenant`**, não faltar um tenant: rode `tenant_list` e passe o `tenant`.
44
+
45
+ ## Prefixo dos paths (referência factual, NÃO um convite a usar REST)
46
+
47
+ Onde estas refs citam `/v1/...` (ex.: `/v1/vault`, `/v1/chatwoot/deployment`), o path HTTP real é `/api/v1/...`. Isto é só pra você **ler** as refs corretamente e casar com as MCP tools equivalentes; **não** é autorização pra chamar REST: a config do fazer.ai agents vai por MCP (acima). A API key (`POST /api/v1/api-keys { "displayName": "..." }`, o campo é `displayName` não `name`) existe para integrações externas do usuário, não para a skill contornar o MCP.
@@ -0,0 +1,55 @@
1
+ # 08: Import do agente + credenciais + embedding + KB
2
+
3
+ > **Ao falar com o usuário** nesta etapa, descreva o resultado, não a mecânica: "vou criar o agente de atendimento (a Maria, uma recepcionista de exemplo); ele nasce desligado e em modo de teste, então não fala com cliente até você liberar". Na hora de pedir as chaves que faltam, diga "faltam algumas chaves (ex.: a da OpenAI) pro agente funcionar; vou te mandar um link direto pra colar cada uma com segurança, elas não passam por mim". **Não** cite `agent_import`/`credential_create`/"pending"/"deeplink"/"vault"/"tenant". Frases boas × ruins em `guardrails.md`.
4
+
5
+ ## 1. Importar (`agent_import`, mcp:write)
6
+
7
+ A skill traz o **agente padrão** vendorado em `samples/agents/maria-clinica-moreira.json` ("Maria", recepção da Clínica Moreira fictícia: agendamento, FAQ via KB, voz, Asaas). **Importe-o por padrão**; só use outro export se o usuário trouxer o dele. Leia o arquivo e passe o conteúdo como `export`:
8
+
9
+ ```jsonc
10
+ agent_import { "export": <conteúdo de samples/agents/maria-clinica-moreira.json>, "tenant": "<slug do tenant_list>" } // dry_run:true → preview, depois dry_run:false
11
+ ```
12
+
13
+ - **SUPER_ADMIN:** inclua `tenant` (o slug/id de `tenant_list`); o token é fleet-level. **NUNCA** `tenant_create`: o tenant do `/setup` já existe; criar outro joga o agente no tenant errado (ver etapa 6).
14
+ - O agente é **sempre** criado **disabled + test mode** (nunca vai ao ar pra cliente por acidente); componentes (KB/tools/etc.) recriados/reusados **por nome**.
15
+ - Credenciais faltantes (os nomes não existem no tenant novo): o import cria uma entrada **pending** (mantendo o ref wired) e emite o aviso `credentialPending`; o usuário preenche no vault.
16
+ - **Exceções** que não viram pending no import → `credentialNotFound`: (a) OAuth gerenciado (`google_oauth`, `mcp_oauth`), que nunca pode ser pending (vem de connect flow); (b) kinds que exigem `base_url`/`param_name`, porque o import não tem esses valores pra passar. Pra (b), crie explicitamente com `credential_create` passando `base_url`/`param_name` (ex.: `openai_compatible`); pra (a), trate o OAuth à parte.
17
+
18
+ ## 2. Preencher credenciais (segredo NUNCA passa pelo agente)
19
+
20
+ - **Sempre entregue o deeplink**, nunca só "vá em Configurações → Credenciais". Cada pending abre direto pelo `fillAt = ${PUBLIC_URL}/resources/vault?fill=<vaultId>` (o `?fill=<id>` abre o modal de preenchimento da entrada). É o caminho canônico das credenciais do **usuário** (OpenAI/ElevenLabs/Gemini/Asaas). O formato está aqui; **não** baixe/grepe o bundle da SPA pra descobrir a rota/deeplink (minificado + hasheado, muda a cada build).
21
+ - **De onde vem o `fillAt`:** um `credential_create` **real** (`dry_run:false`) devolve o `fillAt` na resposta. Mas o **dry-run não devolve**, e re-criar uma que já existe **duplica**. Pra uma pending que **já existe** (import/brownfield/run anterior), **não re-crie**: pegue o `id` no `vault_list` e monte a URL você mesmo (`${PUBLIC_URL}/resources/vault?fill=<id>`).
22
+ - A chave OpenAI e as demais do **usuário** (ElevenLabs/Gemini/Asaas) o usuário preenche por esse deeplink; o segredo nunca passa pelo agente. Acompanhe pelo `vault_list` até o status sair de `pending`. (Exceção: o Langfuse, cujas keys **você** provisiona ao semear o projeto, não é segredo do usuário; ligue no fazer.ai agents via `langfuse_connect` com as keys inline, ver `references/05-langfuse.md`.)
23
+
24
+ ## 3. Religar o modelo + habilitar (`agent_update`)
25
+
26
+ Habilite o agente **mantendo o test mode** (como ele foi importado). Habilitar liga o bot; o `mode:"test"` faz ele responder só em conversas ativadas com `/teste` (etapa 10) e ficar em silêncio nas demais (com uma nota privada), então ele **não** atende cliente real ainda. Ligar pra produção é o **passo final do usuário** (abaixo).
27
+
28
+ ```jsonc
29
+ agent_update {
30
+ "agent_id": "<id>", "enabled": true,
31
+ "model_config": { "provider":"openai", "model":"gpt-5.4-mini", "temperature":0.3, "credentialRef":"<nome da vault entry>" }
32
+ }
33
+ ```
34
+
35
+ - **Não** mande `mode` aqui: o import já criou em `test` e a validação (etapa 10) roda nesse modo via `/teste`. Não promova pra `production` por conta própria.
36
+ - Via MCP, `model_config.credentialRef` aceita o **nome** da entrada do vault (resolvido server-side). Via REST é a forma `"vault:<id>"`.
37
+ - Mande o `model_config` **completo** pra não clobberar campos. (O STT pode reusar a mesma chave.)
38
+
39
+ ### Ir pra produção é decisão do usuário
40
+
41
+ Depois do E2E aprovado (etapa 10), **o usuário** decide quando o agente vai ao ar pra clientes reais: aí sim `agent_update { "agent_id":"<id>", "mode":"production" }`. Entregue o agente validado em test mode e deixe esse flip pro usuário; não o faça no fluxo automático.
42
+
43
+ ## 4. Embedding é por-tenant (senão a KB falha)
44
+
45
+ Ligue a credencial de embedding no tenant **via MCP**: `tenant_settings_update { embedding: { credential_ref: "<nome da entry>" } }` (provider/model default a `openai`/`text-embedding-3-small`). Sem isso (ou com a credencial ainda pendente) a KB **não indexa**: os docs ficam `UNINDEXED`, **não** FAILED (pré-requisito faltando não é falha). É no nível do **tenant**, não por-KB. O embedding usa a chave OpenAI, então ligue-o **depois** do usuário preencher o OpenAI (passo 2).
46
+
47
+ ## 5. Indexar a KB (`knowledge_reindex`)
48
+
49
+ - Os docs do import entram **UNINDEXED**. Com o embedding ligado (passo 4) **e o OpenAI preenchido**, indexe a base inteira numa chamada **via MCP**: `knowledge_reindex { knowledge_base_id }` (dry-run por padrão; `dry_run:false` aplica).
50
+ - **Se o pré-requisito ainda falta** (embedding não configurado, ou credencial pendente), o `knowledge_reindex` **não enfileira nada** e devolve `blocked` + `fillAt` (deeplink pra preencher a credencial); os docs **ficam UNINDEXED**, não FAILED. Entregue o `fillAt` ao usuário, espere preencher, e re-rode.
51
+ - Pra recuperar docs que **de fato** falharam na ingestão (erro real, não pré-requisito), `knowledge_reindex { knowledge_base_id, include_failed:true }` re-enfileira os FAILED em lote (ou `knowledge_document_retry { document_id }` por doc).
52
+
53
+ ## 6. Gate antes de seguir
54
+
55
+ Não declare o import pronto com aviso aberto: **todos os docs da KB READY** + **grounding verificado** no playground (pergunte algo que só a KB sabe); STT/TTS/visão sinalizados → conectar credencial ou desligar a feature. Detalhe + features opcionais (voz, Google OAuth) em [`agent-features.md`](agent-features.md).
@@ -0,0 +1,41 @@
1
+ # 09: Plugar o Chatwoot no fazer.ai agents
2
+
3
+ > **Ao pedir o OK do usuário** para aplicar (cada `dry_run:false` abaixo), descreva o efeito, não a tool: "vou conectar o seu Chatwoot ao agente e ligar o robô na caixa de entrada, pra ele começar a responder as conversas". **Não** cite `deployment_connect`/`inbox_bind`/`webhook`/"Agent Bot". Frases boas × ruins em `guardrails.md`.
4
+
5
+ Sequência MCP-first. As tools de deployment são `mcp:admin` (SUPER_ADMIN); `inbox_bind` é `mcp:write`. O admin token do Chatwoot **não é credencial de vault** (é guardado encriptado na linha do deployment), então **não** use o fluxo de pending/deeplink dele. Há dois caminhos pra entregá-lo, conforme quem tem o token (o **mesmo** vale para `chatwootSource: existing`, Chatwoot BYO): se a instância é on-box/alcançável por SSH, o agente pega o token via Rails runner (Caso A); se é off-box (Chatwoot Cloud / outro host), o usuário fornece o token (Caso B).
6
+
7
+ ## 1. Conectar o deployment
8
+
9
+ ### Caso A: o agente provisionou o Chatwoot (tem o token)
10
+
11
+ O agente extraiu o admin token via Rails runner (etapa 3), então registra direto por MCP, em uma chamada:
12
+
13
+ ```jsonc
14
+ deployment_connect { "base_url":"https://chatwoot.<seu-dominio>", "admin_token":"<token cru>" } // dry_run:false pra aplicar
15
+ ```
16
+
17
+ O token é usado in-band e **redatado no audit** (o audit guarda só metadados). Valida via `/profile`, **persiste o deployment** (URL + token criptografado na linha do deployment) e retorna as contas alcançáveis. Ainda **não** conecta as contas: isso é o passo 2.
18
+
19
+ ### Caso B: traga seu próprio Chatwoot (só o usuário tem o token)
20
+
21
+ O agente **não** tem o token. Em vez de inventar credencial pending (o token nem é de vault), o agente **linka o usuário pra tela `/channels`**: em "Connect instance" (SUPER_ADMIN), o usuário cola Base URL + Admin access token (validado via `/profile`, guardado encriptado). O usuário pode seguir ali pelo "Manage accounts" e pelo bind de inbox, ou devolver pro agente continuar via MCP.
22
+
23
+ ## 2. Conectar a conta + sincronizar inboxes (`deployment_set_accounts`)
24
+
25
+ ```jsonc
26
+ deployment_set_accounts { "account_ids": [1] } // dry_run:false pra aplicar
27
+ ```
28
+
29
+ Conecta as contas selecionadas (cria a instância + sincroniza os inboxes pra agents) e soft-desconecta as de-selecionadas. **É este passo que conecta as contas** (o `deployment_connect` só registrou o deployment e listou as contas).
30
+
31
+ ## 3. Bindar o inbox ao agente (`inbox_bind`)
32
+
33
+ ```jsonc
34
+ inbox_bind { "inbox_id":"<id do inbox no fazer.ai agents>", "agent_id":"<id do agente>" } // dry_run:false pra aplicar
35
+ ```
36
+
37
+ O bind **provisiona/conecta o bot do agente no Chatwoot** (Agent Bot + webhook `/v1/chatwoot/webhook/:routeToken`); o `routeTokenHash`/`inboundSecretRef` ficam encriptados no fazer.ai agents e **nunca** saem no export. Não precisa setar `webhook_url` à mão. Verifique: bot-status do inbox = `active`.
38
+
39
+ ## Só MCP (nada de REST à mão)
40
+
41
+ O fazer.ai agents expõe endpoints REST equivalentes por baixo (o que a tela `/channels` chama), mas **não os chame à mão**: as tools MCP `deployment_connect`/`inbox_bind` são o único caminho (regra MCP-only, ver `SKILL.md` e `06-setup-and-mcp.md`).
@@ -0,0 +1,34 @@
1
+ # 10: Validar E2E
2
+
3
+ ## Pré-condições
4
+
5
+ - Agente importado e habilitado (`enabled:true`), **em test mode** (`mode:test`, como importado; a validação abaixo roda nesse modo via `/teste`; promover pra produção é decisão do usuário, etapa 8), modelo religado a uma vault key real (etapa 8).
6
+ - KB com docs **READY** (etapa 8).
7
+ - Inbox do Chatwoot bound ao agente, bot `active` (etapa 9).
8
+ - Langfuse com ingestion **207** + wired no fazer.ai agents (etapas 5 e 8).
9
+
10
+ ## 1. Playground (modelo real, sem Chatwoot)
11
+
12
+ Via MCP (preferido): `agent_playground` (mcp:read; aceita texto ou `attachment` base64/url, e `reply_with_audio`). Via REST: `POST /api/v1/agents/:id/playground`. O agente responde com o modelo real. Cheque **grounding**: pergunte algo coberto pela KB e confirme que a resposta usa o conteúdo indexado (não uma resposta genérica).
13
+
14
+ ## 2. Integração Chatwoot → agents via Inbox API (obrigatório, sem aparelho)
15
+
16
+ Prova a ponta `incoming → webhook → turn → reply` **sem aparelho**, com um inbox `Channel::Api`:
17
+ - Crie um inbox `Channel::Api` no Chatwoot e benda ao agente (`inbox_bind`, etapa 9), que auto-provisiona o Agent Bot + webhook.
18
+ - Crie uma conversa e **ative o test mode nela**: injete uma mensagem **incoming** com o conteúdo exatamente `/teste`. Em test mode o agente fica em silêncio numa conversa até receber `/teste` (e deixa uma nota privada explicando o porquê); o `/teste` libera as respostas **só nessa conversa**. Sem ele a mensagem chega e espelha, mas o agente **não** responde: é o comportamento correto do test mode, não uma falha.
19
+ - Agora injete a **mensagem real** de teste (incoming, `message_type: incoming`) na mesma conversa. **Monte o JSON da mensagem num arquivo UTF-8** e POSTe apontando pro arquivo (`curl --data @msg.json` ou helper): texto com acento montado inline no PowerShell volta corrompido (`Olá`→`Ol?`), ver `gotchas.md`.
20
+ - Cadeia esperada: incoming → webhook (`/api/v1/chatwoot/webhook/:routeToken`) → **debounce** → turn → modelo real → resposta **outgoing** na conversa. Confirme a resposta + o `ExecutionLog`/trace no Langfuse.
21
+
22
+ Este é o teste que **não pode ficar pendente**: é o que prova que bind + webhook funcionam. O agente segue em test mode depois disto; **não** promova pra produção (decisão do usuário, etapa 8).
23
+
24
+ ## 2b. WhatsApp real (opcional, confirma o transporte)
25
+
26
+ Pareie a inbox real (Baileys via QR) com um número que o usuário controle e mande uma mensagem: mesma cadeia do passo 2, exercitando o transporte WhatsApp de verdade. Pode ficar pendente sem invalidar o core (a integração já foi provada no 2).
27
+
28
+ ## 3. Traces no Langfuse
29
+
30
+ - Confirme que o turn aparece no Langfuse (env `production-playground` ou `production`, session = threadId do fazer.ai agents). A ingestion já foi validada em 207 na etapa 5.
31
+
32
+ ## Critério de aceite
33
+
34
+ Responde no **playground** (com **KB grounding** confirmado) E na **integração via Inbox API** (conversa ativada com `/teste`, em test mode); **trace** no Langfuse. O **WhatsApp físico** é confirmação opcional (a integração já foi provada via Inbox API). O **Kanban** segue o passo 9b: happy-path quando há licença, ausente no OSS.