## Flota — Fleet Agents (agentes elásticos multicanal)

Un Fleet Agent rutea conversaciones (WhatsApp, WABA, web) a workers efímeros. Los
configuras por completo vía SDK/REST — el dashboard es solo un cliente.

**Dos credenciales, no mezclar:**
- `create` / `list` / `delete` → tu credencial de cliente (API key o JWT OAuth del user, scope WRITE).
- Toda la config + mensajería → el **`token` por-agente** que devuelve `create` (persístelo junto al `id`).

⚠️ Ese `token` autoriza TODO: mensajear, cambiar el prompt, leer secretos y borrar el
agente. Para repartirlo (a un backend ajeno, a un widget) emite credenciales **con
alcance** — ver "Credenciales con alcance" más abajo.

### Crear
`POST /fleet-agents`
Body: `{ name?, systemPrompt?, model?, engine?, workerTemplate?, maxWorkersPerVm?, vmMemMb?, maxVms?, idleSuspendMin? }`
`engine` (atajo de alto nivel): `claude` (default) · `deepseek` · `codex` · `easybits` · `glm` — deriva template + modelo por defecto. Motores no-Claude pueden requerir un secret de proveedor (ej. `deepseek` → `DEEPSEEK_API_KEY`, ponlo con `set-secret`).
Returns: `{ fleetAgent: { id, token, ... } }` — guarda `id` **y** `token`.
SDK: `eb.fleet.create({ name, systemPrompt?, model?, engine? })`

### Listar / eliminar
`GET /fleet-agents` → `{ pools: FleetAgent[] }` · SDK: `eb.fleet.list()`
`POST /fleet-agents/:id/delete` · SDK: `eb.fleet.delete(id)`

### Leer configuración
`GET /fleet-agents/:id/capabilities` (auth = `token` del agente, header `Authorization: Bearer` o `?token=`)
Returns: catálogo + estado (`agent`, `buckets`, `bucketTools`, `models`, `skills`, `groups`, …).
SDK: `eb.fleet.getCapabilities(id, token)`

### Configurar (auth = `token` del agente)
`POST /fleet-agents/:id/capabilities` con `{ action, ... }`. A nivel agente (sin `groupId`):
- `set-name { name }` — SDK: `eb.fleet.setName(id, token, name)`
- `set-agent-prompt { systemPrompt }` — instrucciones base · SDK: `eb.fleet.setAgentPrompt(...)`
- `set-model { model }` — SDK: `eb.fleet.setModel(...)`
- `set-effort { effort }` — `low|medium|high|xhigh|max` · SDK: `eb.fleet.setEffort(...)`
- `toggle-own-number { on }`, `set-secret { name, value }`
- `add-mcp { name, pkg?|url?, requiredSecret?, envVar? }`, `remove-mcp { name }`
- `toggle-skill { skillId, on }`, `delete-skill { skillId }`

Por canal (con `groupId`; `"*"` = default del agente):
- `set-prompt { systemPrompt }` — SDK: `eb.fleet.setGroupPrompt(id, token, groupId, ...)`
- `set-cap-level { cap, level }` — `off|read|write` · SDK: `eb.fleet.setCapLevel(...)`
- `toggle-builtin { builtin, on }`, `set-toolgroup { buckets, inherit? }`, `toggle-asset { fileId, on }`

Respuesta uniforme: `{ ok: true }` o `{ error }`.

### Mensajería (auth = cualquier token del agente con scope MESSAGE)
`POST /fleet-agents/:id/message` → `{ reply }` · SDK: `eb.fleet.message(id, token, { groupId, text, configGroupId })`
`POST /fleet-agents/:id/message-stream` → SSE · SDK: `eb.fleet.messageStream(id, token, body, { onChunk })`

Eventos SSE: `chunk` (texto incremental), `tool`, `usage`, `capacity` y `done`.
`done.value` es la respuesta **autoritativa**: arma el mensaje final con ése, no
concatenando los chunks. `capacity` NO es un fallo del turno — tu flota está llena en ese
instante; reintenta pasado su `retryAfter`.

