Docs›IA & MCP›Central de Comando (Agent Console)
IA & MCP

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.