Central de Comando (Agent Console)
Agente admin-only que sincroniza código/menu/permissões com confirmação, backup e auditoria.
Central de Comando (Agent Console)
A Central de Comando ("Mad Agent Command Center") é um agente de IA admin-only que opera o próprio app — sincroniza código gerado, menus e permissões a partir do MadBuilder, roda migrations e tira backups — sempre atrás de uma trava de confirmação e com auditoria completa. É separada do AI Copilot: o Copilot conversa com os dados de negócio do app (via tools MCP); a Central de Comando conversa com o próprio sistema.
AI Copilot (embed/v1/*) |
Agent Console (agent-console/v1/*) |
|
|---|---|---|
| Público | qualquer usuário autenticado/permissionado | admin only |
| Tools | tools de dados do manifesto MCP + blocos visuais | tools de sistema (código/menu/permissões/migrations/backup) + blocos visuais — sem tools de dados MCP |
| O que muda | linhas de tabelas de negócio, dentro do escopo do usuário | arquivos da aplicação, schema do banco, grants de permissão |
| Confirmação | re-valida piso+matriz do manifesto MCP no usuário do request | re-checa session('login') === 'admin' + executa via pipeline com backup/verify/rollback |
| Motor | AgentRunner / ConfirmCoordinator / ConversationStore / SseSink / MadAi — mesmas classes |
idem |
Habilitando e acessando
Usa a mesma flag do Copilot (mad.ai.enabled / MAD_AI_ENABLED) — não
há uma flag dedicada. O que isola a Central de Comando é a combinação de
middleware na rota e o slug do token:
// config/mad.php
'ai' => [
// URL do iframe da Central de Comando (admin-only). Default p/ dev;
// override por env em producao (URL da nuvem HTTPS).
'agent_console_frontend_url' => env('MAD_AI_AGENT_CONSOLE_FRONTEND_URL', 'http://localhost:7400/agent-console.html'),
// Kill-switch SEPARADO do ai.enabled: autoriza o agente a APLICAR
// mudancas (writes/acoes destrutivas), nao so sugerir. Fail-closed.
'apply_enabled' => (bool) env('MAD_AGENT_APPLY_ENABLED', false),
],
'builder' => [
'url' => env('MAD_BUILDER_URL'), // de onde os artefatos canonicos vem
'project_id' => env('MAD_PROJECT_ID'),
'token' => env('MAD_PROJECT_TOKEN'), // Bearer do app junto ao MadBuilder
],
O frontend (uma SPA separada, fora deste repositório) carrega nesse iframe e fala com o backend por SSE (chat) + REST (histórico/auditoria/backups).
// routes/web.php
Route::middleware([EmbedCors::class])->prefix('agent-console/v1')->group(function () {
Route::middleware([
McpManifestAuthMiddleware::class, // Bearer -> McpCurrentUser
\App\Http\Middleware\AgentConsoleAdminMiddleware::class, // slug agent_console + admin
])->group(function () {
Route::post('/chat', \Mad\Ai\Http\AgentConsoleController::class);
Route::get('/conversations', [...]);
Route::get('/conversations/{id}', [...]);
Route::get('/audit', [...]);
Route::get('/backups', [...]);
// API interna de apply: grava arquivos + roda seeders => superficie de
// RCE. Gate PROPRIO, fail-closed: ligar o chat NAO liga a escrita.
if (config('mad.ai.apply_enabled')) {
Route::post('/apply/code', [\App\Http\Controllers\AgentApplyController::class, 'code']);
Route::post('/apply/menu', [\App\Http\Controllers\AgentApplyController::class, 'menu']);
Route::post('/apply/permissions', [\App\Http\Controllers\AgentApplyController::class, 'permissions']);
}
});
});
Duas flags independentes, portanto: mad.ai.enabled registra o chat da
Central de Comando; mad.ai.apply_enabled (env MAD_AGENT_APPLY_ENABLED,
default false) é o que registra as três rotas de apply/*. Com ele
desligado o agente conversa e propõe, mas as tools update_* não têm onde
POSTar — nenhuma escrita de código, menu ou permissão acontece.
AgentConsoleAdminMiddleware exige: Bearer presente, token resolvido com
app_slug === 'agent_console' (um token do Copilot não abre esses
endpoints) e session('login') === 'admin'. Token desse slug é cunhado
com o prefixo mcp_admin_.
As tools de sistema
SystemToolRegistry é a fonte única — cada tool é um handler em
Mad\Ai\Console\Handlers\*:
| Tool | O que faz de verdade | Risco |
|---|---|---|
update_source_code |
busca o código canônico do app (GET {MAD_BUILDER_URL}/api/app/agent/code) e aplica local |
high — confirma + backup |
update_menu |
busca menu.xml/top_menu.xml/etc, valida XML (DOMDocument), aplica em resources/menus/ |
high — confirma + backup |
update_permissions |
busca um seed de permissões, aplica o arquivo e roda db:seed --force para conceder de fato |
high — confirma + backup |
run_migration |
Artisan::call('migrate'|'migrate:rollback'|'migrate:status', ...) numa das 6 conexões (business, iam, comm, ged, ai, log); pretend faz dry-run |
critical — confirma + typed-confirm + backup automático antes de mutar |
backup_database |
dump de uma conexão (sqlite→copy, mysql→mysqldump, pgsql→pg_dump) em app/backup/agent/db/ |
low — roda direto, sem confirmação (é o próprio backup) |
backup_source_code |
snapshot tar.gz de app/, resources/views/, routes/ em app/backup/agent/code/ |
low — roda direto |
ToolRisk (low/medium/high/critical) carrega três flags:
reversible, requiresConfirmation, requiresBackup. critical (hoje só
run_migration) ainda exige typed-confirm no front (o usuário digita
algo para confirmar, não só clica). O nível de risco de cada tool é injetado
no system prompt do agente (AgentConsolePromptAssembler), para o modelo
saber o que está prestes a pedir.
Sincroniza, não gera
Importante para quem avalia a segurança disto: update_source_code,
update_menu e update_permissions não deixam o LLM escrever código,
XML ou grants livremente. Os três handlers usam o trait SyncsFromCloud,
que só sabe fazer duas coisas — buscar o artefato canônico mais recente do
MadBuilder (GET {MAD_BUILDER_URL}/api/app/agent/{code|menu|permissions},
autenticado com o token de projeto) e aplicá-lo local. O modelo decide
quando disparar uma sincronização ("sincronize minhas últimas telas"),
não o que vai ser escrito — o conteúdo é sempre o que o gerador do
MadBuilder já produziu para aquele projeto. Isso fecha a classe de risco
"prompt injection convence o agente a escrever um backdoor": não há caminho
para o LLM compor o payload de código a partir do zero nessas três tools.
run_migration, backup_database e backup_source_code são as únicas que
operam sobre conteúdo que já existe localmente (migrations do projeto,
snapshots) — não recebem conteúdo arbitrário do modelo.
O pipeline: stage → lint → backup → apply atômico → auditoria
Tanto a aplicação manual de diffs (tela de sincronização do MadBuilder) quanto
as tools update_* da Central de Comando passam pelo mesmo pipeline,
implementado em App\Service\Builder\BuilderCodeSyncService::applyFiles():
{path => conteúdo}
│
▼
1. STAGE grava cada arquivo em app/tmp/builder_staging_<batchId>/<path>
(valida que o path está dentro da raiz do projeto)
│
▼
2. LINT lint de sintaxe in-process em cada *.php staged
(App\Service\Builder\PhpLinter, token_get_all +
TOKEN_PARSE) — QUALQUER erro aborta o batch inteiro
ANTES de tocar em arquivo real (XML de menu já foi
validado por DOMDocument antes de chegar aqui)
│
▼
3. BACKUP se o arquivo alvo já existe: copy() para
app/backup/builder/<batchId>/<path> (skip = não promove
esse arquivo, mas não aborta os outros)
│
▼
4. APPLY rename() atômico de staging -> alvo, arquivo por arquivo
(atomicidade é POR ARQUIVO — sem lock/transação cross-file)
│
▼
5. AUDIT 1 linha por arquivo promovido em builder_update_audit
(SQLite local): batch_id, sha256 antes/depois, status
O lint não chama php -l por exec(): PhpLinter tokeniza o fonte no
próprio processo (token_get_all($src, TOKEN_PARSE)), porque exec está em
disable_functions em boa parte das hospedagens — o caminho antigo fatalava
com "Call to undefined function exec()" e derrubava a sincronização inteira.
Tokenizar não executa nada do arquivo; o alcance é o mesmo do php -l: só
sintaxe.
batchId é a chave de tudo (Ymd-His-<hash>) — é o que volta na resposta
da API (POST /agent-console/v1/apply/code → {ok, batchId, applied, new, changed}) e o handle de rollback. Backups antigos são podados
automaticamente, mantendo os 5 batches mais recentes.
# rollback de um batch (reverte arquivos novos = apaga; alterados = restaura do backup)
BuilderCodeSyncService::rollback($batchId);
Esse pipeline de arquivo é separado da trilha de auditoria do agente
(mad_ai_tool_audit, abaixo) — uma audita "qual arquivo mudou e qual era o
sha256 antes", a outra audita "qual ferramenta foi chamada, por quem, e com
que resultado". GET /agent-console/v1/backups junta as duas visões (dumps
de banco, snapshots de código, batches de arquivo) num único feed.
Confirmação e auditoria
O fluxo de aprovação espelha o do Copilot — mesma classe
ConfirmCoordinator, mesmo contrato de bloco confirm por SSE — mas com um
re-gate diferente na hora de aprovar:
usuário: "sincronize o código mais recente"
│
▼ LLM chama update_source_code
SystemTool::handle()
│ risco high => NUNCA executa direto
│ gera confirm-id, registra pendência (ConfirmCoordinator::stash)
│ audita linha "pending" em mad_ai_tool_audit
▼
emite SSE block {type: confirm, id, tool, risk_level: high,
requires_backup: true, danger: true, ...}
│
▼ (nova requisição HTTP, sem chamar o modelo)
usuário aprova: POST /chat {confirm: {id, approved: true}}
│
▼ AgentConsoleController::resolveConfirm()
│ RE-CHECA session('login') === 'admin' (defesa contra "approved"
│ forjado pelo front — nunca confia só nesse campo)
▼
SystemToolExecutor::execute(tool, args)
│ handler->execute(): backup -> muta -> verifica
▼
ok && verificado -> status 'ok'
ok && NAO verificado -> handler->rollback(backupRef); status 'rolled_back'
falhou antes de mutar -> status 'failed' (nada foi tocado)
│
▼
ToolAuditStore::finalize(confirmId, status, backupRef, result)
Cada chamada de tool de sistema grava em mad_ai_tool_audit: conversa,
usuário/login, tool, parâmetros, nível de risco, se exigiu backup, id de
confirmação, se foi confirmada e quando, referência do backup, status final
e resultado. Combinado com o pipeline de arquivo (seção anterior), dá para
reconstruir qualquer mudança que a Central de Comando já aplicou — quem
pediu, o que o modelo decidiu chamar, o que foi de fato escrito e se houve
rollback.
Migrations
run_migration é a única tool critical. Antes de migrate/rollback
(quando pretend não está marcado), ela roda seu próprio backup via
DatabaseBackupTool primeiro — se o dump falhar, a migration é abortada sem
tocar no banco. status é somente leitura e não exige backup nem
confirmação.
Veja também
- Servidor MCP — o motor de permissão/manifesto que as tools de dados usam; a Central de Comando não usa o manifesto MCP, só o motor de agente compartilhado.
- AI Copilot (embed) — a superfície para usuários finais, com tools de dados em vez de tools de sistema.