From a7e773d1d27acb62fbe58b9b985fd719b4468694 Mon Sep 17 00:00:00 2001 From: barckcode Date: Mon, 19 Oct 2026 10:40:05 +0200 Subject: [PATCH 1/3] docs: ephemeral runs (EN/ES) New "Ephemeral runs" / "Ejecuciones efimeras" page, nested under Agent runs: what it is and when to use it instead of a workspace, the agents in the container, the portal, `nan run --ephemeral`, `POST /v1/runs` with executor/size/github_repo/model and the `runs:ephemeral` token scope, ephemeral automations (executor, size, fresh checkout) and private repositories through a GITHUB_TOKEN with Agent (read access), sizes and prices per started minute, free S-minutes per plan with the worked example, the monthly spend cap and concurrency, network rules (HTTP/HTTPS only), security and errors. No EU-only claim, no vendor names. Short cross-links from Agent runs and Automations. Orders from 20 on move by one so the child sits right after its parent; the automations guard now allows only the link to the new page. Co-Authored-By: Claude Opus 5.5 --- src/content/docs-es/apps.md | 2 +- src/content/docs-es/automations.mdx | 4 +- src/content/docs-es/built-with-nan.mdx | 2 +- src/content/docs-es/ephemeral-runs.mdx | 274 ++++++++++++++++++++ src/content/docs-es/examples.md | 2 +- src/content/docs-es/notifications.mdx | 2 +- src/content/docs-es/pr-gatekeeper.mdx | 2 +- src/content/docs-es/runs.mdx | 2 + src/content/docs-es/secrets.mdx | 2 +- src/content/docs-es/webhooks.mdx | 2 +- src/content/docs/apps.md | 2 +- src/content/docs/automations.mdx | 4 +- src/content/docs/built-with-nan.mdx | 2 +- src/content/docs/ephemeral-runs.mdx | 274 ++++++++++++++++++++ src/content/docs/examples.md | 2 +- src/content/docs/notifications.mdx | 2 +- src/content/docs/pr-gatekeeper.mdx | 2 +- src/content/docs/runs.mdx | 2 + src/content/docs/secrets.mdx | 2 +- src/content/docs/webhooks.mdx | 2 +- src/tests/layouts/automationsDocs.test.ts | 17 +- src/tests/layouts/ephemeralRunsDocs.test.ts | 178 +++++++++++++ 22 files changed, 761 insertions(+), 22 deletions(-) create mode 100644 src/content/docs-es/ephemeral-runs.mdx create mode 100644 src/content/docs/ephemeral-runs.mdx create mode 100644 src/tests/layouts/ephemeralRunsDocs.test.ts diff --git a/src/content/docs-es/apps.md b/src/content/docs-es/apps.md index 5cd89072..418badee 100644 --- a/src/content/docs-es/apps.md +++ b/src/content/docs-es/apps.md @@ -1,7 +1,7 @@ --- title: Apps description: Despliega tus apps desde GitHub a NaN Cloud en minutos. -order: 26 +order: 27 group: Guías --- diff --git a/src/content/docs-es/automations.mdx b/src/content/docs-es/automations.mdx index 553a02eb..3a21ccb1 100644 --- a/src/content/docs-es/automations.mdx +++ b/src/content/docs-es/automations.mdx @@ -1,7 +1,7 @@ --- title: Automatizaciones description: Guarda una tarea de agente una vez y deja que la lancen webhooks, programaciones o tus propios scripts, con las acciones que elijas esperando tu aprobación. -order: 20 +order: 21 group: Guías --- @@ -11,7 +11,7 @@ import Callout from '../../components/docs/Callout.astro'; # Automatizaciones. -Una **automatización** es un [run de agente](/es/docs/runs) que configuras una vez y lanzas tantas veces como necesites: el workspace, el agente, las instrucciones, la carpeta, los secretos que recibe y qué puede lanzarla. Cada vez que arranca, crea un run en tu workspace con esos ajustes. +Una **automatización** es un [run de agente](/es/docs/runs) que configuras una vez y lanzas tantas veces como necesites: el workspace, el agente, las instrucciones, la carpeta, los secretos que recibe y qué puede lanzarla. Cada vez que arranca, crea un run en tu workspace con esos ajustes. También puede ejecutarse sin workspace, cada run en su propio contenedor nuevo: consulta [Ejecuciones efímeras](/es/docs/ephemeral-runs#en-automatizaciones). Qué puede lanzar una automatización: diff --git a/src/content/docs-es/built-with-nan.mdx b/src/content/docs-es/built-with-nan.mdx index 3ac2fd61..ea35a7ec 100644 --- a/src/content/docs-es/built-with-nan.mdx +++ b/src/content/docs-es/built-with-nan.mdx @@ -1,7 +1,7 @@ --- title: Built with NaN description: Añade el badge Built with NaN al README de tu proyecto. -order: 27 +order: 28 group: Guías --- diff --git a/src/content/docs-es/ephemeral-runs.mdx b/src/content/docs-es/ephemeral-runs.mdx new file mode 100644 index 00000000..e9a71c64 --- /dev/null +++ b/src/content/docs-es/ephemeral-runs.mdx @@ -0,0 +1,274 @@ +--- +title: Ejecuciones efímeras +description: Lanza un agente sobre un repositorio de GitHub en un contenedor nuevo que se destruye al terminar, sin necesidad de workspace, con cobro por minuto. +order: 20 +group: Guías +parent: runs +--- + +import Details from '../../components/docs/Details.astro'; +import Callout from '../../components/docs/Callout.astro'; + +# Ejecuciones efímeras. + +Una **ejecución efímera** es un [run de agente](/es/docs/runs) que no necesita workspace. NaN arranca un contenedor nuevo solo para ese run, clona el repositorio de GitHub que le indicas, ejecuta el agente con el modelo que eliges y destruye el contenedor cuando el run termina. Sigues su log en directo, lo cancelas y lees su resultado exactamente igual que con cualquier otro run. + +| | Run en un workspace | Ejecución efímera | +|---|---|---| +| Dónde trabaja | En tu workspace, junto a tus archivos | En un contenedor nuevo, solo para ese run | +| Qué necesita | Un workspace encendido con el agente instalado | Un repositorio de GitHub y un modelo | +| Sobre qué trabaja | Una carpeta de tu workspace | Un clon nuevo del repositorio | +| Qué queda al terminar | Tus archivos, y una rama con sus cambios | Nada: el contenedor se destruye | +| Qué cuesta | Va incluido en tu workspace | Por minuto empezado, según el tamaño, después de tus minutos gratis | + +Usa una ejecución efímera cuando: + +- No tienes workspace, o no quieres que una tarea lo toque. +- Quieres muchos runs a la vez: cada uno tiene su propio contenedor, así que nunca comparten archivos, CPU ni memoria. +- La tarea parte del código de un repositorio: revisarlo, analizarlo, responder una pregunta sobre él, pasar sus tests. + +Usa un run en un [workspace](/es/docs/workspaces) cuando el agente necesite tus propios archivos, tus herramientas configuradas a mano o cambios que sigan ahí después del run: una ejecución efímera no guarda nada. + + +Las ejecuciones efímeras se están abriendo poco a poco. Si no ves **Ephemeral** al crear un run, tu cuenta todavía no la tiene: la API responde como si la opción no existiera. + + +## Antes de empezar + +- **Un repositorio de GitHub**, como `owner/name`, por ejemplo `acme/api`. Un run que lanzas tú clona repositorios **públicos**. Para uno privado, usa una [automatización](#repositorios-privados) con un token de GitHub. +- **Un modelo.** No hay configuración de workspace a la que recurrir, así que toda ejecución efímera indica su modelo. +- **Un plan con inferencia, o tu propia clave de proveedor.** Con la inferencia de NaN, los tokens cuentan para el uso de tu plan como en cualquier otro run. Con [tu propia clave de proveedor](/es/docs/runs#usa-tu-propia-clave-de-proveedor-byok), los factura tu proveedor, y puede usarla cualquier miembro con una suscripción activa. + +## Qué se ejecuta en el contenedor + +El contenedor viene listo con **Pi**, **Pi + Gentle Shell** y **Hermes**, además de git, Python y Node. Eliges el agente como en cualquier run (`pi`, `pi_gentle` o `hermes`: consulta [Elige el agente](/es/docs/runs#elige-el-agente)); no hay que instalar nada antes. + +Cada run: + +1. Arranca un contenedor nuevo del tamaño que elegiste. +2. Clona el repositorio en el último commit de su rama por defecto (o, en una automatización, en el commit del que trata el evento: consulta [Qué commit](/es/docs/automations#qué-commit)). +3. Ejecuta el agente en el clon, con tu tarea y el modelo que elegiste. +4. Emite sus eventos como cualquier run, y termina con su estado y el resumen del agente. +5. Destruye el contenedor y todo lo que hay en él, pase lo que pase: terminado, fallido, agotado su tiempo o cancelado. + +El agente trabaja en su clon: **Folder** y **Separate branch** no aplican, y no se hace push de nada. Para actuar sobre el repositorio (comentar, revisar, poner etiquetas), usa una [automatización](#en-automatizaciones): NaN aplica sus acciones después del run, con tu token. + +## Lanza una desde el portal + +Abre **New run** y pon **Where it runs** en **Ephemeral**. Después rellena: + +| Campo | Qué es | +|---|---| +| **Size** | **S**, **M** o **L**. Consulta [Tamaños y precios](#tamaños-y-precios). **S** por defecto. | +| **GitHub repository** | `owner/name`. | +| **Agent** | **Pi**, **Pi + Gentle Shell** o **Hermes**. | +| **Model** | El modelo que usa el agente. | +| **Task** | Lo que quieres que haga, hasta 32 KiB. | +| **Time limit** | Como en cualquier run: 15 min, 30 min (por defecto), 1 hora o 2 horas. | +| **Inference** | **NaN** o **My own key**, como en cualquier run. | + +La lista de runs muestra dónde se ejecutó cada uno y, cuando termina una ejecución efímera, lo que costó. + +## Desde la terminal + +```bash +nan run --ephemeral --repo acme/api --model glm5.3 --size s "explica cómo funciona el middleware de auth" +``` + +| Flag | Qué hace | +|---|---| +| `--ephemeral` | Ejecuta en un contenedor nuevo en vez de en un workspace. No lo combines con `--ws`. | +| `--repo owner/name` | El repositorio de GitHub que se clona. Obligatorio. | +| `--model M` | El modelo. Obligatorio, con `--model` o con `--byok-secret`. | +| `--size s\|m\|l` | El tamaño del contenedor. `s` por defecto. | + +Los demás flags de [`nan run`](/es/docs/runs#desde-la-terminal-nan-run) funcionan igual: `--agent`, `--timeout`, `--detach`, `--json`, `-f`, `--byok-secret`. `--cwd` y `--worktree` no aplican. Actualiza antes la CLI (`curl -fsSL https://nan.builders/install | bash`); `nan run --help` muestra `--ephemeral` cuando tu versión lo tiene. + +Para ver los minutos de este mes, el coste, los minutos gratis que te quedan y tus límites: + +```bash +nan ephemeral usage +nan ephemeral settings +``` + +Los límites solo se cambian en el portal: consulta [Límites](#límite-de-gasto-y-concurrencia). + +## Desde la API + +Créala con `POST https://api.nan.builders/v1/runs`, con `executor` a `ephemeral` y sin `workspace`: + +```bash +curl -s https://api.nan.builders/v1/runs \ + -H "Authorization: Bearer $NAN_TOKEN" \ + -H "Content-Type: application/json" \ + -d '{ + "executor": "ephemeral", + "size": "s", + "github_repo": "acme/api", + "agent": "pi", + "model": "glm5.3", + "prompt": "explica cómo funciona el middleware de auth", + "timeout_seconds": 1800 + }' +``` + +| Campo | Qué es | +|---|---| +| `executor` | `ephemeral`. Si no lo envías, o con `workspace`, el run va a un workspace. | +| `size` | `s`, `m` o `l`. `s` por defecto. | +| `github_repo` | `owner/name`. Obligatorio. | +| `model` | Obligatorio. Con tu propia clave, ponlo en `inference.model`, como en [Elígela para un run](/es/docs/runs#elígela-para-un-run). | +| `agent`, `prompt`, `timeout_seconds`, `inference` | Como en cualquier run. `timeout_seconds` va de 60 a 7200. | +| `workspace`, `cwd`, `git_isolation` | No se permiten: una ejecución efímera no tiene workspace y trabaja en su clon. | + +Los eventos, el stream en directo, la cancelación y el objeto run son los mismos que en cualquier run, por id de run. El objeto run añade `executor` (`ephemeral`), `size` y, cuando el run ya está medido, `cost`: `eur`, `eur_micros` y `billed_seconds`. Su `workspace` es `null`. + +**Credenciales.** Las ejecuciones efímeras cuestan dinero, así que no cualquier credencial puede lanzarlas: + +- Una sesión iniciada (el portal, o la CLI después de `nan auth login`) puede. +- Un token de plataforma (`nan_pat_...`) necesita el scope **`runs:ephemeral`**, además de `runs`. Lo añades al crear el token en **Settings → Tokens**, con la sesión iniciada en el portal. +- Una API key (`sk-...`) no puede: recibe `403 token_scope_denied`. + +Un run con tu propia clave de proveedor lanzado por ti sigue necesitando una sesión iniciada, como cualquier run: consulta [Quién puede usarla](/es/docs/runs#quién-puede-usarla). + +## En automatizaciones + +Una [automatización](/es/docs/automations) también puede ejecutarse en contenedores efímeros: cada run que lanza, con **Run now**, un webhook o una programación, tiene su propio contenedor. En el editor, pon **Where it runs** en **Ephemeral** y elige el **Size**. Entonces necesita: + +- Un **GitHub repository**, y **Checkout** en **Fresh clone per run**: no hay carpeta de workspace en la que trabajar. +- Un **Model**. + +Sin **Workspace**, **Folder** ni **Separate branch**. Desde la CLI o la API, los campos son `executor: ephemeral`, `size`, `checkout: fresh`, `github_repo` y `model`: + +```yaml +name: PR review api +template: pr_review +executor: ephemeral +size: m +checkout: fresh +github_repo: acme/api +agent: pi +model: glm5.3 +secrets: + - { secret: GITHUB_READ_TOKEN, env: GITHUB_TOKEN, exposure: agent } + - { secret: GITHUB_WRITE_TOKEN, env: GITHUB_TOKEN, exposure: actions } +``` + +Todo lo demás funciona como en cualquier automatización con clon nuevo: [qué commit](/es/docs/automations#qué-commit) se usa, el filtro de [pull requests de forks y de colaboradores externos](/es/docs/automations#pull-requests-de-forks-y-de-colaboradores-externos), los secretos, las aprobaciones y las acciones (comentarios, revisiones y etiquetas, nunca un push). Crear o cambiar una automatización efímera, o sus triggers, necesita una sesión iniciada o un token con `runs:ephemeral`. + +### Repositorios privados + +Para clonar un repositorio privado, asigna a la automatización un token de GitHub como `GITHUB_TOKEN` con **Agent (read access)**: un token fine-grained, de solo lectura, limitado a ese repositorio. NaN lo usa para clonar, como se explica en [Clon nuevo por run](/es/docs/automations#clon-nuevo-por-run), y el agente lo recibe para leer el repositorio. Consulta [El patrón del token de GitHub](/es/docs/secrets#el-patrón-del-token-de-github). + +Un run que lanzas tú, desde el portal, `nan run` o `POST /v1/runs`, nunca recibe secretos, así que solo puede clonar repositorios públicos. + +## Tamaños y precios + +| Tamaño | vCPU | Memoria | Disco | Precio por minuto | +|---|---|---|---|---| +| **S** | 1 | 4 GiB | 10 GB | 0,003 € | +| **M** | 2 | 8 GiB | 16 GB | 0,006 € | +| **L** | 4 | 12 GiB | 20 GB | 0,012 € | + +Los runs se cobran **por minuto empezado** de tiempo de contenedor, de principio a fin, al precio de su tamaño. Un run de 14 segundos cuenta como 1 minuto; uno de 61 segundos, como 2. Los tokens del modelo no entran en este precio: cuentan para tu plan, o te los factura tu proveedor con tu propia clave. + +La mayoría de los agentes pasan el tiempo esperando al modelo, así que **S** basta para casi todas las tareas. Elige **M** o **L** para builds o baterías de tests pesadas. + + +Las ejecuciones efímeras se miden y se muestran, en el portal, la CLI y la API, pero todavía no se cobran. Lo anunciaremos antes de empezar a cobrar. + + +## Minutos gratis + +Cada mes tienes minutos gratis, según tu plan: + +| Plan | Minutos gratis al mes | +|---|---| +| Community | 100 | +| Inference | 300 | +| Premium | 1000 | + +**Los minutos gratis son minutos S.** Sirven para cualquier tamaño, pero un minuto de un tamaño mayor gasta más: + +| Un minuto de | Gasta | +|---|---| +| **S** | 1 minuto gratis | +| **M** | 2 minutos gratis | +| **L** | 4 minutos gratis | + +Así, 100 minutos gratis son 100 minutos de **S**, o 50 de **M**, o 25 de **L**, o cualquier combinación. Nunca tienes que elegir un tamaño para no salirte de ellos. + +**Un ejemplo.** Con el plan Inference tienes 300 minutos gratis. Este mes ejecutas 100 minutos de **M** (200 minutos gratis) y 30 minutos de **L** (120 minutos gratis): 320 en total. Los primeros 300 son gratis, y los 20 minutos S que sobran cuestan 20 × 0,003 € = **0,06 €**. + +Los minutos gratis se gastan primero y se renuevan el día 1 de cada mes (UTC). Los que no uses no se acumulan. El `cost` de cada run es el precio completo de sus minutos; los minutos gratis se cuentan sobre el mes, en **Settings → Ephemeral runs** y en `nan ephemeral usage`. + +## Límite de gasto y concurrencia + +Dos límites te protegen de sorpresas. Se cambian en el portal, en **Settings → Ephemeral runs**, con la sesión iniciada: + +| Límite | Por defecto | Rango | +|---|---|---| +| **Límite de gasto mensual** | 100 € | De 0 € a 1000 € | +| **Ejecuciones efímeras a la vez** | 50 | De 1 a 50 | + +**El límite de gasto** se comprueba antes de que arranque cada run. Lo gastado este mes después de tus minutos gratis, más lo máximo que podría costar cada ejecución efímera aún abierta, más lo máximo que podría costar la nueva, tiene que caber en el límite. Lo máximo que puede costar un run es su tiempo límite más 2,5 minutos, al precio de su tamaño: un run de 30 minutos en **S** reserva 33 minutos, 0,099 €. Si no cabe, el run se rechaza con `402 ephemeral_cap_reached`. Sube el límite, espera a que terminen los runs en marcha, o usa un tiempo límite más corto o un tamaño menor. La comprobación usa el precio completo de los runs abiertos, aunque al final los cubran tus minutos gratis: un límite de 0 € frena todas las ejecuciones efímeras. + +**Ejecuciones a la vez.** Los runs que superan tu límite no se rechazan: esperan en la cola y arrancan por orden. Los límites habituales para crear runs siguen aplicando. + +## Red + +Desde el contenedor, el agente solo puede salir a internet por **HTTP y HTTPS** (puertos 80 y 443): registros de paquetes, GitHub, documentación, APIs. + +- **Ningún otro puerto**: ni SSH, ni SMTP (correo), ni bases de datos u otros servicios en sus propios puertos, ni URLs con un puerto explícito distinto de 80 o 443. +- Sin WebSockets, sin direcciones IP sueltas (usa nombres de host) y sin sitios de NaN distintos de `api.nan.builders`. +- Hasta 30 peticiones por segundo por run. +- Hasta 2 GiB descargados y 512 MiB enviados por run. Por encima, el run se detiene y falla con `egress_limit`. + +Así que clona por HTTPS, no por SSH, y descarga las dependencias de registros que sirvan HTTPS. + +## Seguridad + +- Cada run tiene su propio contenedor, creado para él y destruido después. No se comparte nada entre runs, miembros ni con tus workspaces. +- **La clave de inferencia nunca entra en el contenedor**, ni la de NaN ni la tuya. Las peticiones del agente al modelo salen con un marcador, y la clave real se añade por el camino, solo para el proveedor del modelo de ese run. +- El contenedor solo recibe los secretos que asignas a la automatización, [enmascarados](/es/docs/secrets#enmascarado-en-logs-y-eventos) en logs y resultados, como en cualquier run. +- El código que clonas se ejecuta en el contenedor, junto a esos secretos. El consejo sobre [pull requests de forks](/es/docs/automations#pull-requests-de-forks-y-de-colaboradores-externos) también aplica aquí. + +## Si algo sale mal + +| Qué ves | Por qué, y qué hacer | +|---|---| +| `402 ephemeral_cap_reached` | El run podría llevar tu gasto por encima de tu límite mensual. Súbelo en **Settings → Ephemeral runs**, espera a que terminen los runs en marcha, o usa un tiempo límite más corto o un tamaño menor. | +| `400 invalid_request`, `param: executor` | Las ejecuciones efímeras aún no están disponibles para tu cuenta. | +| `400 invalid_request`, `param: github_repo` | Falta el repositorio o no es `owner/name`. | +| `400 invalid_request`, `param: model` | Una ejecución efímera tiene que indicar su modelo. | +| `400 invalid_request`, `param: workspace`, `cwd` o `git_isolation` | Esos campos no aplican a una ejecución efímera: no los envíes. | +| `403 token_scope_denied` | Una API key, o un token sin `runs:ephemeral`. Usa una sesión iniciada, o un token con ese scope. | +| El run falla al clonar el repositorio | El repositorio es privado y el run no tiene un `GITHUB_TOKEN` con **Agent (read access)**, o el token no puede leerlo. Consulta [Repositorios privados](#repositorios-privados). | +| El run falla con `egress_limit` | Descargó más de 2 GiB o envió más de 512 MiB. | +| El agente no puede conectarse a algún sitio | Solo están abiertos HTTP y HTTPS. Consulta [Red](#red). | + +## Preguntas frecuentes + +
+ +No. Se ejecuta en su propio contenedor. Puedes usar ejecuciones efímeras sin tener ningún workspace. + +
+ +
+ +No desde el contenedor: se destruye cuando termina el run. Usa una [automatización](#en-automatizaciones) para comentar, revisar o etiquetar a partir del resultado, o un run en un [workspace](/es/docs/runs) cuando quieras una rama con los cambios. + +
+ +
+ +En **Settings → Ephemeral runs** en el portal, con `nan ephemeral usage`, y en cada run (su `cost`). + +
+ +
+ +Escribe en `#support` en Discord. + +
diff --git a/src/content/docs-es/examples.md b/src/content/docs-es/examples.md index 9c8ae050..0a9a206c 100644 --- a/src/content/docs-es/examples.md +++ b/src/content/docs-es/examples.md @@ -1,7 +1,7 @@ --- title: Ejemplos description: Fragmentos de código para conectarte a la API de NaN con Python, Node.js, curl y más. -order: 25 +order: 26 group: Guías --- diff --git a/src/content/docs-es/notifications.mdx b/src/content/docs-es/notifications.mdx index a7a22f74..7810cd2c 100644 --- a/src/content/docs-es/notifications.mdx +++ b/src/content/docs-es/notifications.mdx @@ -1,7 +1,7 @@ --- title: Notificaciones description: Entérate en Slack, Microsoft Teams, Discord o tu propio endpoint HTTPS cuando una automatización necesita tu aprobación, falla, se desactiva o su webhook no deja de fallar. -order: 23 +order: 24 group: Guías parent: automations --- diff --git a/src/content/docs-es/pr-gatekeeper.mdx b/src/content/docs-es/pr-gatekeeper.mdx index 778636c8..4f8aa98d 100644 --- a/src/content/docs-es/pr-gatekeeper.mdx +++ b/src/content/docs-es/pr-gatekeeper.mdx @@ -1,7 +1,7 @@ --- title: PR gatekeeper description: Monta, paso a paso, una automatización que comprueba cada pull request contra el mínimo de tu equipo y deja el veredicto en un solo comentario. -order: 21 +order: 22 group: Guías parent: automations --- diff --git a/src/content/docs-es/runs.mdx b/src/content/docs-es/runs.mdx index 87417a3f..7a3ae762 100644 --- a/src/content/docs-es/runs.mdx +++ b/src/content/docs-es/runs.mdx @@ -24,6 +24,8 @@ Puedes lanzar un run de tres formas, y las tres ven los mismos runs: Para lanzar la misma tarea una y otra vez, desde un webhook, con una programación o con acciones que esperan tu aprobación, guárdala como una [automatización](/es/docs/automations). +¿No tienes workspace, o prefieres que la tarea no toque el tuyo? Una [ejecución efímera](/es/docs/ephemeral-runs) trabaja sobre un clon nuevo de un repositorio de GitHub, en un contenedor creado para ese run y destruido al terminar, con cobro por minuto. + ## Antes de empezar - **Un workspace encendido**, con al menos un agente instalado: Pi, Pi + Gentle Shell o Hermes. Se instalan desde el [panel de Software](/es/docs/workspaces#agentes-y-el-panel-de-software) del workspace. Consulta [Elige el agente](#elige-el-agente). diff --git a/src/content/docs-es/secrets.mdx b/src/content/docs-es/secrets.mdx index 8859a53e..221895e3 100644 --- a/src/content/docs-es/secrets.mdx +++ b/src/content/docs-es/secrets.mdx @@ -1,7 +1,7 @@ --- title: Secretos description: Guarda los tokens y claves que usan tus automatizaciones, y tus propias claves de proveedores de IA, cifrados y de solo escritura. Gestiónalos desde el portal, la CLI o la API. -order: 22 +order: 23 group: Guías parent: automations --- diff --git a/src/content/docs-es/webhooks.mdx b/src/content/docs-es/webhooks.mdx index 56881599..7fa04aa6 100644 --- a/src/content/docs-es/webhooks.mdx +++ b/src/content/docs-es/webhooks.mdx @@ -1,7 +1,7 @@ --- title: Webhooks y programaciones description: Lanza tus automatizaciones desde GitHub, Stripe o tus propios sistemas con webhooks firmados, o con una programación. -order: 24 +order: 25 group: Guías --- diff --git a/src/content/docs/apps.md b/src/content/docs/apps.md index 882d9349..7a252932 100644 --- a/src/content/docs/apps.md +++ b/src/content/docs/apps.md @@ -1,7 +1,7 @@ --- title: Apps description: Deploy your apps from GitHub to NaN Cloud in minutes. -order: 26 +order: 27 group: Guides --- diff --git a/src/content/docs/automations.mdx b/src/content/docs/automations.mdx index 6574af5e..2d23daa5 100644 --- a/src/content/docs/automations.mdx +++ b/src/content/docs/automations.mdx @@ -1,7 +1,7 @@ --- title: Automations description: Save an agent task once and let webhooks, schedules or your own scripts run it, with the actions you choose waiting for your approval. -order: 20 +order: 21 group: Guides --- @@ -11,7 +11,7 @@ import Callout from '../../components/docs/Callout.astro'; # Automations. -An **automation** is an [agent run](/docs/runs) you set up once and start as often as you need: the workspace, the agent, the instructions, the folder, the secrets it receives and what may start it. Every time it starts, it creates a run in your workspace with those settings. +An **automation** is an [agent run](/docs/runs) you set up once and start as often as you need: the workspace, the agent, the instructions, the folder, the secrets it receives and what may start it. Every time it starts, it creates a run in your workspace with those settings. It can also run without a workspace, each run in its own fresh container: see [Ephemeral runs](/docs/ephemeral-runs#in-automations). What can start an automation: diff --git a/src/content/docs/built-with-nan.mdx b/src/content/docs/built-with-nan.mdx index e5dd30b1..cb3b796e 100644 --- a/src/content/docs/built-with-nan.mdx +++ b/src/content/docs/built-with-nan.mdx @@ -1,7 +1,7 @@ --- title: Built with NaN description: Add the Built with NaN badge to your project's README. -order: 27 +order: 28 group: Guides --- diff --git a/src/content/docs/ephemeral-runs.mdx b/src/content/docs/ephemeral-runs.mdx new file mode 100644 index 00000000..7002c3ca --- /dev/null +++ b/src/content/docs/ephemeral-runs.mdx @@ -0,0 +1,274 @@ +--- +title: Ephemeral runs +description: Run an agent on a GitHub repository in a fresh container that is destroyed when it ends, with no workspace needed, billed per minute. +order: 20 +group: Guides +parent: runs +--- + +import Details from '../../components/docs/Details.astro'; +import Callout from '../../components/docs/Callout.astro'; + +# Ephemeral runs. + +An **ephemeral run** is an [agent run](/docs/runs) that does not need a workspace. NaN starts a fresh container just for that run, clones the GitHub repository you name, runs the agent with the model you choose and destroys the container when the run ends. You follow its log live, cancel it and read its result exactly as with any other run. + +| | Run in a workspace | Ephemeral run | +|---|---|---| +| Where it works | In your workspace, next to your files | In a fresh container, only for that run | +| What it needs | A running workspace with the agent installed | A GitHub repository and a model | +| What it works on | A folder of your workspace | A fresh clone of the repository | +| What is left when it ends | Your files, and a branch with its changes | Nothing: the container is destroyed | +| What it costs | It is part of your workspace | Per started minute, by size, after your free minutes | + +Use an ephemeral run when: + +- You do not have a workspace, or you do not want a task to touch it. +- You want many runs at once: each one gets its own container, so they never share files, CPU or memory. +- The task starts from the code of a repository: review it, analyse it, answer a question about it, run its tests. + +Use a run in a [workspace](/docs/workspaces) when the agent needs your own files, your tools set up by hand, or changes that stay after the run: an ephemeral run keeps nothing. + + +Ephemeral runs are being opened gradually. If you do not see **Ephemeral** when you create a run, your account does not have it yet: the API answers as if the option did not exist. + + +## Before you start + +- **A GitHub repository**, as `owner/name`, for example `acme/api`. A run you start yourself clones **public** repositories. For a private one, use an [automation](#private-repositories) with a GitHub token. +- **A model.** There is no workspace configuration to fall back on, so every ephemeral run names its model. +- **A plan with inference, or your own provider key.** On NaN inference, the tokens count against your plan's usage like any other run. On [your own provider key](/docs/runs#use-your-own-provider-key-byok), your provider bills them, and any member with an active subscription can use it. + +## What runs in the container + +The container comes ready with **Pi**, **Pi + Gentle Shell** and **Hermes**, plus git, Python and Node. You pick the agent as for any run (`pi`, `pi_gentle` or `hermes`: see [Pick the agent](/docs/runs#pick-the-agent)); nothing needs installing first. + +Each run: + +1. Starts a new container of the size you chose. +2. Clones the repository at the latest commit of its default branch (or, for an automation, the commit the event is about: see [Which commit](/docs/automations#which-commit)). +3. Runs the agent in the clone, with your task and the model you chose. +4. Streams its events like any run, and ends with its state and the agent's summary. +5. Destroys the container and everything in it, whatever happened: succeeded, failed, timed out or cancelled. + +The agent works in its clone: **Folder** and **Separate branch** do not apply, and nothing is pushed. To act on the repository (comment, review, add labels), use an [automation](#in-automations): its actions are applied by NaN after the run, with your token. + +## Start one from the portal + +Open **New run** and set **Where it runs** to **Ephemeral**. Then fill in: + +| Field | What it is | +|---|---| +| **Size** | **S**, **M** or **L**. See [Sizes and prices](#sizes-and-prices). **S** by default. | +| **GitHub repository** | `owner/name`. | +| **Agent** | **Pi**, **Pi + Gentle Shell** or **Hermes**. | +| **Model** | The model the agent uses. | +| **Task** | What you want done, up to 32 KiB. | +| **Time limit** | As for any run: 15 min, 30 min (the default), 1 hour or 2 hours. | +| **Inference** | **NaN** or **My own key**, as for any run. | + +The run list shows where each run ran and, once an ephemeral run ends, what it cost. + +## From the terminal + +```bash +nan run --ephemeral --repo acme/api --model glm5.3 --size s "explain how the auth middleware works" +``` + +| Flag | What it does | +|---|---| +| `--ephemeral` | Run in a fresh container instead of a workspace. Do not combine it with `--ws`. | +| `--repo owner/name` | The GitHub repository to clone. Required. | +| `--model M` | The model. Required, as `--model` or with `--byok-secret`. | +| `--size s\|m\|l` | The container size. `s` by default. | + +The other flags of [`nan run`](/docs/runs#from-the-terminal-nan-run) work the same: `--agent`, `--timeout`, `--detach`, `--json`, `-f`, `--byok-secret`. `--cwd` and `--worktree` do not apply. Update the CLI first (`curl -fsSL https://nan.builders/install | bash`); `nan run --help` lists `--ephemeral` once your version has it. + +To see this month's minutes, cost and free minutes left, and your limits: + +```bash +nan ephemeral usage +nan ephemeral settings +``` + +You change the limits in the portal only: see [Limits](#spend-cap-and-concurrency). + +## From the API + +Create it with `POST https://api.nan.builders/v1/runs`, with `executor` set to `ephemeral` and no `workspace`: + +```bash +curl -s https://api.nan.builders/v1/runs \ + -H "Authorization: Bearer $NAN_TOKEN" \ + -H "Content-Type: application/json" \ + -d '{ + "executor": "ephemeral", + "size": "s", + "github_repo": "acme/api", + "agent": "pi", + "model": "glm5.3", + "prompt": "explain how the auth middleware works", + "timeout_seconds": 1800 + }' +``` + +| Field | What it is | +|---|---| +| `executor` | `ephemeral`. Left out, or `workspace`, the run goes to a workspace. | +| `size` | `s`, `m` or `l`. `s` by default. | +| `github_repo` | `owner/name`. Required. | +| `model` | Required. With your own key, put it in `inference.model` instead, as in [Choose it for a run](/docs/runs#choose-it-for-a-run). | +| `agent`, `prompt`, `timeout_seconds`, `inference` | As for any run. `timeout_seconds` goes from 60 to 7200. | +| `workspace`, `cwd`, `git_isolation` | Not allowed: an ephemeral run has no workspace and works in its clone. | + +Events, the live stream, cancelling and the run object are the same as for any run, by run id. The run object adds `executor` (`ephemeral`), `size` and, once the run is metered, `cost`: `eur`, `eur_micros` and `billed_seconds`. Its `workspace` is `null`. + +**Credentials.** Ephemeral runs cost money, so not every credential can start them: + +- A signed-in session (the portal, or the CLI after `nan auth login`) can. +- A platform token (`nan_pat_...`) needs the **`runs:ephemeral`** scope, on top of `runs`. You add it when you create the token under **Settings → Tokens**, signed in on the portal. +- An API key (`sk-...`) cannot: it gets `403 token_scope_denied`. + +A run on your own provider key started by you still needs a signed-in session, as for any run: see [Who can use it](/docs/runs#who-can-use-it). + +## In automations + +An [automation](/docs/automations) can run on ephemeral containers too: every run it starts, by **Run now**, a webhook or a schedule, gets its own container. In the editor, set **Where it runs** to **Ephemeral** and pick the **Size**. It then needs: + +- A **GitHub repository**, and **Checkout** set to **Fresh clone per run**: there is no workspace folder to work in. +- A **Model**. + +No **Workspace**, **Folder** or **Separate branch**. Through the CLI or the API, the fields are `executor: ephemeral`, `size`, `checkout: fresh`, `github_repo` and `model`: + +```yaml +name: PR review api +template: pr_review +executor: ephemeral +size: m +checkout: fresh +github_repo: acme/api +agent: pi +model: glm5.3 +secrets: + - { secret: GITHUB_READ_TOKEN, env: GITHUB_TOKEN, exposure: agent } + - { secret: GITHUB_WRITE_TOKEN, env: GITHUB_TOKEN, exposure: actions } +``` + +Everything else works as in any automation with a fresh clone: [which commit](/docs/automations#which-commit) is checked out, the [pull requests from forks and outside contributors](/docs/automations#pull-requests-from-forks-and-outside-contributors) filter, secrets, approvals and actions (comments, reviews and labels, never a push). Creating or changing an ephemeral automation, or its triggers, needs a signed-in session or a token with `runs:ephemeral`. + +### Private repositories + +To clone a private repository, assign a GitHub token to the automation as `GITHUB_TOKEN` with **Agent (read access)**: a fine-grained token, read-only, limited to that repository. NaN uses it to clone, as described in [Fresh clone per run](/docs/automations#fresh-clone-per-run), and the agent receives it to read the repository. See [The GitHub token pattern](/docs/secrets#the-github-token-pattern). + +A run you start yourself, from the portal, `nan run` or `POST /v1/runs`, never receives secrets, so it can only clone public repositories. + +## Sizes and prices + +| Size | vCPU | Memory | Disk | Price per minute | +|---|---|---|---|---| +| **S** | 1 | 4 GiB | 10 GB | €0.003 | +| **M** | 2 | 8 GiB | 16 GB | €0.006 | +| **L** | 4 | 12 GiB | 20 GB | €0.012 | + +Runs are billed **per started minute** of container time, from start to end, at the price of their size. A run of 14 seconds counts as 1 minute; one of 61 seconds, as 2. The model's tokens are not part of this price: they count against your plan, or your provider bills them with your own key. + +Most agents spend their time waiting for the model, so **S** is enough for most tasks. Pick **M** or **L** for heavy builds or test suites. + + +Ephemeral runs are metered and shown, in the portal, the CLI and the API, but not charged yet. We will announce it before charging starts. + + +## Free minutes + +Every month you get free minutes, according to your plan: + +| Plan | Free minutes per month | +|---|---| +| Community | 100 | +| Inference | 300 | +| Premium | 1000 | + +**Free minutes are S-minutes.** They work for every size, but a minute of a bigger size uses more of them: + +| One minute of | Uses | +|---|---| +| **S** | 1 free minute | +| **M** | 2 free minutes | +| **L** | 4 free minutes | + +So 100 free minutes are 100 minutes of **S**, or 50 of **M**, or 25 of **L**, or any mix. You never have to pick a size to stay within them. + +**A worked example.** On the Inference plan you have 300 free minutes. This month you run 100 minutes of **M** (200 free minutes) and 30 minutes of **L** (120 free minutes): 320 in total. The first 300 are free, and the 20 S-minutes left over cost 20 × €0.003 = **€0.06**. + +Free minutes are used first and reset on the 1st of each month (UTC). Unused ones do not carry over. The `cost` of each run is the full price of its minutes; the free minutes are counted over the month, in **Settings → Ephemeral runs** and in `nan ephemeral usage`. + +## Spend cap and concurrency + +Two limits protect you from surprises. You change them in the portal, under **Settings → Ephemeral runs**, signed in: + +| Limit | Default | Range | +|---|---|---| +| **Monthly spend cap** | €100 | €0 to €1000 | +| **Ephemeral runs at once** | 50 | 1 to 50 | + +**The spend cap** is checked before each run starts. This month's spend after your free minutes, plus the most every ephemeral run still open could cost, plus the most the new one could cost, must stay within the cap. The most a run can cost is its time limit plus 2.5 minutes, at its size price: a 30 minute run of **S** reserves 33 minutes, €0.099. If it does not fit, the run is refused with `402 ephemeral_cap_reached`. Raise the cap, wait for running runs to end, or use a shorter time limit or a smaller size. The check uses the full price of the open runs, even if your free minutes will end up covering them: a cap of €0 stops every ephemeral run. + +**Runs at once.** Runs beyond your limit are not refused: they wait in the queue and start in order. The usual limits on creating runs still apply. + +## Network + +From the container, the agent can reach the internet over **HTTP and HTTPS** only (ports 80 and 443): package registries, GitHub, documentation, APIs. + +- **No other ports**: no SSH, no SMTP (email), no databases or other services on their own ports, and no URLs with an explicit port other than 80 or 443. +- No WebSockets, no bare IP addresses (use host names) and no NaN sites other than `api.nan.builders`. +- Up to 30 requests per second per run. +- Up to 2 GiB downloaded and 512 MiB sent per run. Beyond that, the run is stopped and fails with `egress_limit`. + +So clone over HTTPS, not SSH, and fetch dependencies from registries that serve HTTPS. + +## Security + +- Each run has its own container, created for it and destroyed after it. Nothing is shared between runs, members or your workspaces. +- **The inference key never enters the container**, neither NaN's nor yours. The agent's requests to the model go out with a placeholder, and the real key is added on the way out, only for the model provider of that run. +- The container receives only the secrets you assign to the automation, [masked](/docs/secrets#masking-in-logs-and-events) in logs and results, like in any run. +- The code you clone runs in the container, next to those secrets. The advice for [pull requests from forks](/docs/automations#pull-requests-from-forks-and-outside-contributors) applies here too. + +## If something goes wrong + +| What you see | Why, and what to do | +|---|---| +| `402 ephemeral_cap_reached` | The run could take your spend past your monthly cap. Raise it in **Settings → Ephemeral runs**, wait for running runs to end, or use a shorter time limit or a smaller size. | +| `400 invalid_request`, `param: executor` | Ephemeral runs are not available for your account yet. | +| `400 invalid_request`, `param: github_repo` | The repository is missing or is not `owner/name`. | +| `400 invalid_request`, `param: model` | An ephemeral run must name its model. | +| `400 invalid_request`, `param: workspace`, `cwd` or `git_isolation` | Those fields do not apply to an ephemeral run: leave them out. | +| `403 token_scope_denied` | An API key, or a token without `runs:ephemeral`. Use a signed-in session, or a token with that scope. | +| The run fails cloning the repository | The repository is private and the run has no `GITHUB_TOKEN` with **Agent (read access)**, or the token cannot read it. See [Private repositories](#private-repositories). | +| The run fails with `egress_limit` | It downloaded more than 2 GiB or sent more than 512 MiB. | +| The agent cannot connect somewhere | Only HTTP and HTTPS are open. See [Network](#network). | + +## FAQ + +
+ +No. It runs in its own container. You can use ephemeral runs without any workspace. + +
+ +
+ +Not from the container: it is destroyed when the run ends. Use an [automation](#in-automations) to comment, review or label from the result, or a run in a [workspace](/docs/runs) when you want a branch with the changes. + +
+ +
+ +In **Settings → Ephemeral runs** on the portal, with `nan ephemeral usage`, and on each run (its `cost`). + +
+ +
+ +Write in `#support` on Discord. + +
diff --git a/src/content/docs/examples.md b/src/content/docs/examples.md index 13795c47..e5671e73 100644 --- a/src/content/docs/examples.md +++ b/src/content/docs/examples.md @@ -1,7 +1,7 @@ --- title: Examples description: Code snippets to connect to the NaN API with Python, Node.js, curl, and more. -order: 25 +order: 26 group: Guides --- diff --git a/src/content/docs/notifications.mdx b/src/content/docs/notifications.mdx index b8377cd1..e84e30d6 100644 --- a/src/content/docs/notifications.mdx +++ b/src/content/docs/notifications.mdx @@ -1,7 +1,7 @@ --- title: Notifications description: Get told in Slack, Microsoft Teams, Discord or your own HTTPS endpoint when an automation needs your approval, fails, is switched off or its webhook keeps failing. -order: 23 +order: 24 group: Guides parent: automations --- diff --git a/src/content/docs/pr-gatekeeper.mdx b/src/content/docs/pr-gatekeeper.mdx index c7e6a101..e3e1c3cb 100644 --- a/src/content/docs/pr-gatekeeper.mdx +++ b/src/content/docs/pr-gatekeeper.mdx @@ -1,7 +1,7 @@ --- title: PR gatekeeper description: Build, step by step, an automation that checks every pull request against your team's minimum bar and keeps the verdict in one comment. -order: 21 +order: 22 group: Guides parent: automations --- diff --git a/src/content/docs/runs.mdx b/src/content/docs/runs.mdx index daf44c55..7f294acf 100644 --- a/src/content/docs/runs.mdx +++ b/src/content/docs/runs.mdx @@ -24,6 +24,8 @@ You can start a run in three ways, and all three see the same runs: To run the same task again and again, from a webhook, on a schedule or with actions that wait for your approval, save it as an [automation](/docs/automations). +No workspace, or want the task kept away from yours? An [ephemeral run](/docs/ephemeral-runs) works on a fresh clone of a GitHub repository, in a container created for that run and destroyed when it ends, billed per minute. + ## Before you start - **A workspace that is running**, with at least one agent installed: Pi, Pi + Gentle Shell or Hermes. You install them from the [Software panel](/docs/workspaces#agents-and-the-software-panel) of the workspace. See [Pick the agent](#pick-the-agent). diff --git a/src/content/docs/secrets.mdx b/src/content/docs/secrets.mdx index b7403c85..0dd230f4 100644 --- a/src/content/docs/secrets.mdx +++ b/src/content/docs/secrets.mdx @@ -1,7 +1,7 @@ --- title: Secrets description: Keep the tokens and keys your automations use, and your own AI provider keys, encrypted and write-only. Manage them from the portal, the CLI or the API. -order: 22 +order: 23 group: Guides parent: automations --- diff --git a/src/content/docs/webhooks.mdx b/src/content/docs/webhooks.mdx index e0cd2d44..51108cef 100644 --- a/src/content/docs/webhooks.mdx +++ b/src/content/docs/webhooks.mdx @@ -1,7 +1,7 @@ --- title: Webhooks and schedules description: Start your automations from GitHub, Stripe or your own systems with signed webhooks, or on a schedule. -order: 24 +order: 25 group: Guides --- diff --git a/src/tests/layouts/automationsDocs.test.ts b/src/tests/layouts/automationsDocs.test.ts index e7b2da1e..53496b1b 100644 --- a/src/tests/layouts/automationsDocs.test.ts +++ b/src/tests/layouts/automationsDocs.test.ts @@ -88,14 +88,18 @@ describe.each(Object.entries(pages))('automations guides (%s)', (_locale, page) const autoFlat = autoBody.replace(/\s+/g, ' '); const hooksFlat = hooksBody.replace(/\s+/g, ' '); - test('sit in the guides group, right after Agent runs', () => { + test('sit in the guides group, right after Agent runs and its child', () => { const runs = parseFrontmatter(read(page.dir + 'runs.mdx')).frontmatter as { order: number }; const a = parseFrontmatter(auto).frontmatter as { order: number; group: string; title: string }; const h = parseFrontmatter(hooks).frontmatter as { order: number; group: string; title: string }; expect([a.title, h.title]).toEqual(page.titles); expect(a.group).toBe(page.group); expect(h.group).toBe(page.group); - expect(a.order).toBe(runs.order + 1); + // Ephemeral runs is nested under Agent runs and takes the slot after it. + const eph = parseFrontmatter(read(page.dir + 'ephemeral-runs.mdx')).frontmatter as { order: number; parent: string }; + expect(eph.parent).toBe('runs'); + expect(eph.order).toBe(runs.order + 1); + expect(a.order).toBe(runs.order + 2); // The PR gatekeeper guide, the Secrets page and the Notifications page // are nested under Automations (frontmatter `parent`), so they take the // three slots right after it and Webhooks follows. @@ -340,8 +344,13 @@ describe.each(Object.entries(pages))('automations guides (%s)', (_locale, page) } }); - test('ephemeral executors are not documented', () => { - for (const body of [autoBody, hooksBody]) expect(body).not.toMatch(/ephemeral|ef[ií]mer/i); + // Ephemeral executors live on their own page (ephemeral-runs.mdx); this + // guide only points at it, once, from the introduction. + test('ephemeral executors are only a link to their own page', () => { + expect(hooksBody).not.toMatch(/ephemeral|ef[ií]mer/i); + const withoutLinks = autoBody.replace(/\[[^\]]*\]\([^)]*ephemeral-runs[^)]*\)/g, ''); + expect(withoutLinks).not.toMatch(/ephemeral|ef[ií]mer/i); + expect(autoBody).toContain(`(${page.prefix}/docs/ephemeral-runs#`); }); // BYOK (AGENT-AUTOMATIONS-PHASE3-BYOK.md): portal labels from the cloud-ui diff --git a/src/tests/layouts/ephemeralRunsDocs.test.ts b/src/tests/layouts/ephemeralRunsDocs.test.ts new file mode 100644 index 00000000..56ced86d --- /dev/null +++ b/src/tests/layouts/ephemeralRunsDocs.test.ts @@ -0,0 +1,178 @@ +import { describe, expect, test } from 'vitest'; +import { existsSync, readFileSync } from 'node:fs'; +import { fileURLToPath } from 'node:url'; +import { dirname, resolve } from 'node:path'; +import { parseFrontmatter } from '@astrojs/markdown-remark'; +import { mdxToText } from '../../lib/mdxToText'; + +/** + * The Ephemeral runs page (ephemeral executors, E2 docs). + * + * The facts pinned here come from: + * - devops AGENT-EPHEMERAL-EXECUTORS.md §5.5 and §6 (owner decisions + * 2026-10-19: prices, free S-minutes per plan, the S-minute explanation + * that must be shown wherever usage is, no EU-only claim). + * - cloud-api origin/main: internal/runs/ephemeral.go (cap and concurrency + * defaults and bounds, reservation = timeout + 150 s, per started minute, + * UTC calendar month), internal/runs/validate.go (model and github_repo + * required, workspace/cwd/git_isolation refused, ad-hoc runs never get + * secrets), internal/runs/api.go (402 ephemeral_cap_reached, + * 403 token_scope_denied), internal/pat/scopes.go (runs:ephemeral), + * cloudflare/ephemeral-runner/README.md (egress rules and byte caps). + */ +const here = dirname(fileURLToPath(import.meta.url)); +const src = resolve(here, '../..'); +const read = (p: string) => readFileSync(resolve(src, p), 'utf-8'); +const strip = (raw: string) => raw.replace(/^---[\s\S]*?\n---\n/, ''); + +const pages = { + en: { dir: 'content/docs/', prefix: '', group: 'Guides', title: 'Ephemeral runs' }, + es: { dir: 'content/docs-es/', prefix: '/es', group: 'Guías', title: 'Ejecuciones efímeras' }, +} as const; + +/** Heading ids as the site renders them (github-slugger, as in runsDocs.test.ts). */ +const slug = (h: string) => + h + .toLowerCase() + .replace(/[^\p{L}\p{N}\s_-]/gu, '') + .replace(/ /g, '-'); + +function headingIds(body: string): Set { + const ids = new Set(); + let inFence = false; + for (const line of body.split('\n')) { + if (line.startsWith('```')) inFence = !inFence; + const m = !inFence && /^#{1,6}\s+(.*)$/.exec(line); + if (m) ids.add(slug(m[1].replace(/`/g, ''))); + } + return ids; +} + +/** No vendor or infrastructure names in member copy, and no EU-only claim. */ +const INTERNAL = /cloudflare|durable object|worker|firecracker|micro-?vms?\b|\bvm\b|kubernetes|\bk8s\b|litellm|nan-vmd|systemd|\bvault\b|analytics engine/i; +const EU_CLAIM = /\bEU\b|europe|europa|\bUE\b/i; + +describe.each(Object.entries(pages))('ephemeral runs guide (%s)', (_locale, page) => { + const raw = read(page.dir + 'ephemeral-runs.mdx'); + const fm = parseFrontmatter(raw).frontmatter as { title?: string; group?: string; parent?: string; order?: number }; + const body = strip(raw); + const flat = body.replace(/\s+/g, ' '); + + test('is nested under Agent runs, in the guides group', () => { + expect(fm.title).toBe(page.title); + expect(fm.group).toBe(page.group); + expect(fm.parent).toBe('runs'); + }); + + test('publishes the confirmed sizes and prices, per started minute', () => { + for (const row of [/\| \*\*S\*\* \| 1 \| 4 GiB \| 10 GB \| (€0\.003|0,003 €) \|/, /\| \*\*M\*\* \| 2 \| 8 GiB \| 16 GB \| (€0\.006|0,006 €) \|/, /\| \*\*L\*\* \| 4 \| 12 GiB \| 20 GB \| (€0\.012|0,012 €) \|/]) + expect(body).toMatch(row); + expect(flat).toMatch(/per started minute|por minuto empezado/); + expect(flat).toMatch(/14 (seconds|segundos) (counts as|cuenta como) 1/); + }); + + test('free minutes per plan, explained as S-minutes with the worked example', () => { + expect(body).toMatch(/\| Community \| 100 \|/); + expect(body).toMatch(/\| Inference \| 300 \|/); + expect(body).toMatch(/\| Premium \| 1000 \|/); + expect(flat).toMatch(/Free minutes are S-minutes|Los minutos gratis son minutos S/); + expect(flat).toMatch(/\| \*\*M\*\* \| 2 /); + expect(flat).toMatch(/\| \*\*L\*\* \| 4 /); + expect(flat).toMatch(/100 (free minutes are|minutos gratis son) 100 (minutes of|minutos de) \*\*S\*\*, (or|o) 50 (of|de) \*\*M\*\*, (or|o) 25 (of|de) \*\*L\*\*/); + // 100 M (200) + 30 L (120) = 320 S-minutes; 20 over at the S price. + expect(flat).toMatch(/20 × (€0\.003|0,003 €) = \*\*(€0\.06|0,06 €)\*\*/); + }); + + test('says billing is not active yet', () => { + expect(body).toMatch(//); + }); + + test('documents the member limits with their defaults and ranges', () => { + expect(body).toMatch(/\| (€100|100 €) \| (€0 to €1000|De 0 € a 1000 €) \|/); + expect(body).toMatch(/\| 50 \| (1 to 50|De 1 a 50) \|/); + expect(body).toContain('`402 ephemeral_cap_reached`'); + expect(flat).toContain('Settings → Ephemeral runs'); + // Reservation: ceil((1800 + 150) / 60) = 33 minutes of S. + expect(flat).toMatch(/33 (minutes|minutos), (€0\.099|0,099 €)/); + }); + + test('network: HTTP and HTTPS only, no SSH or SMTP, the byte caps', () => { + expect(flat).toMatch(/HTTP (and|y) HTTPS/); + expect(flat).toContain('SSH'); + expect(flat).toContain('SMTP'); + expect(flat).toMatch(/2 GiB/); + expect(flat).toMatch(/512 MiB/); + expect(body).toContain('`egress_limit`'); + }); + + test('the CLI, the API and the token scope', () => { + expect(body).toContain('nan run --ephemeral --repo acme/api --model'); + expect(body).toContain('--size s'); + expect(body).toContain('"executor": "ephemeral"'); + expect(body).toContain('"github_repo": "acme/api"'); + expect(body).not.toMatch(/"workspace":/); + expect(body).toContain('`runs:ephemeral`'); + expect(body).toContain('`403 token_scope_denied`'); + }); + + test('automations: executor, size, fresh checkout, and private repositories need a GITHUB_TOKEN', () => { + expect(body).toContain('executor: ephemeral'); + expect(body).toContain('checkout: fresh'); + expect(flat).toMatch(/`GITHUB_TOKEN` (with|con) \*\*Agent \(read access\)\*\*/); + expect(flat).toMatch(/never receives secrets|nunca recibe secretos/); + }); + + test('the API example is valid JSON', () => { + const json = /-d '(\{[\s\S]*?\})'/.exec(body)![1]; + const req = JSON.parse(json); + expect(req).toMatchObject({ executor: 'ephemeral', size: 's', github_repo: 'acme/api' }); + expect(req.model).toBeTruthy(); + }); + + test('no vendor names, no EU-only claim, no em-dashes', () => { + expect(body).not.toMatch(INTERNAL); + expect(body).not.toMatch(EU_CLAIM); + expect(body).not.toContain('—'); + }); + + test('in-page anchors point at headings that exist', () => { + const ids = headingIds(body); + const anchors = [...body.matchAll(/\]\(#([^)]+)\)/g)].map((m) => m[1]); + expect(anchors.length).toBeGreaterThan(0); + for (const a of anchors) expect(ids.has(a), a).toBe(true); + }); + + test('links to other docs pages land on pages and headings that exist, in the same locale', () => { + const links = [...body.matchAll(/\]\((\/[^)\s]+)\)/g)].map((m) => m[1]).filter((l) => l.includes('/docs/')); + expect(links.length).toBeGreaterThan(5); + for (const l of links) { + const m = new RegExp(`^${page.prefix}/docs/([\\w-]+)(?:#(.+))?$`).exec(l); + expect(m, `${l} in the wrong locale`).not.toBeNull(); + const [, target, anchor] = m!; + const file = [`${target}.mdx`, `${target}.md`].map((f) => resolve(src, page.dir, f)).find((f) => existsSync(f)); + expect(file, l).toBeDefined(); + if (anchor) expect(headingIds(strip(readFileSync(file!, 'utf-8'))).has(anchor), l).toBe(true); + } + }); + + test('Agent runs and Automations link to it', () => { + expect(read(page.dir + 'runs.mdx')).toContain(`(${page.prefix}/docs/ephemeral-runs)`); + expect(read(page.dir + 'automations.mdx')).toMatch(new RegExp(`\\(${page.prefix}/docs/ephemeral-runs#`)); + }); + + test('is served by /api/docs without errors', async () => { + const text = await mdxToText(body); + expect(text).toContain(page.title); + }); +}); + +describe('ephemeral runs guide, both locales', () => { + test('the Spanish page is a translation with the same structure', () => { + const [en, es] = [pages.en, pages.es].map((p) => strip(read(p.dir + 'ephemeral-runs.mdx'))); + const shape = (b: string) => (b.match(/^#{1,3} /gm) ?? []).join(''); + expect(shape(es)).toEqual(shape(en)); + expect(es).not.toEqual(en); + const count = (b: string, re: RegExp) => (b.match(re) ?? []).length; + for (const re of [/```bash/g, /```yaml/g, /
Date: Mon, 19 Oct 2026 12:15:03 +0200 Subject: [PATCH 2/3] docs(ephemeral): E3 spend cap, live and monthly counters, exact portal labels, payment method and EUR - Spend cap: only chargeable money counts (open runs' and the new run's worst case minus the free minutes left, never below zero); a EUR 0 cap allows only runs the free minutes fully cover (cloud-api #379). - Live counter on a running ephemeral run, and this month's S-minutes, free S-minutes left and chargeable euros on the Runs page and in Settings -> Ephemeral runs. - Exact labels: "Ephemeral container", "Monthly spend cap (EUR)", "Concurrent ephemeral runs" (kept in English, bold, in ES). - Owner decisions: a payment method is always required (402 payment_method_required, "Add a payment method"), free minutes only with a membership (none = 0), EUR only (402 currency_migration_required, switch in Settings -> Billing), ephemeral automations without a workspace. Co-Authored-By: Claude Opus 5.5 --- src/content/docs-es/ephemeral-runs.mdx | 35 ++++++++++++++------- src/content/docs/ephemeral-runs.mdx | 35 ++++++++++++++------- src/tests/layouts/ephemeralRunsDocs.test.ts | 25 +++++++++++++++ 3 files changed, 73 insertions(+), 22 deletions(-) diff --git a/src/content/docs-es/ephemeral-runs.mdx b/src/content/docs-es/ephemeral-runs.mdx index e9a71c64..b60f2fff 100644 --- a/src/content/docs-es/ephemeral-runs.mdx +++ b/src/content/docs-es/ephemeral-runs.mdx @@ -16,7 +16,7 @@ Una **ejecución efímera** es un [run de agente](/es/docs/runs) que no necesita | | Run en un workspace | Ejecución efímera | |---|---|---| | Dónde trabaja | En tu workspace, junto a tus archivos | En un contenedor nuevo, solo para ese run | -| Qué necesita | Un workspace encendido con el agente instalado | Un repositorio de GitHub y un modelo | +| Qué necesita | Un workspace encendido con el agente instalado | Un repositorio de GitHub, un modelo y un método de pago | | Sobre qué trabaja | Una carpeta de tu workspace | Un clon nuevo del repositorio | | Qué queda al terminar | Tus archivos, y una rama con sus cambios | Nada: el contenedor se destruye | | Qué cuesta | Va incluido en tu workspace | Por minuto empezado, según el tamaño, después de tus minutos gratis | @@ -30,12 +30,13 @@ Usa una ejecución efímera cuando: Usa un run en un [workspace](/es/docs/workspaces) cuando el agente necesite tus propios archivos, tus herramientas configuradas a mano o cambios que sigan ahí después del run: una ejecución efímera no guarda nada. -Las ejecuciones efímeras se están abriendo poco a poco. Si no ves **Ephemeral** al crear un run, tu cuenta todavía no la tiene: la API responde como si la opción no existiera. +Las ejecuciones efímeras se están abriendo poco a poco. Si no ves **Ephemeral container** al crear un run, tu cuenta todavía no la tiene: la API responde como si la opción no existiera. ## Antes de empezar - **Un repositorio de GitHub**, como `owner/name`, por ejemplo `acme/api`. Un run que lanzas tú clona repositorios **públicos**. Para uno privado, usa una [automatización](#repositorios-privados) con un token de GitHub. +- **Un método de pago registrado**, siempre, aunque tus minutos gratis cubran todos los runs. Añádelo en **Settings → Ephemeral runs** con **Add a payment method** (el formulario **New run** también lo ofrece). Consulta [Método de pago y moneda](#método-de-pago-y-moneda). - **Un modelo.** No hay configuración de workspace a la que recurrir, así que toda ejecución efímera indica su modelo. - **Un plan con inferencia, o tu propia clave de proveedor.** Con la inferencia de NaN, los tokens cuentan para el uso de tu plan como en cualquier otro run. Con [tu propia clave de proveedor](/es/docs/runs#usa-tu-propia-clave-de-proveedor-byok), los factura tu proveedor, y puede usarla cualquier miembro con una suscripción activa. @@ -55,7 +56,7 @@ El agente trabaja en su clon: **Folder** y **Separate branch** no aplican, y no ## Lanza una desde el portal -Abre **New run** y pon **Where it runs** en **Ephemeral**. Después rellena: +Abre **New run** desde la página **Runs** del portal ([cloud.nan.builders/runs](https://cloud.nan.builders/runs)), o desde la pestaña **Runs** de un workspace, y pon **Where it runs** en **Ephemeral container**. En la página **Runs**, sin workspace, es la única opción. Después rellena: | Campo | Qué es | |---|---| @@ -69,6 +70,8 @@ Abre **New run** y pon **Where it runs** en **Ephemeral**. Después rellena: La lista de runs muestra dónde se ejecutó cada uno y, cuando termina una ejecución efímera, lo que costó. +Mientras una ejecución efímera trabaja, su página muestra un contador en directo: **Elapsed** (tiempo transcurrido), **Minutes billed** (minutos empezados), **S-minutes** (lo que gasta de tus minutos gratis, según el tamaño) y **Estimated cost** (coste estimado, antes de los minutos gratis). La página **Runs** y **Settings → Ephemeral runs** muestran los totales del mes: **S-minutes**, **Free S-minutes left** (minutos S gratis que quedan) y **Chargeable so far** (lo cobrable hasta ahora), en euros. Consulta [Minutos gratis](#minutos-gratis). + ## Desde la terminal ```bash @@ -133,7 +136,7 @@ Un run con tu propia clave de proveedor lanzado por ti sigue necesitando una ses ## En automatizaciones -Una [automatización](/es/docs/automations) también puede ejecutarse en contenedores efímeros: cada run que lanza, con **Run now**, un webhook o una programación, tiene su propio contenedor. En el editor, pon **Where it runs** en **Ephemeral** y elige el **Size**. Entonces necesita: +Una [automatización](/es/docs/automations) también puede ejecutarse en contenedores efímeros: cada run que lanza, con **Run now**, un webhook o una programación, tiene su propio contenedor. En el editor, pon **Where it runs** en **Ephemeral container** y elige el **Size**. No necesitas workspace para ello: sin ningún workspace, tus automatizaciones solo pueden ser efímeras. Entonces necesita: - Un **GitHub repository**, y **Checkout** en **Fresh clone per run**: no hay carpeta de workspace en la que trabajar. - Un **Model**. @@ -180,13 +183,16 @@ Las ejecuciones efímeras se miden y se muestran, en el portal, la CLI y la API, ## Minutos gratis -Cada mes tienes minutos gratis, según tu plan: +Cada mes tu membresía incluye minutos gratis: | Plan | Minutos gratis al mes | |---|---| | Community | 100 | | Inference | 300 | | Premium | 1000 | +| Sin membresía | 0 | + +Los minutos gratis solo vienen con una membresía. Sin ella (por ejemplo, con solo un slot de workspace), se cobra cada minuto. **Los minutos gratis son minutos S.** Sirven para cualquier tamaño, pero un minuto de un tamaño mayor gasta más: @@ -200,7 +206,12 @@ Así, 100 minutos gratis son 100 minutos de **S**, o 50 de **M**, o 25 de **L**, **Un ejemplo.** Con el plan Inference tienes 300 minutos gratis. Este mes ejecutas 100 minutos de **M** (200 minutos gratis) y 30 minutos de **L** (120 minutos gratis): 320 en total. Los primeros 300 son gratis, y los 20 minutos S que sobran cuestan 20 × 0,003 € = **0,06 €**. -Los minutos gratis se gastan primero y se renuevan el día 1 de cada mes (UTC). Los que no uses no se acumulan. El `cost` de cada run es el precio completo de sus minutos; los minutos gratis se cuentan sobre el mes, en **Settings → Ephemeral runs** y en `nan ephemeral usage`. +Los minutos gratis se gastan primero y se renuevan el día 1 de cada mes (UTC). Los que no uses no se acumulan. El `cost` de cada run es el precio completo de sus minutos; los minutos gratis se cuentan sobre el mes. La página **Runs** y **Settings → Ephemeral runs** muestran los **S-minutes** del mes, los **Free S-minutes left** y el **Chargeable so far**; también `nan ephemeral usage`. + +## Método de pago y moneda + +- **Hace falta un método de pago** para usar ejecuciones efímeras, también con minutos gratis. Sin él, el run se rechaza con `402 payment_method_required`. Añádelo en **Settings → Ephemeral runs** con **Add a payment method**; el formulario **New run** ofrece el mismo botón. +- **Las ejecuciones efímeras se facturan solo en euros.** Si tu membresía se factura en dólares, cambia antes tu facturación a EUR en **Settings → Billing**. Hasta entonces, los runs se rechazan con `402 currency_migration_required`. ## Límite de gasto y concurrencia @@ -208,12 +219,12 @@ Dos límites te protegen de sorpresas. Se cambian en el portal, en **Settings | Límite | Por defecto | Rango | |---|---|---| -| **Límite de gasto mensual** | 100 € | De 0 € a 1000 € | -| **Ejecuciones efímeras a la vez** | 50 | De 1 a 50 | +| **Monthly spend cap (€)** (límite de gasto mensual) | 100 € | De 0 € a 1000 € | +| **Concurrent ephemeral runs** (ejecuciones efímeras a la vez) | 50 | De 1 a 50 | -**El límite de gasto** se comprueba antes de que arranque cada run. Lo gastado este mes después de tus minutos gratis, más lo máximo que podría costar cada ejecución efímera aún abierta, más lo máximo que podría costar la nueva, tiene que caber en el límite. Lo máximo que puede costar un run es su tiempo límite más 2,5 minutos, al precio de su tamaño: un run de 30 minutos en **S** reserva 33 minutos, 0,099 €. Si no cabe, el run se rechaza con `402 ephemeral_cap_reached`. Sube el límite, espera a que terminen los runs en marcha, o usa un tiempo límite más corto o un tamaño menor. La comprobación usa el precio completo de los runs abiertos, aunque al final los cubran tus minutos gratis: un límite de 0 € frena todas las ejecuciones efímeras. +**El límite de gasto** se comprueba antes de que arranque cada run, y solo cuenta el dinero que se cobraría. NaN suma lo cobrable de este mes a lo máximo que podrían costar las ejecuciones efímeras aún abiertas y la nueva, menos los minutos gratis que te quedan (nunca por debajo de cero), y eso tiene que caber en el límite. Lo máximo que puede costar un run es su tiempo límite más 2,5 minutos, al precio de su tamaño: un run de 30 minutos en **S** reserva 33 minutos, 0,099 €. Si no cabe, el run se rechaza con `402 ephemeral_cap_reached`. Sube el límite, espera a que terminen los runs en marcha, o usa un tiempo límite más corto o un tamaño menor. Un límite de 0 € solo permite los runs que tus minutos gratis cubren por completo. -**Ejecuciones a la vez.** Los runs que superan tu límite no se rechazan: esperan en la cola y arrancan por orden. Los límites habituales para crear runs siguen aplicando. +**Concurrent ephemeral runs.** Los runs que superan tu límite no se rechazan: esperan en la cola y arrancan por orden. Los límites habituales para crear runs siguen aplicando. ## Red @@ -238,6 +249,8 @@ Así que clona por HTTPS, no por SSH, y descarga las dependencias de registros q | Qué ves | Por qué, y qué hacer | |---|---| | `402 ephemeral_cap_reached` | El run podría llevar tu gasto por encima de tu límite mensual. Súbelo en **Settings → Ephemeral runs**, espera a que terminen los runs en marcha, o usa un tiempo límite más corto o un tamaño menor. | +| `402 payment_method_required` | No tienes un método de pago registrado. Añade uno en **Settings → Ephemeral runs** con **Add a payment method**. | +| `402 currency_migration_required` | Tu membresía se factura en dólares. Cambia tu facturación a EUR en **Settings → Billing**. | | `400 invalid_request`, `param: executor` | Las ejecuciones efímeras aún no están disponibles para tu cuenta. | | `400 invalid_request`, `param: github_repo` | Falta el repositorio o no es `owner/name`. | | `400 invalid_request`, `param: model` | Una ejecución efímera tiene que indicar su modelo. | @@ -263,7 +276,7 @@ No desde el contenedor: se destruye cuando termina el run. Usa una [automatizaci
-En **Settings → Ephemeral runs** en el portal, con `nan ephemeral usage`, y en cada run (su `cost`). +En la página **Runs** y en **Settings → Ephemeral runs** del portal (los minutos S del mes, los minutos S gratis que quedan y los euros cobrables), con `nan ephemeral usage`, y en cada run: un contador en directo mientras trabaja, y su `cost` cuando termina.
diff --git a/src/content/docs/ephemeral-runs.mdx b/src/content/docs/ephemeral-runs.mdx index 7002c3ca..1016f19d 100644 --- a/src/content/docs/ephemeral-runs.mdx +++ b/src/content/docs/ephemeral-runs.mdx @@ -16,7 +16,7 @@ An **ephemeral run** is an [agent run](/docs/runs) that does not need a workspac | | Run in a workspace | Ephemeral run | |---|---|---| | Where it works | In your workspace, next to your files | In a fresh container, only for that run | -| What it needs | A running workspace with the agent installed | A GitHub repository and a model | +| What it needs | A running workspace with the agent installed | A GitHub repository, a model and a payment method | | What it works on | A folder of your workspace | A fresh clone of the repository | | What is left when it ends | Your files, and a branch with its changes | Nothing: the container is destroyed | | What it costs | It is part of your workspace | Per started minute, by size, after your free minutes | @@ -30,12 +30,13 @@ Use an ephemeral run when: Use a run in a [workspace](/docs/workspaces) when the agent needs your own files, your tools set up by hand, or changes that stay after the run: an ephemeral run keeps nothing. -Ephemeral runs are being opened gradually. If you do not see **Ephemeral** when you create a run, your account does not have it yet: the API answers as if the option did not exist. +Ephemeral runs are being opened gradually. If you do not see **Ephemeral container** when you create a run, your account does not have it yet: the API answers as if the option did not exist. ## Before you start - **A GitHub repository**, as `owner/name`, for example `acme/api`. A run you start yourself clones **public** repositories. For a private one, use an [automation](#private-repositories) with a GitHub token. +- **A payment method on file**, always, even if your free minutes cover every run. Add it in **Settings → Ephemeral runs** with **Add a payment method** (the **New run** form offers it too). See [Payment method and currency](#payment-method-and-currency). - **A model.** There is no workspace configuration to fall back on, so every ephemeral run names its model. - **A plan with inference, or your own provider key.** On NaN inference, the tokens count against your plan's usage like any other run. On [your own provider key](/docs/runs#use-your-own-provider-key-byok), your provider bills them, and any member with an active subscription can use it. @@ -55,7 +56,7 @@ The agent works in its clone: **Folder** and **Separate branch** do not apply, a ## Start one from the portal -Open **New run** and set **Where it runs** to **Ephemeral**. Then fill in: +Open **New run** from the **Runs** page of the portal ([cloud.nan.builders/runs](https://cloud.nan.builders/runs)), or from the **Runs** tab of a workspace, and set **Where it runs** to **Ephemeral container**. On the **Runs** page, with no workspace, it is the only option. Then fill in: | Field | What it is | |---|---| @@ -69,6 +70,8 @@ Open **New run** and set **Where it runs** to **Ephemeral**. Then fill in: The run list shows where each run ran and, once an ephemeral run ends, what it cost. +While an ephemeral run works, its page shows a live counter: **Elapsed**, **Minutes billed** (per started minute), **S-minutes** (what it uses of your free minutes, by size) and **Estimated cost** (before free minutes). The **Runs** page and **Settings → Ephemeral runs** show this month's totals: **S-minutes**, **Free S-minutes left** and **Chargeable so far**, in euros. See [Free minutes](#free-minutes). + ## From the terminal ```bash @@ -133,7 +136,7 @@ A run on your own provider key started by you still needs a signed-in session, a ## In automations -An [automation](/docs/automations) can run on ephemeral containers too: every run it starts, by **Run now**, a webhook or a schedule, gets its own container. In the editor, set **Where it runs** to **Ephemeral** and pick the **Size**. It then needs: +An [automation](/docs/automations) can run on ephemeral containers too: every run it starts, by **Run now**, a webhook or a schedule, gets its own container. In the editor, set **Where it runs** to **Ephemeral container** and pick the **Size**. You do not need a workspace for it: without any workspace, your automations can only be ephemeral. It then needs: - A **GitHub repository**, and **Checkout** set to **Fresh clone per run**: there is no workspace folder to work in. - A **Model**. @@ -180,13 +183,16 @@ Ephemeral runs are metered and shown, in the portal, the CLI and the API, but no ## Free minutes -Every month you get free minutes, according to your plan: +Every month your membership comes with free minutes: | Plan | Free minutes per month | |---|---| | Community | 100 | | Inference | 300 | | Premium | 1000 | +| No membership | 0 | + +Free minutes come only with a membership. Without one (for example, with only a workspace slot), every minute is charged. **Free minutes are S-minutes.** They work for every size, but a minute of a bigger size uses more of them: @@ -200,7 +206,12 @@ So 100 free minutes are 100 minutes of **S**, or 50 of **M**, or 25 of **L**, or **A worked example.** On the Inference plan you have 300 free minutes. This month you run 100 minutes of **M** (200 free minutes) and 30 minutes of **L** (120 free minutes): 320 in total. The first 300 are free, and the 20 S-minutes left over cost 20 × €0.003 = **€0.06**. -Free minutes are used first and reset on the 1st of each month (UTC). Unused ones do not carry over. The `cost` of each run is the full price of its minutes; the free minutes are counted over the month, in **Settings → Ephemeral runs** and in `nan ephemeral usage`. +Free minutes are used first and reset on the 1st of each month (UTC). Unused ones do not carry over. The `cost` of each run is the full price of its minutes; the free minutes are counted over the month. The **Runs** page and **Settings → Ephemeral runs** show this month's **S-minutes**, **Free S-minutes left** and **Chargeable so far**; so does `nan ephemeral usage`. + +## Payment method and currency + +- **A payment method is required** to use ephemeral runs at all, free minutes included. Without one, a run is refused with `402 payment_method_required`. Add it in **Settings → Ephemeral runs** with **Add a payment method**; the **New run** form offers the same button. +- **Ephemeral runs are billed in euros only.** If your membership is billed in US dollars, switch your billing to EUR first in **Settings → Billing**. Until then, runs are refused with `402 currency_migration_required`. ## Spend cap and concurrency @@ -208,12 +219,12 @@ Two limits protect you from surprises. You change them in the portal, under **Se | Limit | Default | Range | |---|---|---| -| **Monthly spend cap** | €100 | €0 to €1000 | -| **Ephemeral runs at once** | 50 | 1 to 50 | +| **Monthly spend cap (€)** | €100 | €0 to €1000 | +| **Concurrent ephemeral runs** | 50 | 1 to 50 | -**The spend cap** is checked before each run starts. This month's spend after your free minutes, plus the most every ephemeral run still open could cost, plus the most the new one could cost, must stay within the cap. The most a run can cost is its time limit plus 2.5 minutes, at its size price: a 30 minute run of **S** reserves 33 minutes, €0.099. If it does not fit, the run is refused with `402 ephemeral_cap_reached`. Raise the cap, wait for running runs to end, or use a shorter time limit or a smaller size. The check uses the full price of the open runs, even if your free minutes will end up covering them: a cap of €0 stops every ephemeral run. +**The spend cap** is checked before each run starts, and only money that would be charged counts. NaN adds this month's chargeable spend to the most the ephemeral runs still open and the new one could cost, minus the free minutes you still have (never below zero), and that must stay within the cap. The most a run can cost is its time limit plus 2.5 minutes, at its size price: a 30 minute run of **S** reserves 33 minutes, €0.099. If it does not fit, the run is refused with `402 ephemeral_cap_reached`. Raise the cap, wait for running runs to end, or use a shorter time limit or a smaller size. A cap of €0 allows only the runs your free minutes fully cover. -**Runs at once.** Runs beyond your limit are not refused: they wait in the queue and start in order. The usual limits on creating runs still apply. +**Concurrent ephemeral runs.** Runs beyond your limit are not refused: they wait in the queue and start in order. The usual limits on creating runs still apply. ## Network @@ -238,6 +249,8 @@ So clone over HTTPS, not SSH, and fetch dependencies from registries that serve | What you see | Why, and what to do | |---|---| | `402 ephemeral_cap_reached` | The run could take your spend past your monthly cap. Raise it in **Settings → Ephemeral runs**, wait for running runs to end, or use a shorter time limit or a smaller size. | +| `402 payment_method_required` | No payment method on file. Add one in **Settings → Ephemeral runs** with **Add a payment method**. | +| `402 currency_migration_required` | Your membership is billed in US dollars. Switch your billing to EUR in **Settings → Billing**. | | `400 invalid_request`, `param: executor` | Ephemeral runs are not available for your account yet. | | `400 invalid_request`, `param: github_repo` | The repository is missing or is not `owner/name`. | | `400 invalid_request`, `param: model` | An ephemeral run must name its model. | @@ -263,7 +276,7 @@ Not from the container: it is destroyed when the run ends. Use an [automation](#
-In **Settings → Ephemeral runs** on the portal, with `nan ephemeral usage`, and on each run (its `cost`). +On the **Runs** page and in **Settings → Ephemeral runs** on the portal (this month's S-minutes, free S-minutes left and chargeable euros), with `nan ephemeral usage`, and on each run: a live counter while it works, and its `cost` when it ends.
diff --git a/src/tests/layouts/ephemeralRunsDocs.test.ts b/src/tests/layouts/ephemeralRunsDocs.test.ts index 56ced86d..835b8e95 100644 --- a/src/tests/layouts/ephemeralRunsDocs.test.ts +++ b/src/tests/layouts/ephemeralRunsDocs.test.ts @@ -96,6 +96,31 @@ describe.each(Object.entries(pages))('ephemeral runs guide (%s)', (_locale, page expect(flat).toMatch(/33 (minutes|minutos), (€0\.099|0,099 €)/); }); + // E3 (cloud-api #379): only chargeable money counts against the cap, so a + // EUR 0 cap still admits runs the free S-minutes fully cover. + test('spend cap counts only chargeable money; a EUR 0 cap allows fully free runs', () => { + expect(flat).toMatch(/only money that would be charged counts|solo cuenta el dinero que se cobraría/); + expect(flat).toMatch(/never below zero|nunca por debajo de cero/); + expect(flat).toMatch(/A cap of €0 allows only the runs your free minutes fully cover|Un límite de 0 € solo permite los runs que tus minutos gratis cubren por completo/); + expect(flat).not.toMatch(/stops every ephemeral run|frena todas las ejecuciones/); + }); + + test('uses the exact portal labels (cloud-ui EPHEMERAL_LABEL, EphemeralSection, counters)', () => { + expect(body).toContain('**Where it runs** '); + expect(body).toContain('**Ephemeral container**'); + expect(body).not.toMatch(/\*\*Ephemeral\*\*/); + for (const label of ['Monthly spend cap (€)', 'Concurrent ephemeral runs', 'Add a payment method', 'Elapsed', 'Minutes billed', 'S-minutes', 'Estimated cost', 'Free S-minutes left', 'Chargeable so far']) + expect(body, label).toContain(`**${label}**`); + }); + + test('owner decisions: payment method always, membership-only free minutes, EUR only, no workspace needed', () => { + expect(body).toContain('`402 payment_method_required`'); + expect(body).toContain('`402 currency_migration_required`'); + expect(body).toMatch(/\| (No membership|Sin membresía) \| 0 \|/); + expect(flat).toMatch(/billed in euros only|se facturan solo en euros/); + expect(flat).toMatch(/without any workspace, your automations can only be ephemeral|sin ningún workspace, tus automatizaciones solo pueden ser efímeras/); + }); + test('network: HTTP and HTTPS only, no SSH or SMTP, the byte caps', () => { expect(flat).toMatch(/HTTP (and|y) HTTPS/); expect(flat).toContain('SSH'); From ba16777dbec9462ae5a873487e2a03e0d39b98d7 Mon Sep 17 00:00:00 2001 From: barckcode Date: Mon, 19 Oct 2026 12:43:43 +0200 Subject: [PATCH 3/3] docs(ephemeral): match the final portal copy of cloud-ui #177 Started minutes / Minutes billed by billing state, the automation editor's measured (not charged yet) / billed per container minute, the cost note's free S-minutes converted to the chosen size, and Switch billing to EUR in Settings -> Ephemeral runs and the New run form (EN, English labels in ES). Co-Authored-By: Claude Opus 5.5 --- src/content/docs-es/ephemeral-runs.mdx | 10 ++++++---- src/content/docs/ephemeral-runs.mdx | 10 ++++++---- src/tests/layouts/ephemeralRunsDocs.test.ts | 11 ++++++++++- 3 files changed, 22 insertions(+), 9 deletions(-) diff --git a/src/content/docs-es/ephemeral-runs.mdx b/src/content/docs-es/ephemeral-runs.mdx index b60f2fff..81527982 100644 --- a/src/content/docs-es/ephemeral-runs.mdx +++ b/src/content/docs-es/ephemeral-runs.mdx @@ -68,9 +68,11 @@ Abre **New run** desde la página **Runs** del portal ([cloud.nan.builders/runs] | **Time limit** | Como en cualquier run: 15 min, 30 min (por defecto), 1 hora o 2 horas. | | **Inference** | **NaN** o **My own key**, como en cualquier run. | +Debajo del formulario, una nota de coste muestra lo máximo que podría costar el run si agota su tiempo límite, y tus minutos S gratis que quedan convertidos al tamaño elegido, por ejemplo `80 free S-minutes left this month = 40 minutes of M.` (80 minutos S gratis este mes = 40 minutos de M). + La lista de runs muestra dónde se ejecutó cada uno y, cuando termina una ejecución efímera, lo que costó. -Mientras una ejecución efímera trabaja, su página muestra un contador en directo: **Elapsed** (tiempo transcurrido), **Minutes billed** (minutos empezados), **S-minutes** (lo que gasta de tus minutos gratis, según el tamaño) y **Estimated cost** (coste estimado, antes de los minutos gratis). La página **Runs** y **Settings → Ephemeral runs** muestran los totales del mes: **S-minutes**, **Free S-minutes left** (minutos S gratis que quedan) y **Chargeable so far** (lo cobrable hasta ahora), en euros. Consulta [Minutos gratis](#minutos-gratis). +Mientras una ejecución efímera trabaja, su página muestra un contador en directo: **Elapsed** (tiempo transcurrido), **Started minutes** (minutos empezados; pasa a llamarse **Minutes billed** cuando el cobro está activo), **S-minutes** (lo que gasta de tus minutos gratis, según el tamaño) y **Estimated cost** (coste estimado, antes de los minutos gratis). La página **Runs** y **Settings → Ephemeral runs** muestran los totales del mes: **S-minutes**, **Free S-minutes left** (minutos S gratis que quedan) y **Chargeable so far** (lo cobrable hasta ahora), en euros. Consulta [Minutos gratis](#minutos-gratis). ## Desde la terminal @@ -136,7 +138,7 @@ Un run con tu propia clave de proveedor lanzado por ti sigue necesitando una ses ## En automatizaciones -Una [automatización](/es/docs/automations) también puede ejecutarse en contenedores efímeros: cada run que lanza, con **Run now**, un webhook o una programación, tiene su propio contenedor. En el editor, pon **Where it runs** en **Ephemeral container** y elige el **Size**. No necesitas workspace para ello: sin ningún workspace, tus automatizaciones solo pueden ser efímeras. Entonces necesita: +Una [automatización](/es/docs/automations) también puede ejecutarse en contenedores efímeros: cada run que lanza, con **Run now**, un webhook o una programación, tiene su propio contenedor. En el editor, pon **Where it runs** en **Ephemeral container** y elige el **Size**. No necesitas workspace para ello: sin ningún workspace, tus automatizaciones solo pueden ser efímeras. Mientras el cobro no está activo, el editor dice que sus runs se miden por minuto de contenedor (`measured per container minute (not charged yet)`); cuando esté activo, que se cobran por minuto de contenedor (`billed per container minute`). Entonces necesita: - Un **GitHub repository**, y **Checkout** en **Fresh clone per run**: no hay carpeta de workspace en la que trabajar. - Un **Model**. @@ -211,7 +213,7 @@ Los minutos gratis se gastan primero y se renuevan el día 1 de cada mes (UTC). ## Método de pago y moneda - **Hace falta un método de pago** para usar ejecuciones efímeras, también con minutos gratis. Sin él, el run se rechaza con `402 payment_method_required`. Añádelo en **Settings → Ephemeral runs** con **Add a payment method**; el formulario **New run** ofrece el mismo botón. -- **Las ejecuciones efímeras se facturan solo en euros.** Si tu membresía se factura en dólares, cambia antes tu facturación a EUR en **Settings → Billing**. Hasta entonces, los runs se rechazan con `402 currency_migration_required`. +- **Las ejecuciones efímeras se facturan solo en euros.** Si tu membresía se factura en dólares, pásala antes a EUR con **Switch billing to EUR**, en **Settings → Ephemeral runs** o en el formulario **New run**. Hasta entonces, los runs se rechazan con `402 currency_migration_required`. ## Límite de gasto y concurrencia @@ -250,7 +252,7 @@ Así que clona por HTTPS, no por SSH, y descarga las dependencias de registros q |---|---| | `402 ephemeral_cap_reached` | El run podría llevar tu gasto por encima de tu límite mensual. Súbelo en **Settings → Ephemeral runs**, espera a que terminen los runs en marcha, o usa un tiempo límite más corto o un tamaño menor. | | `402 payment_method_required` | No tienes un método de pago registrado. Añade uno en **Settings → Ephemeral runs** con **Add a payment method**. | -| `402 currency_migration_required` | Tu membresía se factura en dólares. Cambia tu facturación a EUR en **Settings → Billing**. | +| `402 currency_migration_required` | Tu membresía se factura en dólares. Usa **Switch billing to EUR** en **Settings → Ephemeral runs** o en el formulario **New run**. | | `400 invalid_request`, `param: executor` | Las ejecuciones efímeras aún no están disponibles para tu cuenta. | | `400 invalid_request`, `param: github_repo` | Falta el repositorio o no es `owner/name`. | | `400 invalid_request`, `param: model` | Una ejecución efímera tiene que indicar su modelo. | diff --git a/src/content/docs/ephemeral-runs.mdx b/src/content/docs/ephemeral-runs.mdx index 1016f19d..d9f96cf8 100644 --- a/src/content/docs/ephemeral-runs.mdx +++ b/src/content/docs/ephemeral-runs.mdx @@ -68,9 +68,11 @@ Open **New run** from the **Runs** page of the portal ([cloud.nan.builders/runs] | **Time limit** | As for any run: 15 min, 30 min (the default), 1 hour or 2 hours. | | **Inference** | **NaN** or **My own key**, as for any run. | +Below the form, a cost note shows the most the run could cost if it uses its whole time limit, and your free S-minutes left converted to the size you picked, for example `80 free S-minutes left this month = 40 minutes of M.` + The run list shows where each run ran and, once an ephemeral run ends, what it cost. -While an ephemeral run works, its page shows a live counter: **Elapsed**, **Minutes billed** (per started minute), **S-minutes** (what it uses of your free minutes, by size) and **Estimated cost** (before free minutes). The **Runs** page and **Settings → Ephemeral runs** show this month's totals: **S-minutes**, **Free S-minutes left** and **Chargeable so far**, in euros. See [Free minutes](#free-minutes). +While an ephemeral run works, its page shows a live counter: **Elapsed**, **Started minutes** (per started minute; it reads **Minutes billed** once billing is active), **S-minutes** (what it uses of your free minutes, by size) and **Estimated cost** (before free minutes). The **Runs** page and **Settings → Ephemeral runs** show this month's totals: **S-minutes**, **Free S-minutes left** and **Chargeable so far**, in euros. See [Free minutes](#free-minutes). ## From the terminal @@ -136,7 +138,7 @@ A run on your own provider key started by you still needs a signed-in session, a ## In automations -An [automation](/docs/automations) can run on ephemeral containers too: every run it starts, by **Run now**, a webhook or a schedule, gets its own container. In the editor, set **Where it runs** to **Ephemeral container** and pick the **Size**. You do not need a workspace for it: without any workspace, your automations can only be ephemeral. It then needs: +An [automation](/docs/automations) can run on ephemeral containers too: every run it starts, by **Run now**, a webhook or a schedule, gets its own container. In the editor, set **Where it runs** to **Ephemeral container** and pick the **Size**. You do not need a workspace for it: without any workspace, your automations can only be ephemeral. While billing is not active, the editor says its runs are `measured per container minute (not charged yet)`; once it is active, `billed per container minute`. It then needs: - A **GitHub repository**, and **Checkout** set to **Fresh clone per run**: there is no workspace folder to work in. - A **Model**. @@ -211,7 +213,7 @@ Free minutes are used first and reset on the 1st of each month (UTC). Unused one ## Payment method and currency - **A payment method is required** to use ephemeral runs at all, free minutes included. Without one, a run is refused with `402 payment_method_required`. Add it in **Settings → Ephemeral runs** with **Add a payment method**; the **New run** form offers the same button. -- **Ephemeral runs are billed in euros only.** If your membership is billed in US dollars, switch your billing to EUR first in **Settings → Billing**. Until then, runs are refused with `402 currency_migration_required`. +- **Ephemeral runs are billed in euros only.** If your membership is billed in US dollars, switch it to EUR first with **Switch billing to EUR**, in **Settings → Ephemeral runs** or in the **New run** form. Until then, runs are refused with `402 currency_migration_required`. ## Spend cap and concurrency @@ -250,7 +252,7 @@ So clone over HTTPS, not SSH, and fetch dependencies from registries that serve |---|---| | `402 ephemeral_cap_reached` | The run could take your spend past your monthly cap. Raise it in **Settings → Ephemeral runs**, wait for running runs to end, or use a shorter time limit or a smaller size. | | `402 payment_method_required` | No payment method on file. Add one in **Settings → Ephemeral runs** with **Add a payment method**. | -| `402 currency_migration_required` | Your membership is billed in US dollars. Switch your billing to EUR in **Settings → Billing**. | +| `402 currency_migration_required` | Your membership is billed in US dollars. Use **Switch billing to EUR** in **Settings → Ephemeral runs** or in the **New run** form. | | `400 invalid_request`, `param: executor` | Ephemeral runs are not available for your account yet. | | `400 invalid_request`, `param: github_repo` | The repository is missing or is not `owner/name`. | | `400 invalid_request`, `param: model` | An ephemeral run must name its model. | diff --git a/src/tests/layouts/ephemeralRunsDocs.test.ts b/src/tests/layouts/ephemeralRunsDocs.test.ts index 835b8e95..20a916bf 100644 --- a/src/tests/layouts/ephemeralRunsDocs.test.ts +++ b/src/tests/layouts/ephemeralRunsDocs.test.ts @@ -109,10 +109,19 @@ describe.each(Object.entries(pages))('ephemeral runs guide (%s)', (_locale, page expect(body).toContain('**Where it runs** '); expect(body).toContain('**Ephemeral container**'); expect(body).not.toMatch(/\*\*Ephemeral\*\*/); - for (const label of ['Monthly spend cap (€)', 'Concurrent ephemeral runs', 'Add a payment method', 'Elapsed', 'Minutes billed', 'S-minutes', 'Estimated cost', 'Free S-minutes left', 'Chargeable so far']) + for (const label of ['Monthly spend cap (€)', 'Concurrent ephemeral runs', 'Add a payment method', 'Switch billing to EUR', 'Elapsed', 'Started minutes', 'Minutes billed', 'S-minutes', 'Estimated cost', 'Free S-minutes left', 'Chargeable so far']) expect(body, label).toContain(`**${label}**`); }); + // cloud-ui #177 (1b964d0): labels that change with billing on/off, and the + // cost note converting free S-minutes to the chosen size. + test('describes the labels in both billing states and the per-size free minutes note', () => { + expect(body).toContain('`measured per container minute (not charged yet)`'); + expect(body).toContain('`billed per container minute`'); + expect(body).toContain('`80 free S-minutes left this month = 40 minutes of M.`'); + expect(body).not.toContain('Settings → Billing'); + }); + test('owner decisions: payment method always, membership-only free minutes, EUR only, no workspace needed', () => { expect(body).toContain('`402 payment_method_required`'); expect(body).toContain('`402 currency_migration_required`');