feat: update default company email prompt in install command and fix … - #74
feat: update default company email prompt in install command and fix …#74luizhakan wants to merge 34 commits into
Conversation
…package name in lockfile
…a tarefa com dados corretos
…e testes correspondentes
- Implement SchemaSnapshotServiceTest to validate schema snapshot generation and caching behavior. - Create CliWhatsappMessageSuggesterEnrichedContextTest to ensure AI suggestions for WhatsApp messages are accurate based on tasks and client analysis. - Add ClientAiAnalysisUiTest to verify UI behavior for client analysis generation and display. - Develop TaskAiActionsTest to test AI-driven actions for task summaries and subtasks creation. - Introduce TaskDraftMessageActionTest to validate draft message generation and handling in task context. - Implement TaskSuggestDescriptionActionTest to ensure AI suggestions populate task descriptions correctly. - Create WhatsappDictateMessageTest to validate AI message generation based on user instructions in WhatsApp conversations. - Add GenerateClientAnalysisTest to ensure client analysis generation and notification functionality works as expected. - Implement TaskWaitingClientTagTest to validate task tagging behavior based on client waiting status. - Create WhatsappConversationTokenServiceTest to verify token counting for WhatsApp conversations. - Add ConversationHistoryRendererTest to ensure proper rendering of conversation history with attachments. - Implement ReadOnlySelectValidatorTest to validate SQL query restrictions for read-only operations. - Create PejotaHelperOrDefaultTest to ensure default settings are applied when no user is authenticated.
- Added functionality to handle AI-generated suggestions in the EvolutionWebhookHandler. - Introduced a new database migration for the `whatsapp_suggestions` table to store AI suggestions. - Updated the `WhatsappConversationSyncService` to disable suggestion dispatching during bulk imports. - Enhanced the UI to display pending AI suggestions in the WhatsApp conversation resource. - Created tests for the new suggestion service and actions related to accepting and dismissing suggestions. - Implemented job handling for analyzing WhatsApp conversations and dispatching suggestions with a delay.
- storeMessage persisted the whole sync batch payload on every message,
exhausting PHP memory on the chat poll (blank 500 modal); each message
now keeps only its own record, without inline media base64
- the 10s chat poll now syncs without media downloads/AI enrichment
(withMedia: false); full sync stays on the manual action
- context token refresh runs once per conversation instead of per message
- fromMe messages no longer rename conversations to the account owner's
pushName ("Luiz Fernando"); candidate discovery keeps existing names
- messages chat: edit/delete own messages on WhatsApp via Evolution API
(updateMessage/deleteMessageForEveryone), respecting WhatsApp windows
- messages chat: fix leading whitespace from Blade indentation inside
whitespace-pre-wrap bubbles; add wire:key to the message loop
- assistant schema snapshot documents units (duration in minutes, money
in cents) so the data assistant stops misreading worked hours
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The data assistant gains its single write capability: proposing an
invoice draft ({"invoice": {...}}) that the server validates (client,
project, items with product/unit, mandatory due_date), stores as a
pending action and answers with a data summary plus a one-word
passphrase generated in PHP. The invoice (status Sent) is only created
when the user's next message matches the passphrase exactly
(case-sensitive), compared deterministically before any AI runs; the
pending draft expires in 15 minutes and any other message just
continues the conversation.
The model can never create, update or delete anything by itself: SQL
stays SELECT-only on the read-only connection, unknown write-like
actions are ignored, and tests pin all of it down.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…do suporte a conversas e conversão de timestamps para o fuso horário local
…ling - Implement `AssistantWhatsappWebhookHandlerTest` to cover various scenarios for handling WhatsApp messages, including session management, command processing, and message types. - Introduce `MakesAttachmentFixtures` trait for creating realistic file uploads in tests. - Create `ProcessAssistantWhatsappMessageTest` to validate the processing of incoming messages and responses, including audio transcription and error handling. - Add `WhatsappMarkdownConverterTest` to ensure proper conversion of markdown to WhatsApp format. - Implement `WhatsappJidNormalizerTest` to validate normalization of WhatsApp JIDs and number handling. - Enhance `EvolutionApiClientTest` with tests for sending text messages and configuring webhooks for specific instances. - Update existing tests to reflect changes in message handling logic and ensure proper assertions.
…ion feature - Added a nullable 'name' column to the whatsapp_conversations table and populated it based on existing data. - Implemented a modal in the messages chat view for users to ask questions to an AI, including validation for input. - Created tests for the new AI question feature and ensured it does not interfere with existing conversation data. - Updated existing tests to reflect changes in conversation handling and ensure proper functionality.
…incluindo campos e lógica para gerenciamento de grupos
…tifications - Implement DailyPlanGeneratorTest to validate daily plan generation logic. - Create DailyPlanResponseParserTest to ensure correct parsing of responses. - Add DailyPlanWhatsappNotifierTest to verify WhatsApp notifications for daily plans. - Introduce GenerateDailyPlansCommandTest to test command for generating daily plans. - Implement PlanOfTheDayPageTest for Livewire component handling of daily plans. - Add PlannerCapacityTest to validate planner capacity calculations. - Create SendDailyPlansCommandTest to test sending of daily plans via command. - Implement SyncRecentWhatsappConversationsCommandTest to verify synchronization of recent WhatsApp conversations.
|
Note Reviews pausedIt looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the Use the following commands to manage reviews:
Use the checkboxes below for quick actions:
📝 WalkthroughWalkthroughEste PR introduz um assistente de dados baseado em IA (chat web e via WhatsApp, com suporte a anexos), uma integração completa com a Evolution API para conversas de WhatsApp, um planejador diário automático gerado por IA e enviado via WhatsApp, análises de relacionamento de clientes e ações de IA em tarefas, além de configuração, migrações e um script Python independente de exportação de e-mails. ChangesPlataforma de IA, WhatsApp e Planner
Estimated code review effort: 5 (Critical) | ~180 minutes Script utilitário de exportação de e-mails
Sequence Diagram(s)sequenceDiagram
participant Usuario
participant Chat as AssistantChat (Livewire)
participant Job as ProcessAssistantMessage
participant Servico as AssistantChatService
participant CLI as AiCliRunner
Usuario->>Chat: envia mensagem/anexo
Chat->>Job: dispatch(conversation, user)
Job->>Servico: respond(conversation)
Servico->>CLI: complete(prompt)
CLI-->>Servico: resposta JSON (say/query/invoice)
Servico-->>Job: texto final
Job-->>Chat: mensagem do assistente persistida
sequenceDiagram
participant Scheduler
participant Command as pj:daily-plan
participant Job as GenerateDailyPlan
participant Generator as DailyPlanGenerator
participant CLI as AiCliRunner
participant Notifier as DailyPlanWhatsappNotifier
participant Cliente
Scheduler->>Command: executa diariamente às 07h
Command->>Job: dispatch(company, date, mode)
Job->>Generator: generate(company, date, mode)
Generator->>CLI: complete(prompt do planner)
CLI-->>Generator: itens priorizados
Generator-->>Job: DailyPlan READY
Command->>Notifier: send(plan)
Notifier->>Cliente: envia plano via WhatsApp
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
…horário do usuário e criar testes correspondentes
There was a problem hiding this comment.
Actionable comments posted: 2
Note
Due to the large number of review comments, Critical severity comments were prioritized as inline comments.
🟠 Major comments (32)
export_emails.py-35-52 (1)
35-52: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win
urlopensem timeout.A chamada de rede em
run_querynão define timeout, podendo travar o script indefinidamente se o Grafana ficar sem resposta.🕒 Proposta de correção
try: - with urllib.request.urlopen(req) as response: + with urllib.request.urlopen(req, timeout=30) as response: data = json.loads(response.read().decode('utf-8'))Quanto aos alertas de SSRF (Ruff S310 / ast-grep
urlopen-unsanitized-data) para essas linhas:GRAFANA_URLé uma constante fixa no arquivo, não input externo, então considero falso positivo nesse contexto.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@export_emails.py` around lines 35 - 52, Atualize a chamada de rede em run_query, especificamente urllib.request.urlopen(req), para fornecer um timeout finito usando a configuração ou constante de timeout existente; preserve o processamento bem-sucedido e o tratamento atual de exceções.Source: Linters/SAST tools
export_emails.py-109-115 (1)
109-115: 🔒 Security & Privacy | 🟠 Major | ⚡ Quick winSQL injection via interpolação de datas não validadas em três queries
rawSql.start_date/end_datevêm diretamente deargs.inicio/args.fim(CLI) sem validação de formato, e são inseridas por f-string narawSqlenviada ao datasource MySQL do Grafana nos três modos.
export_emails.py#L109-L115: validar/parsearstart_date/end_datecomo datas antes de montar a query (ex.:datetime.strptime), ou usar placeholders ($__timeFrom()/$__timeTo()do Grafana) em vez de interpolação de string.export_emails.py#L149-L154: aplicar a mesma validação/parametrização usada no modo "templates".export_emails.py#L190-L195: aplicar a mesma validação/parametrização usada no modo "templates".🛡️ Proposta de correção (validação mínima antes de montar as queries)
+def _validate_date(value, label): + for fmt in ("%Y-%m-%d %H:%M:%S", "%Y-%m-%d"): + try: + datetime.strptime(value, fmt) + return value + except ValueError: + continue + raise SystemExit(f"Data inválida para {label}: {value!r}") + start_date = args.inicio end_date = args.fim if args.fim else datetime.now().strftime("%Y-%m-%d %H:%M:%S") + start_date = _validate_date(start_date, "--inicio") + end_date = _validate_date(end_date, "--fim")🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@export_emails.py` around lines 109 - 115, Validate and parse the CLI-derived start_date and end_date before constructing rawSql, using one consistent safe approach across the three query-building sites in export_emails.py: lines 109-115, 149-154, and 190-195. Prefer parameterization or strict date parsing such as datetime.strptime, and ensure all three modes use the same validated values without direct untrusted f-string interpolation.Source: Linters/SAST tools
bootstrap/app.php-14-16 (1)
14-16: 🔒 Security & Privacy | 🟠 Major | ⚡ Quick winNão exponha o webhook quando os segredos estiverem vazios.
Com
EVOLUTION_WEBHOOK_TOKENeEVOLUTION_API_KEYvazios,EvolutionWebhookController::authorizeWebhook()não rejeita a requisição. Esta exceção de CSRF deixa qualquer cliente capaz de disparar o processamento do webhook. Exija ao menos um segredo configurado antes de aceitar eventos, ou falhe a inicialização quando a verificação estiver habilitada.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@bootstrap/app.php` around lines 14 - 16, Update EvolutionWebhookController::authorizeWebhook() and the webhook configuration so requests are rejected when both EVOLUTION_WEBHOOK_TOKEN and EVOLUTION_API_KEY are empty; require at least one configured secret before authorizing events, or fail application initialization when webhook verification is enabled. Keep the CSRF exception only for a webhook endpoint protected by this validation.database/migrations/2026_07_13_000000_add_name_to_whatsapp_conversations_table.php-16-34 (1)
16-34: 🚀 Performance & Scalability | 🟠 Major | ⚡ Quick winRemova o N+1 do backfill. Cada conversa dispara uma consulta em
clientse umUPDATEindividual. Em bases grandes, isso alonga a migração e aumenta o tempo de bloqueio; carregue os clientes em lote e aplique os updates em chunks.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@database/migrations/2026_07_13_000000_add_name_to_whatsapp_conversations_table.php` around lines 16 - 34, Remove the N+1 pattern in the migration backfill: replace the per-conversation client lookup inside the whatsapp_conversations iteration with batched client loading, and apply name updates in chunks rather than issuing one UPDATE per record. Preserve the existing name-priority fallback using push_name, client name/tradename, phone_number, and remote_jid.config/database.php-51-58 (1)
51-58: 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick winPropague
DB_URLparasqlite_readonlytambém. A conexão principal já usaurl, mas a variante somente leitura não; em ambientes que dependem só deDB_URL, o assistente pode abrir o arquivo padrão ou apontar para o banco errado.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@config/database.php` around lines 51 - 58, Atualize a configuração `sqlite_readonly` para propagar o valor de `DB_URL` por meio da mesma chave `url` usada pela conexão principal. Preserve a configuração atual de `database`, `prefix` e das opções somente leitura, garantindo que ambientes configurados exclusivamente com `DB_URL` usem a URL fornecida.app/Services/Ai/AiCliRunner.php-35-40 (1)
35-40: 🔒 Security & Privacy | 🟠 Major | 🏗️ Heavy liftNão trate esse fluxo como somente leitura em
app/Services/Ai/AiCliRunner.php:131-133,185-197.
Quandoservices.ai_cli.agy_skip_permissionsliga--dangerously-skip-permissionseservices.ai_cli.use_sudoexecuta o AGY comoroot, um prompt/anexo malicioso pode virar execução com privilégios totais no host. Desative essa combinação ou use um usuário dedicado sem privilégios comsudoersmínimo.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@app/Services/Ai/AiCliRunner.php` around lines 35 - 40, Update the AGY execution flow in AiCliRunner, including the agy_skip_permissions and use_sudo handling, so they cannot be enabled together; reject or disable this combination before launching AGY. Preserve normal permission-skipping and sudo behavior when used independently, and ensure AGY never runs as root with permissions skipped.app/Services/Ai/ConversationContextBuilder.php-11-27 (1)
11-27: 🔒 Security & Privacy | 🟠 Major | ⚡ Quick winHistórico de conversa (WhatsApp) não é envolvido pelo
PromptGuard.
conversationContext(linha 14, usado na linha 23) é o histórico armazenado da conversa — exatamente o tipo de conteúdo quePromptGuard.phpfoi criado para proteger contra prompt injection, mas ele é interpolado semPromptGuard::wrap().Será consolidado com o achado equivalente em
NotesContextSection.php.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@app/Services/Ai/ConversationContextBuilder.php` around lines 11 - 27, Atualize o método build da classe ConversationContextBuilder para envolver conversationContext com PromptGuard::wrap() antes de passá-lo à seção “Histórico completo armazenado desta conversa”. Preserve o comportamento atual para valores nulos ou vazios e reutilize a API existente do PromptGuard, alinhando o tratamento ao de NotesContextSection.app/Services/Ai/Context/NotesContextSection.php-22-58 (1)
22-58: 🔒 Security & Privacy | 🟠 Major | ⚡ Quick winConteúdo de notas não é envolvido pelo
PromptGuard.O
PromptGuard.php(novo, nesta mesma camada) documenta que Any content originating from a client (WhatsApp messages, audio transcriptions, image descriptions, notes, etc.) must be wrapped with wrap() before being interpolated into a prompt, citando "notas" explicitamente. Aqui, o excerto de cada nota é interpolado diretamente na string final semPromptGuard::wrap(), deixando esse trecho vulnerável a prompt injection via conteúdo de nota malicioso.Será consolidado com o achado equivalente em
ConversationContextBuilder.php.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@app/Services/Ai/Context/NotesContextSection.php` around lines 22 - 58, Atualize o método build da classe NotesContextSection para envolver o conteúdo variável de cada nota, especialmente o excerpt gerado por excerpt(), com PromptGuard::wrap() antes de interpolá-lo no prompt final. Preserve o título, a data e o formato atual das linhas, aplicando a proteção somente ao conteúdo da nota conforme o padrão documentado por PromptGuard.app/Services/Ai/Context/PromptGuard.php-16-28 (1)
16-28: 🔒 Security & Privacy | 🟠 Major | ⚡ Quick win
wrap()não sanitiza ocorrências dos próprios delimitadores no conteúdo.Os delimitadores
START/ENDsão strings estáticas e previsíveis. Se o conteúdo do cliente (mensagem de WhatsApp, nota, transcrição) contiver literalmente<<<FIM_DADOS>>>, o atacante pode forjar o fechamento da seção de dados e fazer o restante do texto ser interpretado como instrução real pelo modelo — justamente o ataque que esta classe deveria prevenir.🔒️ Correção sugerida: remover ocorrências dos delimitadores antes de envolver o conteúdo
public static function wrap(string $content): string { - return self::START."\n".$content."\n".self::END; + $sanitized = str_replace([self::START, self::END], '', $content); + + return self::START."\n".$sanitized."\n".self::END; }🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@app/Services/Ai/Context/PromptGuard.php` around lines 16 - 28, Atualize o método PromptGuard::wrap para remover ou neutralizar todas as ocorrências de PromptGuard::START e PromptGuard::END no conteúdo recebido antes de adicionar os delimitadores externos. Preserve a estrutura atual de quebra de linhas e o contrato de retorno, garantindo que o conteúdo não consiga forjar o fechamento da seção.app/Services/Ai/SchemaSnapshotService.php-49-62 (1)
49-62: 🔒 Security & Privacy | 🟠 Major | ⚡ Quick winMascarar colunas sensíveis no snapshot do schema
EXCLUDED_TABLESsó remove tabelas inteiras; o snapshot ainda enumera todas as colunas das tabelas incluídas, entãousers.passwordeusers.remember_tokencontinuam indo para o prompt da IA. Vale aplicar exclusão/mascaramento por coluna para campos sensíveis, em vez de depender só da exclusão por tabela.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@app/Services/Ai/SchemaSnapshotService.php` around lines 49 - 62, Atualize o fluxo de geração do snapshot em `SchemaSnapshotService` para excluir ou mascarar colunas sensíveis, incluindo `users.password` e `users.remember_token`, antes de montar os dados enviados ao prompt da IA. Preserve `EXCLUDED_TABLES` para tabelas inteiras e centralize a regra de colunas sensíveis em uma configuração ou método reutilizável.app/Filament/App/Resources/TaskResource.php-259-277 (1)
259-277: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick winChamadas de IA síncronas sem tratamento de erro em toda a UI de tarefas. Todas essas ações executam o CLI de IA no thread da requisição (o job
GenerateClientAnalysisreserva 420s para o mesmo tipo de chamada) e nenhuma trata exceção, então falha/lentidão do CLI vira erro na tela.
app/Filament/App/Resources/TaskResource.php#L259-L277: envolvasuggestDescription()emtry/catchcom notificação de erro.app/Filament/App/Resources/TaskResource.php#L1184-L1186: protejasummaryForClient()nofillForme retorne texto vazio com aviso em caso de falha.app/Filament/App/Resources/TaskResource.php#L1204-L1207: protejadraftClientMessage()da mesma forma.app/Filament/App/Resources/TaskResource.php#L1242-L1249: protejasuggestSubtasks()e trate lista vazia.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@app/Filament/App/Resources/TaskResource.php` around lines 259 - 277, Protect every synchronous AI call in TaskResource.php: in lines 259-277, wrap suggestDescription() with exception handling and show an error notification; in lines 1184-1186, wrap summaryForClient() in fillForm, return empty text on failure, and notify the user; in lines 1204-1207, apply the same handling to draftClientMessage(); and in lines 1242-1249, protect suggestSubtasks(), notify on failure, and handle an empty returned list. Use the existing action, fillForm, and notification patterns without changing successful results.app/Filament/App/Resources/TaskResource.php-1094-1101 (1)
1094-1101: 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win
status_idsemrequired()permite gravar nulo.Submeter o modal sem selecionar nada chama
update(['status_id' => null]), que quebra a FK/coluna obrigatória.🛡️ Correção sugerida
->form([ Select::make('status_id') ->label('Status') + ->required() ->options(fn (): array => Status::all()->pluck('name', 'id')->toArray()), ]),🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@app/Filament/App/Resources/TaskResource.php` around lines 1094 - 1101, Atualize o Select::make('status_id') no formulário da ação para torná-lo obrigatório usando required(), impedindo o envio do modal sem um status selecionado e evitando que o callback de update grave status_id como null.app/Filament/App/Resources/TaskResource.php-824-847 (1)
824-847: 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick winExtração de índice frágil e sem validação da chave.
O laço pega o primeiro segmento numérico do state path; se o path ganhar qualquer prefixo numérico (ação montada, infolist aninhada de subtarefa na Linha 898), o índice resolvido aponta para o item errado e sobrescreve outro checklist. Além disso,
$checklist[$index]é gravado sem checar se a chave existe, criando entradas espúrias em estado dessincronizado.🐛 Correção sugerida
- $parts = explode('.', $statePath); - $index = null; - foreach ($parts as $part) { - if (is_numeric($part)) { - $index = (int) $part; - break; - } - } - - if ($index !== null) { - $checklist = $record->checklist; + $numericParts = array_filter(explode('.', $statePath), 'is_numeric'); + $index = $numericParts === [] ? null : (int) end($numericParts); + + $checklist = $record->checklist ?? []; + + if ($index !== null && array_key_exists($index, $checklist)) { $checklist[$index]['completed'] = ! ($checklist[$index]['completed'] ?? false);🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@app/Filament/App/Resources/TaskResource.php` around lines 824 - 847, Atualize a closure de action do checklist para resolver o índice do item a partir do segmento correto do state path, sem assumir que o primeiro segmento numérico representa o checklist. Antes de alterar $checklist[$index], valide que a chave existe; para índices ausentes ou inválidos, não grave alterações nem crie entradas espúrias.app/Jobs/GenerateClientAnalysis.php-42-56 (1)
42-56: 🔒 Security & Privacy | 🟠 Major | ⚡ Quick winRestaure o Auth no fim do job
Auth::onceUsingId()deixa o usuário preso no guard do worker. Sem umfinallypara limpar o estado, o próximo job no mesmo processo pode herdar esse contexto e operar no tenant errado. UseAuth::forgetUser()(ouAuth::setUser(null)) ao final.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@app/Jobs/GenerateClientAnalysis.php` around lines 42 - 56, Atualize o job em torno de Auth::onceUsingId() para sempre limpar o usuário autenticado ao final da execução, incluindo quando service->generate() lançar uma exceção. Use um bloco finally com Auth::forgetUser() (ou Auth::setUser(null)), preservando o relatório, a notificação e o retorno existentes no catch.app/Console/Commands/AiBriefing.php-55-79 (1)
55-79: 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win
briefCompany()precisa limpar o guard entre empresas
Auth::onceUsingId()permanece no guard durante o mesmo processo; quando uma empresa semuser_idvem depois de outra comuser_id, ela herda o usuário anterior e oPejotaHelperusa timezone/formato/locale errados. Limpe o guard antes de resolver o owner e adicione um teste cobrindo duas empresas no mesmoCompany::all().🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@app/Console/Commands/AiBriefing.php` around lines 55 - 79, Atualize briefCompany() para limpar o guard de autenticação antes de resolver o owner da empresa, evitando que empresas sem user_id herdem o usuário anterior; mantenha o login via Auth::onceUsingId() apenas quando houver owner. Adicione um teste que processe duas empresas no mesmo Company::all(), sendo a segunda sem user_id, e valide que o contexto da segunda usa as configurações corretas.app/Services/Ai/AssistantWhatsappMediaIngestor.php-101-134 (1)
101-134: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick winCatch restrito demais em
ingest()para uma chamada disparada por webhook externo.Só
InvalidArgumentExceptioneRuntimeExceptionsão capturadas. Qualquer outroThrowabledepersistAttachment()/storeTemporaryAudio()(ex.: falha de disco, erro inesperado do uploader) propaga sem tratamento até o chamador do webhook, que — pelo trecho deAssistantWhatsappWebhookHandler::handleMessagefornecido como contexto — não envolve essa chamada em try/catch. Isso pode interromper o processamento do webhook com a mensagem do usuário já persistida e sem resposta.AssistantAttachmentProcessor::processAll, na mesma camada, já capturaThrowablede forma ampla — vale alinhar este método ao mesmo padrão.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@app/Services/Ai/AssistantWhatsappMediaIngestor.php` around lines 101 - 134, Amplie o tratamento de exceções em AssistantWhatsappMediaIngestor::ingest para capturar qualquer Throwable lançado por storeTemporaryAudio ou persistAttachment, mantendo o retorno atual com kind, audio_path nulo e a mensagem da exceção no campo error; alinhe o método ao padrão amplo usado por AssistantAttachmentProcessor::processAll.app/Services/Documents/AttachmentTextExtractor.php-43-81 (1)
43-81: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win
docx()/xlsx()carregam conteúdo do ZIP sem limite de tamanho descomprimido ("zip bomb").
ZipArchive::getFromName('word/document.xml'),getFromName('xl/sharedStrings.xml')e a leitura de cadaxl/worksheets/sheetN.xmlcarregam o conteúdo descomprimido inteiro em memória sem nenhum teto. Um DOCX/XLSX malicioso dentro do limite de 25MB (comprimido) pode se expandir para muito mais que isso, o que é especialmente arriscado no VPS de uma CPU e ~4GB de RAM sem swap descrito no plano de deploy. Considere checarstatName($entry)['size'](ou usargetStream()com leitura limitada) antes degetFromName()e rejeitar/truncar entradas anormalmente grandes.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@app/Services/Documents/AttachmentTextExtractor.php` around lines 43 - 81, Adicione um limite explícito para o tamanho descomprimido das entradas ZIP antes de carregá-las em memória nos métodos docx() e xlsx(). Use ZipArchive::statName() para validar word/document.xml, xl/sharedStrings.xml e cada xl/worksheets/sheetN.xml, rejeitando ou truncando entradas acima do limite definido; preserve o processamento normal das entradas dentro do limite e garanta o fechamento do ZIP em todos os caminhos.app/Http/Controllers/AttachmentsController.php-25-41 (1)
25-41: 🔒 Security & Privacy | 🟠 Major | ⚡ Quick winEndpoint de anexos do WhatsApp sem
X-Content-Type-Options: nosniff.
getAssistantAttachmentprotege contra MIME sniffing e usaContent-Dispositionseguro, masgetWhatsappAttachmentnão. Omime_typede umWhatsappAttachmentvem de metadados reportados pelo remetente/Evolution API (fonte externa), então é o caso mais exposto a MIME spoofing (ex.: um contato mal-intencionado enviando conteúdo HTML commime_typede imagem, sendo então "sniffado" e renderizado pelo navegador). Alinhe este método ao padrão já usado no endpoint do assistente.🛡️ Sugestão de correção
return response()->file($disk->path($attachment->path), [ 'Accept-Ranges' => 'bytes', 'Content-Type' => $this->contentType($attachment), + 'X-Content-Type-Options' => 'nosniff', ]);🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@app/Http/Controllers/AttachmentsController.php` around lines 25 - 41, Update getWhatsappAttachment to match the security headers and safe Content-Disposition behavior used by getAssistantAttachment: add X-Content-Type-Options: nosniff and use the established safe attachment filename/disposition handling while preserving the existing authorization and file validation.app/Services/Ai/AssistantQuickAnswers.php-110-120 (1)
110-120: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick winTarefas com vencimento hoje aparecem como "atrasadas por 0 dia(s)".
$task->due_dateé uma data cast na timezone da aplicação (00:00), enquanto$todayé meia-noite na timezone do usuário; quando as duas diferem (ex.:America/Sao_Paulovs UTC),lt()é verdadeiro para a data de hoje e a linha cai no ramo de atraso comdiffInDays= 0. Comparar apenas as datas civis elimina a dependência de fuso.🐛 Sugestão
- if ($task->due_date->lt($today)) { - $days = (int) $task->due_date->diffInDays($today); + if ($task->due_date->toDateString() < $today->toDateString()) { + $days = (int) $task->due_date->startOfDay()->diffInDays($today->startOfDay());🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@app/Services/Ai/AssistantQuickAnswers.php` around lines 110 - 120, Atualize o callback de mapeamento em AssistantQuickAnswers para comparar apenas as datas civis de due_date e today, sem considerar horário ou timezone, antes de decidir entre os ramos de atraso e vencimento hoje. Preserve o cálculo de dias para tarefas realmente atrasadas e faça com que a data de hoje siga sempre o texto “due today”.app/Services/Evolution/AssistantWhatsappWebhookHandler.php-378-392 (1)
378-392: 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick winEvite o fallback implícito para a primeira empresa
Company::query()->value('id')pega a primeira empresa da tabela quandoEVOLUTION_DEFAULT_COMPANY_IDestá vazio; em ambiente com mais de um tenant, isso pode atribuir mensagens ao tenant errado. Se a configuração for obrigatória, falhe ou registre erro explícito em vez de escolher uma empresa arbitrária.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@app/Services/Evolution/AssistantWhatsappWebhookHandler.php` around lines 378 - 392, Update companyId() to remove the implicit Company::query()->value('id') fallback when services.evolution.default_company_id is missing; treat the configuration as required and fail explicitly or log an explicit error instead of selecting an arbitrary company, while preserving the configured ID path.app/Services/Evolution/EvolutionWebhookHandler.php-159-176 (1)
159-176: 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick winDocumentos do WhatsApp enviados com legenda nunca geram anexo.
messageType()classificadocumentWithCaptionMessagecomodocument_with_caption(via o loop genérico de sufixo*Message), masisMediaType()só aceita['audio', 'image', 'video', 'document', 'sticker']. Ao mesmo tempo,mediaInfo()só inspecionaaudioMessage/imageMessage/videoMessage/documentMessage/stickerMessagediretamente sobmessage, nunca desembrulhandodocumentWithCaptionMessage.message.documentMessage. Assim, emstoreAttachment()a condição$media === null && ! $this->isMediaType($message->message_type)é verdadeira para esse tipo de mensagem e a função retorna sem criar nenhum anexo — o arquivo é perdido, apenas a legenda é salva como texto.A própria
messageText()já desembrulha esse caminho aninhado (message.documentWithCaptionMessage.message.documentMessage.caption), confirmando que esse formato ocorre de fato nos payloads reais do Evolution/Baileys.
base64()emessagePayload()têm a mesma lacuna: nenhuma delas verificamessage.documentWithCaptionMessage.message.documentMessage.base64, então, se esse base64 aparecer ali, ele não seria extraído para um arquivo nem removido dopayloadpersistido.🐛 Correção proposta para mediaInfo() (aplicar o mesmo padrão em base64() e messagePayload())
private function mediaInfo(array $messageData): ?array { $message = data_get($messageData, 'message', []); if (! is_array($message)) { return null; } + // "documentWithCaptionMessage" aninha o documentMessage real um + // nível mais profundo; messageText() já desembrulha isso para a + // legenda, mas este método nunca fez o mesmo, então documentos com + // legenda eram silenciosamente tratados como "sem mídia" abaixo. + $captioned = data_get($message, 'documentWithCaptionMessage.message.documentMessage'); + if (is_array($captioned)) { + $mime = data_get($captioned, 'mimetype'); + $filename = data_get($captioned, 'fileName'); + + return [ + 'type' => 'document', + 'mime_type' => is_string($mime) ? $mime : null, + 'filename' => is_string($filename) ? $filename : null, + 'extension' => is_string($filename) ? pathinfo($filename, PATHINFO_EXTENSION) : $this->extensionFromMime($mime), + ]; + } + foreach (['audioMessage', 'imageMessage', 'videoMessage', 'documentMessage', 'stickerMessage'] as $key) { $media = data_get($message, $key); ...private function base64(array $messageData): ?array { $raw = data_get($messageData, 'message.base64') ?: data_get($messageData, 'base64') ?: data_get($messageData, 'message.audioMessage.base64') ?: data_get($messageData, 'message.imageMessage.base64') ?: data_get($messageData, 'message.videoMessage.base64') - ?: data_get($messageData, 'message.documentMessage.base64'); + ?: data_get($messageData, 'message.documentMessage.base64') + ?: data_get($messageData, 'message.documentWithCaptionMessage.message.documentMessage.base64');Also applies to: 431-450, 452-472, 474-497, 499-509, 549-552
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@app/Services/Evolution/EvolutionWebhookHandler.php` around lines 159 - 176, Extend the document-media handling in mediaInfo(), base64(), and messagePayload() to unwrap message.documentWithCaptionMessage.message.documentMessage and process its media fields exactly like the existing direct documentMessage path. Ensure mediaInfo() returns the nested document metadata, base64() extracts its base64 content, and messagePayload() removes nested base64 before persisting the payload, while preserving current handling for other message types.app/Services/Evolution/EvolutionWebhookHandler.php-32-68 (1)
32-68: 🩺 Stability & Availability | 🟠 Major | 🏗️ Heavy liftTrabalho pesado e bloqueante executado de forma síncrona na requisição do webhook do Evolution. O controller (ver snippet de
EvolutionWebhookController) chamaEvolutionWebhookHandler::handle()eEvolutionWebhookForwarder::forward()sequencialmente, ambos de forma síncrona, na mesma requisição HTTP que responde ao webhook do Evolution API — sem despachar nada para fila.
app/Services/Evolution/EvolutionWebhookHandler.php#L32-L68:handle()roda com$withMedia=truepor padrão (download de mídia + CLIs de IA para transcrição/descrição, que podem levar minutos conforme documentado emProcessAssistantMessage.php), mesmo sendo o próprio docblock (linhas 24-31) que alerta contra isso em "caminhos vinculados a requisições"; considere despachar um job para o processamento de mídia/enriquecimento e responder ao webhook rapidamente com$withMedia=false.app/Services/Evolution/EvolutionWebhookForwarder.php#L10-L25:forward()adiciona até 8s de chamada HTTP bloqueante na mesma requisição; considere despachar via job/queue em vez de chamarHttp::post()de forma síncrona no fluxo do webhook.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@app/Services/Evolution/EvolutionWebhookHandler.php` around lines 32 - 68, Make EvolutionWebhookHandler::handle() return quickly from the webhook path by processing the initial message with $withMedia=false and dispatching media/enrichment work to a queue job instead of performing it synchronously. In app/Services/Evolution/EvolutionWebhookHandler.php lines 32-68, preserve message persistence and token behavior while moving heavy processing out of the request. In app/Services/Evolution/EvolutionWebhookForwarder.php lines 10-25, replace the synchronous Http::post() in forward() with queued dispatch so forwarding no longer blocks the webhook response.app/Services/Evolution/EvolutionApiClient.php-23-43 (1)
23-43: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick winTratar
ConnectionExceptionnos clientes HTTP da Evolution API
Todos os métodos desta classe capturam sóRequestException. Timeout/DNS/conexão caem emConnectionException, entãofetchAllGroups()ainda pode estourar a tela em vez de retornar[], e os demais métodos deixam de retornar aRuntimeExceptionpadronizada. IncluaConnectionExceptionnocatche trate a mensagem conforme o tipo da exceção.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@app/Services/Evolution/EvolutionApiClient.php` around lines 23 - 43, Atualize os métodos da classe EvolutionApiClient, incluindo sendTextToNumber e fetchAllGroups, para capturar também ConnectionException além de RequestException. Preserve a mensagem do corpo da resposta para RequestException e use a mensagem da exceção de conexão para ConnectionException, mantendo o retorno [] de fetchAllGroups e a RuntimeException padronizada nos demais métodos.app/Services/Evolution/WhatsappConversationSyncService.php-113-116 (1)
113-116: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win
syncAll()perdeu o filtrois_array()quesync()aplica.Em
sync()(linhas 49-53) os registros passam por->filter(fn ($record) => is_array($record))antes dosortBy(fn (array $record) ...). Aqui não: qualquer item não-array vindo defindMessagesPage()(string,null) provocaTypeErrorno callback tipado — e também emmessageTimestamp(array $record)— abortando o backfill completo no meio da paginação.🐛 Correção proposta
$records = collect($page['records']) + ->filter(fn ($record) => is_array($record)) ->sortBy(fn (array $record): int => $this->messageTimestamp($record)) ->values() ->all();🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@app/Services/Evolution/WhatsappConversationSyncService.php` around lines 113 - 116, Adicione ao fluxo de `syncAll()` no processamento de `$page['records']` o mesmo filtro `is_array()` usado por `sync()` antes de `sortBy` e de `messageTimestamp`. Preserve apenas registros array, mantendo a ordenação e a conversão final para lista existentes, para que itens inválidos não alcancem os callbacks tipados nem interrompam o backfill.app/Filament/App/Resources/WhatsappConversationResource/RelationManagers/MessagesRelationManager.php-96-97 (1)
96-97: 🚀 Performance & Scalability | 🟠 Major | ⚡ Quick winTabela sem paginação carrega o histórico inteiro.
paginated(false)combinado comwith('attachments')traz todas as mensagens da conversa em cada render/poll. ComoSyncWhatsappConversationHistoryimporta o histórico completo, conversas antigas podem render centenas/milhares de registros por requisição. Considere limitar a janela (ex.: últimas N mensagens viamodifyQueryUsing) ou usar paginação simples.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@app/Filament/App/Resources/WhatsappConversationResource/RelationManagers/MessagesRelationManager.php` around lines 96 - 97, Atualize a configuração da tabela em MessagesRelationManager para evitar carregar todo o histórico a cada renderização: remova paginated(false) e habilite paginação simples, ou aplique em modifyQueryUsing uma janela limitada às últimas N mensagens. Preserve o eager loading de attachments e garanta que o limite/paginação seja aplicado à consulta de mensagens.app/Filament/App/Resources/WhatsappConversationResource/RelationManagers/MessagesRelationManager.php-449-471 (1)
449-471: 🩺 Stability & Availability | 🟠 Major | 🏗️ Heavy liftChamadas de IA síncronas no request Livewire.
CliWhatsappMessageSuggester/CliWhatsappConversationQuestionAnswererexecutam CLI de IA no thread da requisição. Este mesmo PR assume que essas chamadas podem levar minutos (AnalyzeWhatsappConversation::$timeout = 420), o que tende a estourarmax_execution_time/timeout do proxy e devolver 504 ao usuário. Considere despachar em job e devolver o resultado via notificação/polling.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@app/Filament/App/Resources/WhatsappConversationResource/RelationManagers/MessagesRelationManager.php` around lines 449 - 471, Altere generateAiSuggestion para não executar CliWhatsappMessageSuggester::suggest de forma síncrona no request Livewire: despache um job assíncrono com a conversa e composerMessage, retorne imediatamente e informe o usuário de que o processamento foi iniciado. Preserve a atualização de composerMessage/aiSuggestion e as notificações de sucesso ou falha no fluxo assíncrono, usando polling ou outro mecanismo existente para disponibilizar o resultado.app/Http/Controllers/Webhooks/EvolutionWebhookController.php-53-62 (1)
53-62: 🔒 Security & Privacy | 🟠 Major | ⚡ Quick winVerificação de
apikeypode ser burlada omitindo o campo.Com
webhook_verify_api_keyativo eapi_keyconfigurada, um payload semapikey(ou com string vazia) passa direto, porque a comparação só ocorre quando$payloadKeyé string não vazia. Sewebhook_tokennão estiver configurado, o endpoint fica sem autenticação alguma. Quando a verificação está habilitada e a chave está configurada, a ausência do campo deveria ser rejeitada.🔒️ Correção proposta
$apiKey = config('services.evolution.api_key'); $payloadKey = $request->input('apikey'); - if (is_string($apiKey) && $apiKey !== '' && is_string($payloadKey) && $payloadKey !== '' && ! hash_equals($apiKey, $payloadKey)) { + if (! is_string($apiKey) || $apiKey === '') { + return; + } + + if (! is_string($payloadKey) || ! hash_equals($apiKey, $payloadKey)) { abort(Response::HTTP_UNAUTHORIZED); }🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@app/Http/Controllers/Webhooks/EvolutionWebhookController.php` around lines 53 - 62, Update the apikey validation in EvolutionWebhookController so that when webhook_verify_api_key is enabled and services.evolution.api_key is configured, a missing or empty request apikey is rejected with HTTP_UNAUTHORIZED. Preserve hash_equals validation for non-empty payload keys and the existing early return when verification is disabled.Source: Linters/SAST tools
app/Console/Commands/SendDailyPlans.php-54-58 (1)
54-58: 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick winMesmo problema de vazamento de autenticação entre empresas do arquivo
GenerateDailyPlans.php.
Auth::onceUsingIdsó é chamado se$company->user_idexistir; caso contrário, o usuário autenticado da iteração anterior permanece resolvido para esta empresa (mesmo processoforeach). Isso afetaPejotaHelper::getUserTimeZoneOrDefault()na linha 60, podendo calcular "hoje" com o fuso de outra empresa.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@app/Console/Commands/SendDailyPlans.php` around lines 54 - 58, Atualize sendForCompany para limpar o estado de autenticação antes de processar cada empresa sem user_id, evitando reutilizar o usuário da iteração anterior. Preserve Auth::onceUsingId para empresas com user_id e garanta que PejotaHelper::getUserTimeZoneOrDefault() execute sem usuário autenticado quando não houver vínculo.app/Services/Planner/DailyPlanResponseParser.php-83-135 (1)
83-135: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick winItens do tipo
contractnão têm verificação anti-hallucination equivalente à detask/invoice/habit.
normalizeItem()rejeita itensTASKsemtask_idválido (linhas 105-107),INVOICEseminvoice_ide semclient_id(linhas 109-111), eHABITsemtask_id(linhas 113-115). Não há verificação equivalente paraDailyPlanItemTypeEnum::CONTRACT: um item desse tipo é aceito mesmo comcontract_idnulo (e sem fallback porclient_id, como acontece cominvoice). Isso contraria o objetivo documentado da classe ("ids que nunca apareceram no contexto são removidos... anti-hallucination") e permite que a IA "invente" uma renovação de contrato sem nenhum contrato real referenciado.🔧 Correção sugerida
if ($type === DailyPlanItemTypeEnum::HABIT && $taskId === null) { return null; } + + if ($type === DailyPlanItemTypeEnum::CONTRACT && $contractId === null && $clientId === null) { + return null; + }🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@app/Services/Planner/DailyPlanResponseParser.php` around lines 83 - 135, Update normalizeItem() to reject DailyPlanItemTypeEnum::CONTRACT items when contractId is null, matching the existing validation for TASK, INVOICE, and HABIT. Keep valid contract-linked items unchanged and continue using the whitelisted contract ID from context->validContractIds.app/Console/Commands/GenerateDailyPlans.php-56-68 (1)
56-68: 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick winEstado de autenticação pode "vazar" de uma empresa para a próxima no mesmo processo.
Auth::onceUsingId($company->user_id)só é chamado quando$company->user_idexiste. Se a empresa anterior noforeach(Linha 49) tinhauser_ide a próxima não tem,auth()->user()continua resolvendo o usuário da empresa anterior durante todo o restante deplanCompany()para a nova empresa — inclusive emPejotaHelper::getUserTimeZoneOrDefault()(linhas 71-72), que decide o "hoje" usado paraplan_date. Isso pode gerar planos com data/fuso da empresa errada. Esse comportamento de "vazamento" de auth entre iterações no mesmo processo PHP é um problema documentado do Laravel quandoAuth::setUser()/onceUsingId()é chamado condicionalmente dentro de loops longos.🔧 Correção sugerida
- if ($company->user_id) { - Auth::onceUsingId($company->user_id); - } + if ($company->user_id) { + Auth::onceUsingId($company->user_id); + } else { + Auth::forgetGuards(); + }Ligado ao mesmo problema em
app/Console/Commands/SendDailyPlans.php(linhas 56-58) — ver comentário consolidado.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@app/Console/Commands/GenerateDailyPlans.php` around lines 56 - 68, Atualize o método planCompany para limpar o estado de autenticação antes de processar cada empresa, garantindo que empresas sem user_id não reutilizem o usuário autenticado pela iteração anterior. Preserve a autenticação via Auth::onceUsingId quando user_id existir e assegure que auth()->user() e PejotaHelper::getUserTimeZoneOrDefault() reflitam somente a empresa atual.app/Jobs/GenerateDailyPlan.php-56-66 (1)
56-66: 🔒 Security & Privacy | 🟠 Major | ⚡ Quick winMensagem de exceção crua exposta ao usuário final.
$exception?->getMessage()é persistido emfailure_reasone exibido diretamente na página "Plan of the day" (verPlanOfTheDayPageTest::test_failed_plan_shows_the_failure_reason, que fazassertSee('CLI indisponível')). Exceções de chamadas de CLI/API externas podem conter detalhes internos sensíveis (tokens, caminhos, respostas de terceiros) que não deveriam ir para a UI sem sanitização; o detalhe completo deveria ser logado (report()/Log::error()) e um texto genérico exibido ao usuário.🔒 Correção sugerida
public function failed(?Throwable $exception): void { + if ($exception) { + report($exception); + } + DailyPlan::allTenants() ->where('company_id', $this->company->id) ->whereDate('plan_date', $this->date) ->where('status', DailyPlanStatusEnum::GENERATING->value) ->update([ 'status' => DailyPlanStatusEnum::FAILED->value, - 'failure_reason' => $exception?->getMessage() ?? __('The plan generation job failed.'), + 'failure_reason' => __('The plan generation job failed.'), ]); }🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@app/Jobs/GenerateDailyPlan.php` around lines 56 - 66, Update GenerateDailyPlan::failed so failure_reason never persists the raw exception message: report or log the full Throwable for diagnostics, then store a generic user-facing failure message instead. Preserve the existing fallback wording and ensure the failed plan UI no longer receives sensitive CLI/API exception details.app/Filament/App/Pages/PlanOfTheDay.php-147-150 (1)
147-150: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick winTimezone helper inconsistente com o resto da feature de Daily Plan.
today()usaPejotaHelper::getUserTimeZone(), enquantoDailyPlanOverview::todayPlan()e o comandoSendDailyPlansusamPejotaHelper::getUserTimeZoneOrDefault()para o mesmo cálculo de "hoje". Se o usuário não tiver timezone configurado, essa divergência pode lançar exceção aqui e quebrar toda a página (computedplan()eworkedTodayMinutes()dependem detoday()), enquanto o widget e o comando continuam funcionando com o fallback.🛠️ Correção sugerida
private function today(): CarbonImmutable { - return CarbonImmutable::now(PejotaHelper::getUserTimeZone())->startOfDay(); + return CarbonImmutable::now(PejotaHelper::getUserTimeZoneOrDefault())->startOfDay(); }🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@app/Filament/App/Pages/PlanOfTheDay.php` around lines 147 - 150, Atualize o método today() em PlanOfTheDay para usar PejotaHelper::getUserTimeZoneOrDefault(), mantendo o cálculo com CarbonImmutable::now(...)->startOfDay(). Alinhe o comportamento com DailyPlanOverview::todayPlan() e SendDailyPlans, preservando o fallback quando o usuário não tiver timezone configurado.
| public function score(string $left, string $right): int | ||
| { | ||
| $left = $this->digits($left); | ||
| $right = $this->digits($right); | ||
|
|
||
| if ($left === '' || $right === '') { | ||
| return 0; | ||
| } | ||
|
|
||
| if ($left === $right) { | ||
| return 100; | ||
| } | ||
|
|
||
| if (str_ends_with($left, $right) || str_ends_with($right, $left)) { | ||
| return 92; | ||
| } | ||
|
|
||
| if (substr($left, -9) === substr($right, -9)) { | ||
| return 88; | ||
| } | ||
|
|
||
| if (substr($left, -8) === substr($right, -8)) { | ||
| return 80; | ||
| } | ||
|
|
||
| similar_text($left, $right, $percent); | ||
|
|
||
| return (int) round($percent); | ||
| } |
There was a problem hiding this comment.
🔒 Security & Privacy | 🔴 Critical | ⚡ Quick win
Heurística de sufixo casa números curtos/parciais e pode vincular a conversa ao cliente errado.
Sem exigência de comprimento mínimo, um telefone parcial casa com alta pontuação:
score('1490', '5554999371490')→str_ends_with→ 92substr('1490', -9) === '1490'esubstr('1490', -8) === '1490', logo até os ramos de 9/8 dígitos degeneram para comparação da string inteira quando um lado é mais curto.similar_textsobre dígitos ultrapassa 70 com facilidade entre dois celulares do mesmo DDD.
Como linkClient()/linkConversation() gravam client_id via forceFill()->save() sem confirmação humana, um falso positivo reatribui a conversa e expõe o histórico de WhatsApp de um cliente a outro.
🔒️ Correção sugerida
public function score(string $left, string $right): int
{
$left = $this->digits($left);
$right = $this->digits($right);
if ($left === '' || $right === '') {
return 0;
}
if ($left === $right) {
return 100;
}
+ // Sufixos só são confiáveis com número suficientemente longo em ambos
+ // os lados; abaixo disso qualquer fragmento casaria com muitos números.
+ if (strlen($left) < 8 || strlen($right) < 8) {
+ return 0;
+ }
+
if (str_ends_with($left, $right) || str_ends_with($right, $left)) {
return 92;
}
if (substr($left, -9) === substr($right, -9)) {
return 88;
}
if (substr($left, -8) === substr($right, -8)) {
return 80;
}
- similar_text($left, $right, $percent);
-
- return (int) round($percent);
+ // Semelhança textual entre dígitos não é evidência de mesmo número.
+ return 0;
}📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| public function score(string $left, string $right): int | |
| { | |
| $left = $this->digits($left); | |
| $right = $this->digits($right); | |
| if ($left === '' || $right === '') { | |
| return 0; | |
| } | |
| if ($left === $right) { | |
| return 100; | |
| } | |
| if (str_ends_with($left, $right) || str_ends_with($right, $left)) { | |
| return 92; | |
| } | |
| if (substr($left, -9) === substr($right, -9)) { | |
| return 88; | |
| } | |
| if (substr($left, -8) === substr($right, -8)) { | |
| return 80; | |
| } | |
| similar_text($left, $right, $percent); | |
| return (int) round($percent); | |
| } | |
| public function score(string $left, string $right): int | |
| { | |
| $left = $this->digits($left); | |
| $right = $this->digits($right); | |
| if ($left === '' || $right === '') { | |
| return 0; | |
| } | |
| if ($left === $right) { | |
| return 100; | |
| } | |
| // Sufixos só são confiáveis com número suficientemente longo em ambos | |
| // os lados; abaixo disso qualquer fragmento casaria com muitos números. | |
| if (strlen($left) < 8 || strlen($right) < 8) { | |
| return 0; | |
| } | |
| if (str_ends_with($left, $right) || str_ends_with($right, $left)) { | |
| return 92; | |
| } | |
| if (substr($left, -9) === substr($right, -9)) { | |
| return 88; | |
| } | |
| if (substr($left, -8) === substr($right, -8)) { | |
| return 80; | |
| } | |
| // Semelhança textual entre dígitos não é evidência de mesmo número. | |
| return 0; | |
| } |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@app/Services/Evolution/WhatsappConversationMatcher.php` around lines 80 -
108, Atualize o método WhatsappConversationMatcher::score para impedir
pontuações de correspondência de sufixo ou similaridade para números
curtos/parciais: exija um comprimento mínimo adequado antes dessas heurísticas e
retorne 0 quando qualquer número não atender ao limite. Preserve a pontuação de
igualdade exata apenas quando aplicável e mantenha linkClient/linkConversation
inalterados.
| GRAFANA_URL = "https://grafana.topmassagens.com.br/api/ds/query?ds_type=mysql" | ||
| DATASOURCE_UID = "dfpqllglhd88wf" | ||
| AUTH_USER = "timetop" | ||
| AUTH_PASS = "77308515" |
There was a problem hiding this comment.
🔒 Security & Privacy | 🔴 Critical | ⚡ Quick win
Credenciais em texto puro no código-fonte.
AUTH_USER/AUTH_PASS (e a URL/UID de produção) estão hardcoded e serão versionados no git. Isso expõe credenciais reais do Grafana/Top Massagens a qualquer pessoa com acesso ao repositório.
🔒️ Proposta de correção usando variáveis de ambiente
-GRAFANA_URL = "https://grafana.topmassagens.com.br/api/ds/query?ds_type=mysql"
-DATASOURCE_UID = "dfpqllglhd88wf"
-AUTH_USER = "timetop"
-AUTH_PASS = "77308515"
+GRAFANA_URL = os.environ["GRAFANA_URL"]
+DATASOURCE_UID = os.environ["GRAFANA_DATASOURCE_UID"]
+AUTH_USER = os.environ["GRAFANA_AUTH_USER"]
+AUTH_PASS = os.environ["GRAFANA_AUTH_PASS"]Recomendo também revogar/rotacionar essa senha, já que foi exposta.
📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| GRAFANA_URL = "https://grafana.topmassagens.com.br/api/ds/query?ds_type=mysql" | |
| DATASOURCE_UID = "dfpqllglhd88wf" | |
| AUTH_USER = "timetop" | |
| AUTH_PASS = "77308515" | |
| GRAFANA_URL = os.environ["GRAFANA_URL"] | |
| DATASOURCE_UID = os.environ["GRAFANA_DATASOURCE_UID"] | |
| AUTH_USER = os.environ["GRAFANA_AUTH_USER"] | |
| AUTH_PASS = os.environ["GRAFANA_AUTH_PASS"] |
🧰 Tools
🪛 Ruff (0.15.21)
[error] 14-14: Possible hardcoded password assigned to: "AUTH_PASS"
(S105)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@export_emails.py` around lines 11 - 14, Remova as credenciais e configurações
de produção hardcoded em GRAFANA_URL, DATASOURCE_UID, AUTH_USER e AUTH_PASS;
carregue esses valores por variáveis de ambiente, validando sua presença antes
de executar as requisições. Atualize o fluxo de exportação para usar os valores
carregados e revogue/rotacione a senha atualmente exposta.
Source: Linters/SAST tools
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@tests/Feature/Planner/GenerateDailyPlanJobTest.php`:
- Around line 30-35: Atualize o matcher de generate no teste para também validar
que $mode é DailyPlanModeEnum::FULL e que $date representa 00:00:00, preservando
as verificações existentes de data e timezone; isso deve eliminar os parâmetros
não utilizados apontados pelo PHPMD.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro Plus
Run ID: e742b50e-28b3-4a60-b801-fffe02938543
📒 Files selected for processing (2)
app/Jobs/GenerateDailyPlan.phptests/Feature/Planner/GenerateDailyPlanJobTest.php
🚧 Files skipped from review as they are similar to previous changes (1)
- app/Jobs/GenerateDailyPlan.php
| $generator->shouldReceive('generate') | ||
| ->once() | ||
| ->withArgs(function (Company $company, CarbonImmutable $date, DailyPlanModeEnum $mode): bool { | ||
| return $date->toDateString() === '2026-07-25' | ||
| && $date->timezone->getName() === 'America/Sao_Paulo'; | ||
| }) |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
Valide o modo e o início do dia no matcher
O teste verifica apenas a data e o timezone. Uma regressão que envie DailyPlanModeEnum::LIGHT ou uma data que não esteja em 00:00:00 continuaria passando, apesar de o job dever encaminhar FULL e aplicar startOfDay(). Isso também explica os parâmetros não utilizados apontados pelo PHPMD.
Correção sugerida
- ->withArgs(function (Company $company, CarbonImmutable $date, DailyPlanModeEnum $mode): bool {
- return $date->toDateString() === '2026-07-25'
- && $date->timezone->getName() === 'America/Sao_Paulo';
+ ->withArgs(function (Company $company, CarbonImmutable $date, DailyPlanModeEnum $mode): bool {
+ return $date->toDateString() === '2026-07-25'
+ && $date->format('H:i:s') === '00:00:00'
+ && $date->timezone->getName() === 'America/Sao_Paulo'
+ && $mode === DailyPlanModeEnum::FULL;
})📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| $generator->shouldReceive('generate') | |
| ->once() | |
| ->withArgs(function (Company $company, CarbonImmutable $date, DailyPlanModeEnum $mode): bool { | |
| return $date->toDateString() === '2026-07-25' | |
| && $date->timezone->getName() === 'America/Sao_Paulo'; | |
| }) | |
| $generator->shouldReceive('generate') | |
| ->once() | |
| ->withArgs(function (Company $company, CarbonImmutable $date, DailyPlanModeEnum $mode): bool { | |
| return $date->toDateString() === '2026-07-25' | |
| && $date->format('H:i:s') === '00:00:00' | |
| && $date->timezone->getName() === 'America/Sao_Paulo' | |
| && $mode === DailyPlanModeEnum::FULL; | |
| }) |
🧰 Tools
🪛 PHPMD (2.15.0)
[warning] 32-32: Avoid unused parameters such as '$company'. (undefined)
(UnusedFormalParameter)
[warning] 32-32: Avoid unused parameters such as '$mode'. (undefined)
(UnusedFormalParameter)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@tests/Feature/Planner/GenerateDailyPlanJobTest.php` around lines 30 - 35,
Atualize o matcher de generate no teste para também validar que $mode é
DailyPlanModeEnum::FULL e que $date representa 00:00:00, preservando as
verificações existentes de data e timezone; isso deve eliminar os parâmetros não
utilizados apontados pelo PHPMD.
Source: Linters/SAST tools
…esposta e follow-up
… tempo restante e implementar testes correspondentes
There was a problem hiding this comment.
Actionable comments posted: 2
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (4)
app/Services/Planner/DailyPlanPromptBuilder.php (2)
40-40: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick winValide no parser o
reasonobrigatório para follow-ups.A regra exige um trecho da última mensagem, mas
DailyPlanResponseParserapenas limita o campo e aceitanullou texto sem citação. Uma resposta inválida da IA ainda vira um item sem evidência; rejeite ou sinalize esse caso.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@app/Services/Planner/DailyPlanPromptBuilder.php` at line 40, Atualize o DailyPlanResponseParser para validar o reason obrigatório em itens de follow_up e responder cliente, rejeitando ou sinalizando valores nulos, vazios ou sem um trecho entre aspas da última mensagem relevante. Preserve a validação existente de tamanho e permita a exceção descrita para anexos sem texto, desde que o reason indique explicitamente essa situação.
44-46: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick winNão trate todo
LIGHTcomo dia de folga.
DailyPlanGeneratortambém mudaFULLparaLIGHTquando a capacidade restante chega a zero em um dia útil. Esta regra pode fazer a IA afirmar que é folga e produzir um resumo incorreto. Diferencie “dia sem expediente” de “capacidade já esgotada”.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@app/Services/Planner/DailyPlanPromptBuilder.php` around lines 44 - 46, Atualize a regra adicionada em DailyPlanPromptBuilder para identificar explicitamente quando o dia não tem expediente, em vez de aplicá-la a todo DailyPlanModeEnum::LIGHT. Preserve um comportamento separado para dias úteis cuja capacidade foi esgotada por DailyPlanGenerator, evitando que a IA os descreva como folga ou gere um resumo incorreto.app/Services/Planner/DailyPlanContextBuilder.php (2)
47-55: 🚀 Performance & Scalability | 🟠 Major | 🏗️ Heavy liftGaranta que
context_max_charsseja um limite efetivo.Se as três versões ainda excederem o limite,
build()retorna o último contexto mesmo assim. Isso pode ultrapassar limites de tokens do CLI e torna a degradação ineficaz. A etapa final deve remover/truncar seções de menor prioridade ou falhar explicitamente.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@app/Services/Planner/DailyPlanContextBuilder.php` around lines 47 - 55, Atualize o método build(), no fluxo após iterar por todas as degradações, para garantir que o contexto retornado nunca exceda context_max_chars. Se nenhum contexto couber, remova ou trunque as seções de menor prioridade na etapa final; caso isso não seja possível, falhe explicitamente em vez de retornar o último contexto excedente.
109-114: 🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy liftAlinhe as whitelists aos IDs realmente presentes no contexto.
Os marcadores de tarefas exibem
conversation_idvindo de todas as conversas, masvalidConversationIdsusa apenas o recorte recente; o parser pode remover um ID que o modelo acabou de receber. Além disso,validClientIdsaceita qualquer cliente da empresa, mesmo sem aparecer no texto, permitindo itens sem evidência contextual. Gere as whitelists a partir dos registros renderizados.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@app/Services/Planner/DailyPlanContextBuilder.php` around lines 109 - 114, Atualize a construção de validConversationIds e validClientIds no contexto para derivá-las dos registros efetivamente renderizados, mantendo apenas IDs presentes no texto enviado ao modelo. Não use o recorte recente de $conversations nem todos os clientes retornados por Client::allTenants() como fonte das whitelists.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@app/Services/Planner/DailyPlanPromptBuilder.php`:
- Line 34: Unifique o limiar de silêncio entre o prompt e os marcadores: em
app/Services/Planner/DailyPlanPromptBuilder.php#L34-L34, derive a instrução do
mesmo limiar usado pelos marcadores; em
app/Services/Planner/DailyPlanContextBuilder.php#L228-L228, calcule esse limiar
a partir da configuração única escolhida; e em
app/Services/Planner/DailyPlanContextBuilder.php#L285-L288, aplique exatamente a
mesma fronteira ao gerar os marcadores.
- Line 33: Centralize the effective planning budget in the parser rather than
relying only on the prompt text: update the parser’s capacity validation to
reserve approximately 15% of the remaining capacity and reject plans exceeding
that budget, including follow_up and habit items currently allowed up to 120%.
Add or update tests covering the reserved-capacity boundary and over-budget
plans.
---
Outside diff comments:
In `@app/Services/Planner/DailyPlanContextBuilder.php`:
- Around line 47-55: Atualize o método build(), no fluxo após iterar por todas
as degradações, para garantir que o contexto retornado nunca exceda
context_max_chars. Se nenhum contexto couber, remova ou trunque as seções de
menor prioridade na etapa final; caso isso não seja possível, falhe
explicitamente em vez de retornar o último contexto excedente.
- Around line 109-114: Atualize a construção de validConversationIds e
validClientIds no contexto para derivá-las dos registros efetivamente
renderizados, mantendo apenas IDs presentes no texto enviado ao modelo. Não use
o recorte recente de $conversations nem todos os clientes retornados por
Client::allTenants() como fonte das whitelists.
In `@app/Services/Planner/DailyPlanPromptBuilder.php`:
- Line 40: Atualize o DailyPlanResponseParser para validar o reason obrigatório
em itens de follow_up e responder cliente, rejeitando ou sinalizando valores
nulos, vazios ou sem um trecho entre aspas da última mensagem relevante.
Preserve a validação existente de tamanho e permita a exceção descrita para
anexos sem texto, desde que o reason indique explicitamente essa situação.
- Around line 44-46: Atualize a regra adicionada em DailyPlanPromptBuilder para
identificar explicitamente quando o dia não tem expediente, em vez de aplicá-la
a todo DailyPlanModeEnum::LIGHT. Preserve um comportamento separado para dias
úteis cuja capacidade foi esgotada por DailyPlanGenerator, evitando que a IA os
descreva como folga ou gere um resumo incorreto.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro Plus
Run ID: 9067b304-cdda-448e-b890-ab889bee54b3
📒 Files selected for processing (4)
app/Services/Planner/DailyPlanContextBuilder.phpapp/Services/Planner/DailyPlanGenerator.phpapp/Services/Planner/DailyPlanPromptBuilder.phptests/Feature/Planner/DailyPlanGeneratorTest.php
🚧 Files skipped from review as they are similar to previous changes (2)
- app/Services/Planner/DailyPlanGenerator.php
- tests/Feature/Planner/DailyPlanGeneratorTest.php
| ]; | ||
|
|
||
| $rules = [ | ||
| "1. A soma de estimated_minutes NÃO pode passar da capacidade RESTANTE de hoje ({$capacity}), que já desconta o tempo trabalhado até agora; deixe ~15% de folga para imprevistos.", |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟠 Major | ⚡ Quick win
Não deixe a política de capacidade apenas como instrução do prompt.
O parser aceita a capacidade total e ainda permite follow_up/habit até 120% dela; portanto, não reserva os ~15% solicitados aqui e pode gerar um plano acima do tempo restante. Centralize o orçamento efetivo no parser e cubra essa regra com testes.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@app/Services/Planner/DailyPlanPromptBuilder.php` at line 33, Centralize the
effective planning budget in the parser rather than relying only on the prompt
text: update the parser’s capacity validation to reserve approximately 15% of
the remaining capacity and reject plans exceeding that budget, including
follow_up and habit items currently allowed up to 120%. Add or update tests
covering the reserved-capacity boundary and over-budget plans.
…itar overflow e melhorar a responsividade
…que usuários especifiquem minutos adicionais e ajustem a geração do plano diário
…de conversas O Codex/AGY rejeitam UTF-8 inválido na stdin e a mensagem de erro resultante (que ecoava o prompt) quebrava a resposta JSON do Livewire. O prompt agora passa por mb_scrub antes de chegar nos CLIs, e o max_execution_time da requisição acompanha o timeout do processo. Junto: botão "ver mensagens anteriores" no chat do WhatsApp, que carrega o histórico de 50 em 50 até o início da conversa, e filtros da tela de tarefas responsivos no celular. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PqRofErS4gVjLz9BZoadc5
Permite plugar qualquer agente que fale MCP por HTTP (Claude Code, codex, agy) no PeJota para pegar contexto de um cliente: cadastro, contexto de IA, projetos, tarefas, notas e o histórico de WhatsApp com transcrições. O token é o cliente. Nenhuma tool recebe client_id, então não há caminho para outro cliente: o escopo vem do token, não dos argumentos. Faturas e valores não são expostos. Três camadas garantem que nada escreve: só existem tools de leitura; toda query nasce do cliente do token com company_id e client_id fixos; e a requisição roda na conexão sqlite_readonly com um guard que barra qualquer statement que não seja select. Na interface, Settings > MCP Accesses mostra quantos clientes estão expostos, a lista de clientes ganhou coluna e ação de MCP, e criar um acesso leva ao manual de conexão com o token (exibido uma única vez), o comando do Claude Code e o JSON para .mcp.json, além do botão de revogar. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PqRofErS4gVjLz9BZoadc5
O pacote só existia no projeto como dependência do laravel/boost, que é de desenvolvimento. Em produção o composer instala com --no-dev, então o vendor/laravel/mcp não existia e a rota /mcp/client respondia 404. A versão instalada é a mesma que já estava no lock (v0.5.9), apenas mudou de seção. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PqRofErS4gVjLz9BZoadc5
|




…package name in lockfile
Summary by CodeRabbit
.env.example.