🚨 **Manda siempre `configGroupId`.** Es la unidad de CONFIGURACIÓN (prompt, MCPs,
capacidades); `groupId` sólo identifica la conversación y lo eliges tú (cualquier id
estable, p.ej. `web-<uuid>`). Si omites `configGroupId`, la config se busca por
conversación, no encuentra nada, y el agente arranca **sin sus conectores** — sin error
visible, indistinguible de un MCP roto ("no tengo esa herramienta"). Usa un valor estable
por canal o tenant: `"mi-app"`, `"waba:<integrationId>"`, `"crm:acme"`.
Los MCP se montan al CREAR la sesión, no en cada turno: para comprobar un cambio de
configuración, prueba con un `groupId` nuevo.

### Credenciales con alcance (para embeber el agente en tu app)
`POST /fleet-agents/:id/tokens` con `{ name, scopes:["MESSAGE"|"MANAGE"|"ADMIN"], publishable?, cfgId?, allowedOrigins? }`
→ `{ token: { id, prefix, raw } }`. El `raw` se muestra **una sola vez**.
`GET /fleet-agents/:id/tokens` lista (sin valores) · `DELETE` con `{ tokenId }` revoca.

| Scope | Puede | No puede |
|-------|-------|----------|
| `MESSAGE` | mandar turnos | nada de configuración |
| `MANAGE` | leer y ajustar config: prompt, modelo, canales, capacidades | secretos, MCPs, skills, motor, borrar |
| `ADMIN` | todo, incl. `set-secret`, `add-mcp`, `set-engine`, borrar | — |

Dos prefijos: `flt_sk_` es secreta (cualquier scope, **sólo por header**; se rechaza por
query string) y `flt_pk_` es publishable (sólo MESSAGE, admitida en el navegador y
acotada por `allowedOrigins`).

**Para un chat en el navegador**, no mandes un `flt_sk_` al cliente: pide desde tu
servidor un token de sesión con `POST /fleet-agents/:id/session-token`
`{ cfgId?, ttlMin?, allowedOrigins? }` → `{ token, expiresAt }`, un `flt_pk_` efímero
(15 min por defecto). SDK: `eb.fleet.sessionToken(id, miFltSk, { cfgId })`.
Si le pasas `cfgId`, los turnos hechos con ese token **ignoran** el `configGroupId` que
mande el cliente — sin eso, una sesión emitida para un cliente podría pedir la
configuración de otro cambiando un campo del body.

### Conexión WhatsApp (Baileys) — auth = credencial del cliente (dueño)
Vincula un número **personal** (NO Business/WABA) para que el agente atienda grupos.
- `POST /fleet-agents/:id/connect` — inicia el socket. Sin body → QR; `{ pairingPhone }` → código de emparejamiento. `?disconnect=1` para desvincular. SDK: `eb.fleet.connect(id, { pairingPhone? })` / `eb.fleet.disconnect(id)`.
- `GET /fleet-agents/:id/connect` → `{ baileys: { status, qr?, pairingCode?, phone?, pairBlockedUntil? } }`. Estados: `qr_pending|pairing|connecting|connected|failed|disconnected`. **Poll cada ~2.5s**; respeta `pairBlockedUntil` (cooldown). SDK: `eb.fleet.connectionState(id)`.
- `GET /fleet-agents/:id/groups` → `{ groups: [{ groupId, subject, enabled, isMain }] }` (toca el socket en vivo → on-demand, no en el poll). SDK: `eb.fleet.listGroups(id)`.
- `POST /fleet-agents/:id/groups` — `{ groupId, on }` prende/apaga · `{ groupId, main:true }` designa el grupo MAIN (canal admin). SDK: `eb.fleet.toggleGroup(id, groupId, on)` / `eb.fleet.setMain(id, groupId)`.
- ⚠️ El backend NO valida si el número es Business/WABA — muéstralo como advertencia. Números Business van por WABA, no por aquí.

### WABA
`POST /fleet-agents/:id/waba/config` — SDK: `eb.fleet.waba.config(id, token, {...})`
