## eve (Vercel) sobre EasyBits

[eve](https://eve.dev) es el framework open-source de Vercel para agentes: un agente es un directorio (instrucciones, tools, canales, schedules) y cada sesión es un run durable del Workflow SDK. Cuando un agente necesita ejecutar código, eve le pide una caja a un **SandboxBackend**. `@easybits.cloud/eve-sandbox` es ese backend para EasyBits: cada sesión de agente corre en su propia microVM, en tu cuenta, cobrada con tu plan.

### 1. Sandboxes para agentes eve

```bash
npm i @easybits.cloud/eve-sandbox   # Node ≥ 24 (lo exige eve)
```

```ts
// agent/sandbox.ts
import { defineSandbox } from "eve/sandbox";
import { easybits } from "@easybits.cloud/eve-sandbox";

export default defineSandbox({
  backend: easybits(),                 // lee EASYBITS_API_KEY
  async bootstrap({ use }) {
    const s = await use();
    await s.run({ command: "git clone https://github.com/tu-org/herramientas /workspace/tools && cd /workspace/tools && npm ci" });
  },
});
```

| eve | EasyBits |
|---|---|
| `prewarm` (corre en `eve start`, **no** en `eve build`) | caja temporal + seeds + `bootstrap()` → **snapshot** copy-on-write `eve:<templateKey>:<hash>`. `eve build` sólo compila (~10 s); el primer `eve start` loguea `easybits: snapshot snap_… listo` y los siguientes `reusado` (arranque ~3 s) |
| `create()` | fork del snapshot (~7 s), o caja fresca del `template` si eve no manda template |
| entre turnos | la caja sigue viva con siesta (`idleTtlSeconds` 600 → suspend, resume ~1 s) y se reattacha por `sandboxId` |
| `stop()` / `shutdown()` · `delete()` | suspend · destroy |
| `run` / `spawn` | `bash -lc` por `/bg`; stdout/stderr en streams, `kill()` señala al grupo |
| archivos | `/files/*`; rutas relativas ancladas en `/workspace`, `$HOME/…` se resuelve dentro de la caja |

Opciones: `easybits({ apiKey, baseUrl, template: "node", timeoutSeconds, workingDirectory, runTimeoutSeconds, idleTtlSeconds, hardTtlSeconds, metadata })`. `setNetworkPolicy` aplica una **política de egress por caja**, con el mismo shape que eve usa en Vercel: `"allow-all"`, `"deny-all"` o una allow-list por dominio (`{ allow: { "api.github.com": [], "registry.npmjs.org": [] } }`; `"*"` abre todo). El host la resuelve a IPs por microVM con refresco DNS, la persiste con la caja y la vuelve a aplicar al reanudar; toma efecto cuando la promesa resuelve, así que `await` antes del egress que quieres gobernar. **No soportado**: `transform` (inyectar headers en el firewall) — lanza error explícito; ese flujo (checkout de GitHub sin que el token entre a la caja) eve lo hace con su `defaultBackend`. Fuera de eve, la misma política vive en `PUT/GET /sandboxes/:id/network-policy` · SDK `sb.setNetworkPolicy(policy)` · MCP `sandbox_set_network_policy`. La llave necesita scope WRITE (crear, snapshot, fork) y DELETE si eve debe borrar snapshots.

### 2. El servidor eve dentro de una caja

Template `eve-nitro`: Node 24, pnpm, `eve` CLI, git/curl/tar; `/data` es un volumen persistente de 4 GB y el directorio de trabajo; puerto 3000.

Seis pasos: crear la caja, crear la app dentro con `eve init` (o clona la tuya si ya existe) e instalar los dos paquetes, elegir modelo, poner auth, arrancar el servidor, exponer el puerto. Antes define la base y los headers en bash:

```bash
B=https://www.easybits.cloud/api/v2; H=(-H "Authorization: Bearer $EASYBITS_API_KEY" -H "Content-Type: application/json")
SB=$(curl -s -X POST "$B/sandboxes" "${H[@]}" -d '{"template":"eve-nitro","timeoutSeconds":3600,"suspendOnIdle":true,"hardTtlSeconds":2592000}' | jq -r .sandboxId)
curl -s -X POST "$B/sandboxes/$SB/exec" "${H[@]}" -d '{"command":"cd /data && eve init app && cd app && pnpm add @easybits.cloud/eve-sandbox @easybits.cloud/eve-world @ai-sdk/anthropic && eve build","timeoutSeconds":600}'
```

**Modelo fuera de Vercel.** El scaffold de `eve init` apunta al AI Gateway de Vercel (`model: "anthropic/claude-sonnet-5"` como string) y sin `AI_GATEWAY_API_KEY` falla con *"AI Gateway received no credentials"*. En EasyBits pasa el modelo como objeto de cualquier proveedor del AI SDK — aquí Anthropic — y su llave en el env de `eve start`:

```ts
// agent/agent.ts
import { defineAgent } from "eve";
import { anthropic } from "@ai-sdk/anthropic";

export default defineAgent({
  model: anthropic("claude-sonnet-5"),   // lee ANTHROPIC_API_KEY
  experimental: { workflow: { world: "@easybits.cloud/eve-world" } },
});
```

**Auth del servidor.** En producción (`eve start`) la API HTTP exige auth: el scaffold trae `agent/channels/eve.ts` con `vercelOidc()`/`localDev()`/`placeholderAuth()` y la URL pública responde `401 Authorization is required for this route`. Edítalo, por ejemplo con basic auth:

```ts
// agent/channels/eve.ts
import { eveChannel } from "eve/channels/eve";
import { httpBasic, localDev } from "eve/channels/auth";

export default eveChannel({
  auth: [localDev(), httpBasic({ username: "eve", password: process.env.EVE_PASSWORD! })],
});
```

Arranca y expón. `eve build` sólo compiló; el snapshot del `prewarm` se crea en este primer `eve start` (log `easybits: snapshot snap_… listo`; en arranques siguientes `reusado`):

```bash
curl -s -X POST "$B/sandboxes/$SB/bg"   "${H[@]}" -d '{"command":"exec eve start","cwd":"/data/app","env":{"EASYBITS_API_KEY":"<key>","ANTHROPIC_API_KEY":"<key>","EVE_PASSWORD":"<pass>","PORT":"3000"}}'
curl -s -X POST "$B/sandboxes/$SB/expose" "${H[@]}" -d '{"port":3000}'   # → { url }
```

`EASYBITS_DB_URL` ya viene en el entorno de una caja `eve-nitro` creada con `POST /sandboxes`; no hay que pasarlo en `env` (sí en una caja hecha por fork de snapshot — sección 3).

La URL pública proxea todo el path (`/eve/` y `/.well-known/workflow/` llegan a Nitro sin configurar nada), pero eve pide la auth que declaraste: cada llamada va con `-u eve:$EVE_PASSWORD`. Proyecto y `.eve/.workflow-data` van bajo `/data` para sobrevivir suspend/resume; declara un `bootstrap` que relance `eve start` en cada despertar. El estado durable de eve vive por default en disco; para que sobreviva a la caja usa `@easybits.cloud/eve-world` (sección 3).

**Hablarle al servidor.** Crear una sesión devuelve `{ sessionId }`; el stream es NDJSON de eventos (`message.completed` trae la respuesta); el mismo id acepta más mensajes:

```bash
curl -s -u eve:$EVE_PASSWORD -X POST "$URL/eve/v1/session" -H "Content-Type: application/json" -d '{"message":"hola, ¿qué puedes hacer?"}'   # → { sessionId }
curl -s -u eve:$EVE_PASSWORD "$URL/eve/v1/session/$SESSION/stream"                                                                   # NDJSON; message.completed = respuesta
curl -s -u eve:$EVE_PASSWORD -X POST "$URL/eve/v1/session/$SESSION" -H "Content-Type: application/json" -d '{"message":"sigue"}'      # continúa la sesión
```

### 3. Estado durable en EasyBits DB (`@easybits.cloud/eve-world`)

Por default eve guarda sus runs, steps, hooks y streams en el disco de la caja (`.eve/.workflow-data`). `@easybits.cloud/eve-world` es un **World** del Workflow SDK sobre libSQL: el mismo estado vive en EasyBits DB, así que la caja del servidor se puede destruir y recrear sin perder un run a medias. Es un port 1:1 de `@workflow/world-postgres` (cola de entregas por lease en tabla) para `@workflow/world@5.0.0-beta.35`, la línea que pinea eve 0.58.1.

```bash
npm i @easybits.cloud/eve-world   # pnpm add en eve-nitro
```

```ts
// agent/agent.ts
import { defineAgent } from "eve";

export default defineAgent({
  model: /* ver sección 2 */,
  experimental: { workflow: { world: "@easybits.cloud/eve-world" } },
});
```

**Sin token que pegar.** Una caja `eve-nitro` creada con `POST /sandboxes` nace con `EASYBITS_DB_URL` ya puesto (una base `eve-<id>` por caja, creada al primer uso; el acceso lo resuelve el host por la identidad de la caja). ⚠️ Una caja creada por **fork de snapshot** NO lo trae: pásalo en el `env` del `/bg` — y para retomar runs tras destruir el servidor, con el **mismo** valor. Env completo de `eve start`: `EASYBITS_API_KEY`, `ANTHROPIC_API_KEY` (o el de tu proveedor), `EVE_PASSWORD`, `PORT=3000` y, si aplica, `EASYBITS_DB_URL`. Medido en producción: un run de 8 pasos retomó en el paso 3 en una máquina nueva 59 s después de destruir la primera, y la **sesión de chat** también sobrevive — servidor destruido a las 21:05:07, otro desde snapshot respondiendo a las 21:06:22 con toda la memoria de la conversación. Fuera de EasyBits, `WORKFLOW_LIBSQL_URL` + `WORKFLOW_LIBSQL_AUTH_TOKEN` apuntan a cualquier libSQL/Turso; si no hay env, cae a `world-local`. `WORKFLOW_SERVICE_URL` sólo hace falta con varios workers (default: el propio servidor en localhost).

No implementado (opcional en el contrato): `events.createBatch`, `queueBatch`, `runs.cancelMany`, analytics.

MCP: `sandbox_create({ template: "eve-nitro", … })` · skill: `npx skills add https://www.easybits.cloud --skill easybits-eve`.
