diff --git a/src/content/docs-es/apps.md b/src/content/docs-es/apps.md index 0a9b3233..0591bb3e 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: 24 +order: 25 group: Guías --- diff --git a/src/content/docs-es/automations.mdx b/src/content/docs-es/automations.mdx index d16fcd81..404d9fad 100644 --- a/src/content/docs-es/automations.mdx +++ b/src/content/docs-es/automations.mdx @@ -86,25 +86,13 @@ Cada automatización tiene cuatro pestañas: **Settings**, **Triggers**, **Runs* ## Secretos -Los secretos son los tokens y claves que usan tus automatizaciones: un token de GitHub, una API key de algún servicio tuyo. Los guardas en tus ajustes y los asignas a las automatizaciones que los necesitan. +Los secretos son los tokens y claves que usan tus automatizaciones: un token de GitHub, una API key de algún servicio tuyo. Los guardas en tus ajustes y los asignas a las automatizaciones que los necesitan. Todo sobre ellos (los dos tipos, el portal, la CLI y la API, el enmascarado, la rotación y los errores que puedes ver) está en [Secretos](/es/docs/secrets). ### Añade un secreto -Abre tus ajustes en el portal, **Secrets**, debajo de **Tokens**, y pulsa **Add secret**. En **Kind**, **Secret** es para los tokens y claves que reciben tus automatizaciones; **Provider key** es para tu propia clave con un proveedor de IA, que se usa como inferencia de runs y automatizaciones: consulta [Usa tu propia clave de proveedor](#usa-tu-propia-clave-de-proveedor-byok). +Abre tus ajustes en el portal, **Secrets**, debajo de **Tokens**, y pulsa **Add secret**. Dale un **Name**, normalmente el nombre de la variable que ven tus automatizaciones (por ejemplo `GITHUB_TOKEN`), y pega el **Value**, hasta 16 KiB. En **Kind**, **Secret** es para los tokens que reciben tus automatizaciones, y **Provider key** para tu propia clave con un proveedor de IA: consulta [Usa tu propia clave de proveedor](#usa-tu-propia-clave-de-proveedor-byok). -- **Name**: el nombre de la variable que ven tus automatizaciones, en mayúsculas, dígitos y `_`, empezando por una letra. Por ejemplo `GITHUB_TOKEN`. Hasta 64 caracteres. -- **Value**: se guarda tal cual lo pegas, hasta 16 KiB. Los saltos de línea dentro del valor están bien (una clave PEM, por ejemplo), pero no al principio ni al final. -- **Description**, opcional. - -Los secretos son de **solo escritura**: una vez guardados, nadie puede volver a leer el valor, ni siquiera tú. La lista muestra el nombre, los 4 últimos caracteres cuando el valor es lo bastante largo, la descripción y qué automatizaciones usan cada uno. Puedes: - -- **Replace value**: el valor nuevo lo usan todos los runs que arranquen a partir de ese momento, incluidos los que ya están en cola. El anterior no se puede recuperar. -- Cambiar su **Description**. -- **Delete**. El borrado se rechaza mientras una automatización lo use, como uno de sus secretos o como su provider key: quítalo antes de esas automatizaciones. - -Puedes guardar hasta 100 secretos. Mejor tokens de vida corta, limitados a lo que necesita la automatización. - -Cada secreto muestra desde dónde se cambió por última vez (**Last changed from**: **Browser**, **CLI** o **API token**), así que un cambio que no hiciste tú salta a la vista. Desde la CLI: `nan secrets set`, `nan secrets edit` y `nan secrets rm`, en [Gestiónalas desde la CLI o la API](#gestiónalas-desde-la-cli-o-la-api). +Los secretos son de **solo escritura**: una vez guardados, nadie puede volver a leer el valor, ni siquiera tú. Puedes usar **Replace value**, cambiar la descripción o borrar uno que no use ninguna automatización. Puedes guardar hasta 100 secretos, y cada uno muestra desde dónde se cambió por última vez (**Last changed from**). Consulta [Gestiona los secretos en el portal](/es/docs/secrets#gestiona-los-secretos-en-el-portal). ### Asigna secretos a una automatización @@ -123,7 +111,7 @@ El mismo nombre de variable puede aparecer una vez por opción. La configuració Una automatización admite hasta 20 secretos, y todos los secretos que recibe un run pueden sumar como mucho 48 KiB. Los secretos solo llegan a los runs de las automatizaciones a las que los asignas: un run que lanzas con `nan run` o desde la pestaña **Runs** de un workspace no recibe ninguno. -En los logs, resultados y propuestas de los runs, todos los valores de los secretos aparecen enmascarados. Un run solo arranca si todos los secretos que necesita están disponibles; si no, falla con `secrets_unavailable` y nada se ejecuta sin ellos. +En los logs, resultados y propuestas de los runs, todos los valores de los secretos aparecen enmascarados: consulta [Enmascarado en logs y eventos](/es/docs/secrets#enmascarado-en-logs-y-eventos). Un run solo arranca si todos los secretos que necesita están disponibles; si no, falla con `secrets_unavailable` y nada se ejecuta sin ellos. Un agente que recibe un secreto puede leerlo, y podría mandarlo a algún sitio si algo le convence de hacerlo. El enmascarado protege nuestros logs, no la red del agente. Dale al agente solo tokens de lectura, deja los tokens de escritura como **Actions only** y limita cada token al repositorio para el que es. @@ -402,13 +390,7 @@ nan secrets edit GITHUB_WRITE_TOKEN --description "expires in December" nan secrets rm GITHUB_READ_TOKEN ``` -- **El valor nunca va en la línea de comandos**, donde se quedaría en el historial de tu shell y otros programas podrían verlo: `--value-file PATH` lo lee de un archivo y `--value-stdin` de una tubería. `--value-stdin` se niega a leer de la terminal, así que nunca se teclea un valor a la vista. -- Se quita un salto de línea al final (los archivos suelen acabar en uno). Aparte de eso, el valor no puede empezar ni terminar con un salto de línea. -- `set` crea el secreto o **reemplaza el valor** del que tenga ese nombre. El tipo, el proveedor y el nombre quedan fijos al crearlo. -- `edit` cambia la descripción, o la URL base de una clave de proveedor compatible con OpenAI. -- `rm` pregunta antes de borrar (si respondes que no, sale con código 1); `--yes` se salta la pregunta en scripts. Se rechaza mientras una automatización use el secreto, y la CLI te dice cuáles. -- Con un token, nombrar un secreto que ya existe necesita `secrets:read` para buscar el nombre; un token solo con `secrets:write` puede crear un secreto nuevo. -- `--json` imprime los metadatos en JSON. +El valor se lee de un archivo (`--value-file PATH`) o de una tubería (`--value-stdin`), nunca de la línea de comandos, y nada lo vuelve a imprimir. `set` crea un secreto o reemplaza su valor, `edit` cambia su descripción o su URL base, y `rm` lo borra cuando ninguna automatización lo usa. Todas las opciones, las credenciales que acepta cada comando y la API están en [Gestiona los secretos desde la CLI](/es/docs/secrets#gestiona-los-secretos-desde-la-cli). ### Automatizaciones desde la CLI @@ -478,7 +460,7 @@ jq -n --rawfile v token.txt '{name: "GITHUB_WRITE_TOKEN", value: ($v | rtrimstr( - Crear un trigger devuelve la URL del webhook y el secreto de firma una sola vez, con `Cache-Control: no-store`; rotarlo, igual. Crear un trigger no es idempotente: si se pierde la respuesta, lista los triggers y rota o borra el que no llegaste a ver. - Demasiados cambios reciben `429 rate_limited`: consulta [Límites](#límites). -Todas las rutas, cuerpos y errores están en [Referencia de la API → Secrets](/es/docs/api#tag/secrets) y [Referencia de la API → Automations](/es/docs/api#tag/automations). +Las rutas de secretos se explican en [Gestiona los secretos desde la API](/es/docs/secrets#gestiona-los-secretos-desde-la-api). Todas las rutas, cuerpos y errores están en [Referencia de la API → Secrets](/es/docs/api#tag/secrets) y [Referencia de la API → Automations](/es/docs/api#tag/automations). ## Tokens para automatizaciones diff --git a/src/content/docs-es/examples.md b/src/content/docs-es/examples.md index 74aa53e5..ff92d520 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: 23 +order: 24 group: Guías --- diff --git a/src/content/docs-es/intro.md b/src/content/docs-es/intro.md index c133281c..427b3473 100644 --- a/src/content/docs-es/intro.md +++ b/src/content/docs-es/intro.md @@ -35,5 +35,6 @@ Los límites van por API key — un tope de peticiones por minuto y un máximo d - [Automatizaciones](/es/docs/automations): guarda una tarea de agente y lánzala desde un webhook, una programación o tus scripts, con las aprobaciones que elijas. - [Webhooks y programaciones](/es/docs/webhooks): conecta GitHub, Stripe o tus propios sistemas, o ejecútala con una programación. - [PR gatekeeper](/es/docs/pr-gatekeeper): una guía paso a paso, con capturas, de una automatización que comprueba cada pull request y deja un comentario con el veredicto. +- [Secretos](/es/docs/secrets): guarda los tokens y claves que usan tus automatizaciones, y tus propias claves de proveedor, cifrados y de solo escritura, desde el portal, la CLI o la API. - [Ejemplos](/es/docs/examples): fragmentos en Python, Node.js y curl. - Soporte: reporta incidencias en `#support` de Discord. diff --git a/src/content/docs-es/nan-cli.mdx b/src/content/docs-es/nan-cli.mdx index 210486e3..c8be9e05 100644 --- a/src/content/docs-es/nan-cli.mdx +++ b/src/content/docs-es/nan-cli.mdx @@ -196,7 +196,7 @@ nan automations disable "PR review" nan automations triggers add "PR review" -f webhook.yaml # imprime la URL y el secreto una vez ``` -El valor de un secreto se lee de un archivo o de una tubería, nunca de la línea de comandos, y nada lo vuelve a imprimir. Mientras tu cuenta no tenga la API de escritura, estos comandos te lo dicen y te mandan al portal. Aprobar propuestas siempre se hace en el portal, con sesión iniciada en tu navegador. +El valor de un secreto se lee de un archivo o de una tubería, nunca de la línea de comandos, y nada lo vuelve a imprimir. Todas las opciones de `nan secrets`, cómo rotar un secreto y los errores que puedes ver están en [Secretos](/es/docs/secrets#gestiona-los-secretos-desde-la-cli). Mientras tu cuenta no tenga la API de escritura, estos comandos te lo dicen y te mandan al portal. Aprobar propuestas siempre se hace en el portal, con sesión iniciada en tu navegador. Estos comandos usan primero tu sesión de `nan auth login` (gana a `NAN_TOKEN`; `--token-file` gana a las dos), o un token de plataforma con los scopes que necesitan: `secrets:read` y `secrets:write` para secretos, `automations:write` (y `automations:read` para usar nombres) para automatizaciones; consulta [Tokens para automatizaciones](/es/docs/automations#tokens-para-automatizaciones). Inicia sesión en tu propia máquina, nunca dentro de un workspace: los agentes de ahí podrían leer la sesión y cambiar tus secretos. Todos los comandos, los archivos YAML y los avisos están en [Gestiónalas desde la CLI o la API](/es/docs/automations#gestiónalas-desde-la-cli-o-la-api). diff --git a/src/content/docs-es/pr-gatekeeper.mdx b/src/content/docs-es/pr-gatekeeper.mdx index 92265020..778636c8 100644 --- a/src/content/docs-es/pr-gatekeeper.mdx +++ b/src/content/docs-es/pr-gatekeeper.mdx @@ -106,7 +106,7 @@ La automatización tiene que hacer dos cosas en GitHub: clonar el repositorio, s Un repositorio público no necesita el token de lectura. -Guarda los dos en el portal, con esos nombres: tus ajustes, **Secrets**, **Add secret**. Un secreto es de solo escritura: una vez guardado, nadie puede volver a leerlo, ni siquiera tú. Consulta [Añade un secreto](/es/docs/automations#añade-un-secreto). +Guarda los dos en el portal, con esos nombres: tus ajustes, **Secrets**, **Add secret**. Un secreto es de solo escritura: una vez guardado, nadie puede volver a leerlo, ni siquiera tú. Consulta [Secretos](/es/docs/secrets) para ver cómo se guardan, se enmascaran y se rotan. El agente ejecuta el código de la pull request, tests incluidos, y puede leer todos los secretos que recibe. Un test podría intentar mandar ese token a otra parte. Mantén el token que puede escribir fuera del alcance del agente: dale el de solo lectura y deja que NaN publique el comentario con el otro. Nuestra configuración de las capturas usa un único token para las dos filas; dos tokens es la opción más segura. diff --git a/src/content/docs-es/runs.mdx b/src/content/docs-es/runs.mdx index d03b5b7f..87417a3f 100644 --- a/src/content/docs-es/runs.mdx +++ b/src/content/docs-es/runs.mdx @@ -149,9 +149,9 @@ Cada run trabaja en su propio proceso dentro del workspace, con su parte de CPU Los runs con la inferencia de NaN usan la clave de inferencia del workspace, la misma que usan sus agentes cuando trabajas con ellos a mano. Sus peticiones cuentan para el uso de tu plan como cualquier otra. Los runs con [tu propia clave de proveedor](#usa-tu-propia-clave-de-proveedor-byok) usan tu clave en su lugar, y nunca reciben la del workspace. -Los secretos del archivo de entorno de tu workspace, como esa clave de inferencia o un token de bot de Telegram, aparecen como `***` en los logs y resultados de los runs, así que un log no los filtra. +Los secretos del archivo de entorno de tu workspace, como esa clave de inferencia o un token de bot de Telegram, aparecen como `[masked]` en los logs y resultados de los runs, así que un log no los filtra. Consulta [Enmascarado en logs y eventos](/es/docs/secrets#enmascarado-en-logs-y-eventos). -Los secretos que guardas en tus ajustes no se les dan a estos runs: solo llegan a los runs de las [automatizaciones](/es/docs/automations#secretos) a las que los asignas. +Los secretos que guardas en tus ajustes no se les dan a estos runs: solo llegan a los runs de las [automatizaciones](/es/docs/automations#secretos) a las que los asignas. Consulta [Secretos](/es/docs/secrets). ## Usa tu propia clave de proveedor (BYOK) @@ -179,24 +179,15 @@ Abre tus ajustes en el portal, **Secrets**, y pulsa **Add secret**. Rellena (el - **Name**: cómo la nombran los runs y las automatizaciones, por ejemplo `ANTHROPIC_KEY`. Mayúsculas, dígitos y `_`, empezando por una letra. - **Kind**: **Provider key**. - **Provider**: **Anthropic**, **OpenAI**, **Google Gemini** u **OpenAI-compatible**. No se puede cambiar después: para cambiar de proveedor, añade otra clave. -- **Base URL**, solo para **OpenAI-compatible**: la raíz del endpoint, que suele acabar en `/v1`, por ejemplo `https://api.together.xyz/v1`. Mira las reglas más abajo. +- **Base URL**, solo para **OpenAI-compatible**: la raíz del endpoint, que suele acabar en `/v1`, por ejemplo `https://api.together.xyz/v1`. Mira las [reglas de la URL base](/es/docs/secrets#reglas-de-la-url-base). - **Value**: la clave, una sola línea sin espacios, de 8 a 4096 caracteres. - **Description (optional)**. Como todo secreto, una provider key es de **solo escritura**: nadie puede volver a leerla, ni siquiera tú. La lista la muestra como **Provider key · Anthropic** (o su proveedor) normalmente con sus 4 últimos caracteres y, en una clave OpenAI-compatible, su URL base, que puedes cambiar con **Base URL**. **Replace value** cambia la clave para todos los runs que arranquen a partir de ese momento. `nan secrets ls` la muestra con **KIND** `provider_key`, su **PROVIDER** y su **BASE URL**. -Si tu cuenta tiene la API de escritura, también puedes guardarla desde la CLI, leyendo la clave de un archivo o de una tubería: `nan secrets set ANTHROPIC_KEY --value-file key.txt --kind provider_key --provider anthropic`. Consulta [Secretos desde la CLI](/es/docs/automations#secretos-desde-la-cli). +Si tu cuenta tiene la API de escritura, también puedes guardarla desde la CLI, leyendo la clave de un archivo o de una tubería: `nan secrets set ANTHROPIC_KEY --value-file key.txt --kind provider_key --provider anthropic`. Consulta [Gestiona los secretos desde la CLI](/es/docs/secrets#gestiona-los-secretos-desde-la-cli). -**Reglas de la URL base.** NaN comprueba la URL base al guardarla y nunca se conecta a ella: solo lo hace el agente de tu workspace. - -- Solo `https`, hasta 512 caracteres. Un puerto vale (`https://llm.example.com:8443/v1`). -- Un nombre de host público (con al menos un punto) o una dirección IP pública. Se rechazan `localhost`, las direcciones privadas y de enlace local, y los nombres que acaban en `.local`, `.internal`, `.lan`, `.home.arpa` o `.svc`. -- No valen los hosts de NaN (`nan.builders`, `helmcode.com` y sus subdominios): para los modelos de NaN, elige **NaN** como inferencia. -- Sin `usuario:contraseña@`, sin parámetros (`?...`) y sin fragmento (`#...`). - - -La clave va en **Value**, donde se guarda cifrada y enmascarada. La URL base no es un secreto: el portal, `nan secrets ls` y la API la muestran tal cual. Una URL con un token dentro se rechaza si lleva `usuario:contraseña@` o parámetros, pero cualquier otra forma de esconder un token en ella quedaría a la vista de quien lea tus ajustes. - +**Reglas de la URL base.** NaN comprueba la URL base al guardarla y nunca se conecta a ella: solo lo hace el agente de tu workspace. Tiene que ser una dirección `https` pública, que no sea uno de los hosts de NaN, sin `usuario:contraseña@`, parámetros ni fragmento: la lista completa está en [Reglas de la URL base](/es/docs/secrets#reglas-de-la-url-base). **No pongas nunca un token en la URL base**: no es un secreto, y el portal, `nan secrets ls` y la API la muestran en claro. ### Elígela para un run @@ -240,7 +231,7 @@ Tu proveedor factura todos los tokens de estos runs, en tu cuenta con él. NaN n ### Seguridad -- Tu clave se guarda como cualquier [secreto](/es/docs/automations#secretos): cifrada, de solo escritura y enmascarada en logs, resultados y propuestas. +- Tu clave se guarda como cualquier [secreto](/es/docs/secrets): cifrada, de solo escritura y [enmascarada](/es/docs/secrets#enmascarado-en-logs-y-eventos) en logs, resultados y propuestas. - Solo llega al agente del run, dentro de tu propio workspace, como variable de entorno: nunca en la línea de comandos, en un archivo ni en los logs de NaN. El run no recibe la clave de inferencia de NaN de tu workspace. - **NaN nunca llama a tu URL base ni a tu proveedor.** Lo hace el agente de tu workspace, por la red de tu workspace. - El run lee los ajustes de tu agente para conservar tus skills y preferencias, pero usa su propia copia de la configuración, con solo el proveedor de tu clave: tus archivos de configuración no cambian. diff --git a/src/content/docs-es/secrets.mdx b/src/content/docs-es/secrets.mdx new file mode 100644 index 00000000..4008939c --- /dev/null +++ b/src/content/docs-es/secrets.mdx @@ -0,0 +1,325 @@ +--- +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 +group: Guías +parent: automations +--- + +import Callout from '../../components/docs/Callout.astro'; + +# Secretos. + +Un **secreto** es un token o una clave que guardas en tu cuenta de NaN para que tus [automatizaciones](/es/docs/automations) lo usen sin que tengas que pegarlo en ningún sitio: un token de GitHub, una API key de uno de tus servicios o tu propia clave con un proveedor de IA. Lo guardas una vez y luego lo eliges por su nombre en las automatizaciones, runs y ajustes que lo necesitan. + +Los secretos son de **solo escritura**. Una vez guardado, nadie puede volver a leer un valor: ni el portal, ni la CLI, ni la API, ni tú. Puedes reemplazar un valor, nunca verlo. + +Para añadir uno ya, ve a [Añade un secreto](#añade-un-secreto). + +## Qué es un secreto + +Cada secreto tiene: + +| Parte | Qué es | +|---|---| +| **Name** | Cómo lo eliges, por ejemplo `GITHUB_TOKEN`. Mayúsculas, dígitos y `_`, empezando por una letra, hasta 64 caracteres, único en tu cuenta. No se puede renombrar: añade un secreto nuevo y borra el antiguo. | +| **Kind** | **Secret** o **Provider key**. Consulta [Dos tipos](#dos-tipos). Se fija al crearlo. | +| **Value** | El propio token o clave. No se vuelve a mostrar nunca. | +| **Description** | Opcional, para ti: para qué es, qué repositorio, cuándo caduca. Hasta 256 caracteres, en una línea. Nunca pongas aquí el valor. | + +**Dónde vive.** El valor se cifra con una clave que pertenece solo a tu cuenta antes de guardarse, y solo se conserva el valor cifrado. Se descifra únicamente cuando arranca un run que lo ha recibido, y solo los secretos que ese run ha recibido. NaN nunca envía un secreto a ningún sitio para comprobarlo: un token erróneo o caducado se nota cuando un run lo usa. + +**Qué ves en lugar del valor.** La lista del portal, `nan secrets ls` y la API muestran solo los metadatos: + +- Una **pista**: los 4 últimos caracteres, como `…a1b2`, cuando el valor tiene al menos 20 caracteres y esos 4 son letras, dígitos, `_` o `-`. Si no, no hay pista. NaN no guarda ningún otro rastro del valor, ni siquiera un hash. +- El tamaño del valor, la descripción y, en una provider key, su proveedor y su URL base. +- **Used by**: las automatizaciones que lo usan (o **Not used by any automation**). +- **Updated**, **Last used** (o **never**) y **Last changed from**. **Last used** se actualiza cuando un run recibe el secreto, como mucho una vez por minuto. + +## Dos tipos + +| Tipo | En el portal | Para qué sirve | +|---|---|---| +| `generic` | **Secret** | Un token o un valor que tus automatizaciones reciben como variable de entorno: un token de GitHub, un webhook de Slack, una API key de un servicio tuyo. | +| `provider_key` | **Provider key** | Tu propia clave con un proveedor de IA, que se usa como inferencia de [runs](/es/docs/runs#usa-tu-propia-clave-de-proveedor-byok) y [automatizaciones](/es/docs/automations#usa-tu-propia-clave-de-proveedor-byok) en lugar de la inferencia de NaN (trae tu propia clave, BYOK). | + +### Secret + +El valor se guarda tal cual lo pegas, hasta 16 KiB: nunca se recorta, así que una clave pegada nunca cambia sin que te enteres. Tiene que ser texto (UTF-8), sin caracteres de control salvo tabuladores y saltos de línea. Los saltos de línea dentro del valor están bien (una clave privada PEM, por ejemplo), pero no al principio ni al final. + +### Provider key + +Una provider key tiene además un **Provider**, que se fija al crearla (para cambiar de proveedor, añade otra clave): + +| Proveedor | En el portal | `provider` | +|---|---|---| +| Anthropic | **Anthropic** | `anthropic` | +| OpenAI | **OpenAI** | `openai` | +| Google Gemini | **Google Gemini** | `gemini` | +| Cualquier endpoint que hable la API de OpenAI (Together, Groq, OpenRouter, tu propio servidor) | **OpenAI-compatible** | `openai_compatible` | + +Su valor es la clave: una línea sin espacios, de 8 a 4096 caracteres ASCII normales. Una clave **OpenAI-compatible** tiene además una **Base URL**: la raíz del endpoint, que suele acabar en `/v1`, por ejemplo `https://api.together.xyz/v1`. Es lo único de un secreto que puedes cambiar además del valor y la descripción. + +La lista la muestra como **Provider key · Anthropic** (o su proveedor), con su URL base cuando la tiene. + +### Reglas de la URL base + +NaN comprueba la URL base cuando la guardas, y nunca se conecta a ella: solo lo hace el agente de tu workspace. + +- Solo `https`, hasta 512 caracteres. Un puerto está bien (`https://llm.example.com:8443/v1`). +- Un nombre de host público (con al menos un punto) o una dirección IP pública. Se rechazan `localhost`, las direcciones privadas y de enlace local, y los nombres que acaban en `.local`, `.internal`, `.lan`, `.home.arpa` o `.svc`. +- No los hosts de NaN (`nan.builders`, `helmcode.com` y sus subdominios): para los modelos de NaN, elige NaN como inferencia. +- Sin `user:password@`, sin query string (`?...`), sin fragmento (`#...`) y sin espacios. + + +La clave va en **Value**, donde se guarda cifrada y enmascarada. La URL base no es un secreto: el portal, `nan secrets ls` y la API la muestran tal cual, a ti y a cualquier credencial que pueda leer los metadatos de tus secretos. Una URL con un token dentro se rechaza cuando lleva `user:password@` o una query string, pero cualquier otra forma de esconder un token en ella (un segmento de la ruta, por ejemplo) se mostraría en claro. + + +## Gestiona los secretos en el portal + +Tus secretos están en tus ajustes de [cloud.nan.builders](https://cloud.nan.builders/): abre **Settings** y luego **Secrets**, debajo de **Tokens**. + +### Añade un secreto + +1. Pulsa **Add secret** y escribe el **Name**. +2. Elige el **Kind**: **Secret** para un token que reciben tus automatizaciones, **Provider key** para tu propia clave de un proveedor de IA. En una provider key, elige el **Provider** y, para **OpenAI-compatible**, escribe la **Base URL**. +3. Pega el **Value**: hasta 16 KiB en un secreto, una línea de 8 a 4096 caracteres en una provider key. +4. Añade una **Description** si quieres y pulsa **Save secret**. + +Si el guardado falla, el portal borra el valor por seguridad ("The value was cleared for safety. Paste it again to retry."): corrige lo que te indica y vuelve a pegarlo. + +### Reemplaza el valor + +**Replace value**, en la fila del secreto, pide el valor nuevo. El anterior se sobrescribe y no se puede recuperar. El nombre, la descripción y todas las automatizaciones que usan el secreto se quedan como están: [Cuando reemplazas o borras un secreto](#cuando-reemplazas-o-borras-un-secreto) explica qué runs reciben el valor nuevo. + +### Cambia la descripción o la URL base + +**Description** edita la descripción. **Base URL** (solo en una provider key **OpenAI-compatible**) edita su URL base, con las mismas [reglas](#reglas-de-la-url-base). El nombre, el tipo y el proveedor no se pueden cambiar. + +### Borra un secreto + +**Delete** te pide confirmación, y el secreto desaparece para siempre. Mientras una automatización lo use, como uno de sus secretos o como su provider key, el borrado se rechaza y el portal lista esas automatizaciones: quítalo primero de ellas. + +## Gestiona los secretos desde la CLI + +Con la [CLI de NaN](/es/docs/nan-cli) **0.1.33** o posterior, si tu cuenta tiene la API de escritura (se está abriendo poco a poco): + +```bash +nan secrets ls # nombres y metadatos, nunca valores +nan secrets show GITHUB_TOKEN # los metadatos de un secreto +nan secrets set GITHUB_WRITE_TOKEN --value-file ./token.txt --description "repo acme/api, write" +gh auth token | nan secrets set GITHUB_READ_TOKEN --value-stdin +nan secrets set ANTHROPIC_KEY --value-file key.txt --kind provider_key --provider anthropic +nan secrets set TOGETHER_KEY --value-file key.txt --kind provider_key \ + --provider openai_compatible --base-url https://api.together.xyz/v1 +nan secrets edit GITHUB_WRITE_TOKEN --description "expires in December" +nan secrets rm GITHUB_READ_TOKEN +``` + +- **El valor nunca va en la línea de comandos**, donde se quedaría en el historial de tu shell y lo verían otros programas de tu máquina. `--value-file PATH` lo lee de un archivo, `--value-stdin` de una tubería, y `--value-stdin` se niega a leer de la terminal, así que un valor nunca se teclea en claro. Si has escrito un valor en la línea de comandos por error, ya está en tu historial: revócalo donde se emitió y guarda uno nuevo. +- Si otros usuarios de tu máquina pueden leer el archivo, la CLI te avisa (hazle `chmod 600`). +- Se quita un salto de línea al final (los archivos suelen acabar con uno). +- `set` crea el secreto, o **reemplaza el valor** del que tiene ese nombre. `--kind` (`generic` o `provider_key`), `--provider` (`anthropic`, `openai`, `gemini` o `openai_compatible`) y el nombre se fijan al crearlo. +- `edit` cambia la descripción (`--description ""` la vacía) o la URL base de una clave OpenAI-compatible. No el valor: eso es `set`. +- `rm` pregunta antes de borrar (si respondes que no, sale con código 1); `--yes` se salta la pregunta en scripts. Se rechaza mientras una automatización use el secreto, y la CLI te dice cuáles. +- `show`, `edit` y `rm` aceptan el nombre del secreto o su id. `ls`, `show`, `set` y `edit` aceptan `--json` e imprimen los metadatos en JSON. +- Hasta que tu cuenta tenga la API de escritura, los comandos que cambian secretos te dicen que uses el portal y salen con código 64. + +La CLI usa la primera credencial que encuentra: `--token-file`, luego tu sesión de `nan auth login`, luego `NAN_TOKEN` y luego un token guardado con `nan auth login --api-token`. Con un token, necesita `secrets:read` para listar secretos o encontrar uno (`show`, `edit` y `rm` buscan antes el secreto, incluso por id), y `secrets:write` para cambiarlos. Un token solo con `secrets:write` puede crear un secreto nuevo con `set`; reemplazar el valor de uno que ya existe necesita los dos. Una API key (`sk-...`) se rechaza antes de enviar nada. + +## Gestiona los secretos desde la API + +Las mismas operaciones están en la API, en `https://api.nan.builders/v1/secrets`: + +| Método y ruta | Scope | Qué hace | +|---|---|---| +| `GET /v1/secrets` | `secrets:read` | Lista tus secretos: solo metadatos. | +| `GET /v1/secrets/{id}` | `secrets:read` | Los metadatos de un secreto. | +| `POST /v1/secrets` | `secrets:write` | Crea un secreto: `{"name", "value", "kind"?, "provider"?, "base_url"?, "description"?}`. `201`, o `200` en un reintento idempotente. | +| `PUT /v1/secrets/{id}/value` | `secrets:write` | Reemplaza el valor: `{"value"}`. | +| `PATCH /v1/secrets/{id}` | `secrets:write` | Cambia `description` y, en una clave OpenAI-compatible, `base_url`. | +| `DELETE /v1/secrets/{id}` | `secrets:write` | Lo borra. `204`. | + +**Qué credenciales sirven:** + +- Tu **sesión de la CLI**, de `nan auth login` en tu propia máquina. +- Un **token de plataforma** con el scope de la ruta. Lo creas en el portal, **Settings → Tokens**, con sesión iniciada en el navegador: un token con un scope `secrets:` no se puede crear desde la CLI ni desde otro token. Los dos scopes son independientes: `secrets:write` no incluye `secrets:read`. Consulta [Tokens para automatizaciones](/es/docs/automations#tokens-para-automatizaciones). +- Nunca una API key (`sk-...`), la clave de dentro de un workspace ni una sesión de una CLI antigua: reciben `403 credential_not_allowed` (`workspace_key_not_allowed` para la clave del workspace). Un token sin el scope recibe `403 token_scope_denied`. + +Si tu cuenta todavía no tiene la API de escritura, todas las rutas de `/v1/secrets` responden `404`. + +Crea un secreto desde un archivo, para que el valor nunca aparezca en el historial de tu shell ni en una lista de procesos: + +```bash +jq -n --rawfile v token.txt '{name: "GITHUB_WRITE_TOKEN", value: ($v | rtrimstr("\n")), description: "repo acme/api, write"}' | + curl https://api.nan.builders/v1/secrets \ + -H "Authorization: Bearer $NAN_TOKEN" \ + -H "Content-Type: application/json" \ + -H "Idempotency-Key: create-github-write-token-1" \ + --data-binary @- +``` + +La respuesta son los metadatos del secreto. Nunca lleva un valor, en ninguna ruta: + +```json +{ + "id": "…", + "object": "secret", + "name": "GITHUB_WRITE_TOKEN", + "kind": "generic", + "provider": null, + "base_url": null, + "hint": "…a1b2", + "value_bytes": 93, + "description": "repo acme/api, write", + "used_by": [{ "id": "…", "name": "PR review api" }], + "updated_by": "platform_token", + "created_at": "…", + "updated_at": "…", + "last_used_at": null +} +``` + +- **Idempotencia.** `Idempotency-Key` (de 1 a 128 caracteres de `A-Z a-z 0-9 . _ : -`) hace seguro reintentar una creación: la misma clave con el mismo nombre, tipo y proveedor devuelve el secreto ya creado (`200`), y el valor del reintento se ignora. La misma clave para un secreto distinto es `409 idempotency_key_reused`. Borrar el secreto libera su clave. Sin la cabecera, un reintento recibe `409 name_taken`. +- **Cuerpo de la petición.** Un objeto JSON de hasta 32 KiB, solo con los campos documentados. +- **Los errores** llegan como `{"error": {"message", "type", "param", "code"}}`. `param` nombra el campo que está mal y nunca repite lo que enviaste. `secret_in_use` y `secrets_too_large` añaden las `automations` afectadas. +- **Límite de ritmo.** Los cambios en secretos (crear, reemplazar, editar, borrar) comparten un mismo cupo por cuenta: una ráfaga de 30 y luego 120 por hora, sumando el portal, la CLI y la API. Por encima, `429 rate_limited`. + +Todas las rutas, cuerpos y errores están en [Referencia de la API → Secrets](/es/docs/api#tag/secrets). + +## Usa secretos en automatizaciones + +Un secreto no hace nada hasta que se lo das a una automatización. Los secretos solo llegan a los runs de las automatizaciones a las que los asignas: un run que lanzas con `nan run` o desde la pestaña **Runs** de un workspace nunca recibe ninguno (salvo la provider key que elijas para ese run). + +### Asigna un secreto + +En la tarjeta **Secrets** de la automatización, **Add a secret** y elige, para cada uno: + +- **El secreto**, por su nombre. +- **El nombre de la variable** con el que lo ve el run. Puede ser distinto del nombre del secreto, y no puede ser un nombre que fije el propio entorno de ejecución: `OPENAI_API_KEY`, `OPENAI_BASE_URL`, `HOME`, `USER`, `PATH`, `LANG`, `CREDENTIALS_DIRECTORY` ni nada que empiece por `NAN_`, `PI_` o `HERMES_`. +- **Quién lo recibe**: + +| Opción | API | Quién recibe el valor | Para qué usarlo | +|---|---|---|---| +| **Agent (read access)** | `agent` | El agente, como variable de entorno, en cada run. | Tokens que el agente necesita para leer: clonar un repositorio privado, traer una pull request, leer logs de CI. | +| **Actions only (write access)** | `actions` | Solo el paso que aplica las acciones que propone una automatización. Ese paso no ejecuta ningún agente ni ningún modelo de IA. **El agente nunca ve un secreto `actions`.** | Tokens de escritura: push, comentar, revisar, etiquetar, abrir pull requests. | + +Desde la CLI o la API, la misma asignación es una línea de los `secrets` de la automatización: + +```yaml +secrets: + - { secret: GITHUB_READ_TOKEN, env: GITHUB_TOKEN, exposure: agent } + - { secret: GITHUB_WRITE_TOKEN, env: GITHUB_TOKEN, exposure: actions } +``` + +El mismo nombre de variable puede aparecer una vez por opción. Una automatización admite hasta 20 secretos, y todo lo que recibe un run (sus secretos y su provider key) puede sumar como mucho 48 KiB. Un run solo arranca si se pueden leer todos los secretos que necesita; si no, falla con `secrets_unavailable` y nada se ejecuta sin ellos. El resto de los ajustes de la automatización están en [Automatizaciones](/es/docs/automations#asigna-secretos-a-una-automatización). + +### El patrón del token de GitHub + +La configuración habitual de GitHub usa dos [fine-grained personal access tokens](https://github.com/settings/personal-access-tokens/new), cada uno limitado al único repositorio en el que trabaja la automatización, con fecha de caducidad: + +| Guárdalo como | Permisos | Asígnalo como | +|---|---|---| +| `GITHUB_READ_TOKEN` | Solo lectura: **Contents**, más **Pull requests**, **Issues** o **Actions** si el agente los lee. | `GITHUB_TOKEN`, **Agent (read access)**. NaN también lo usa para clonar un repositorio privado con **Fresh clone per run**. | +| `GITHUB_WRITE_TOKEN` | Lo que necesitan las acciones: **Contents** de lectura y escritura para hacer push, **Pull requests** de lectura y escritura para abrir, comentar y revisar pull requests, **Issues** de lectura y escritura para comentar y etiquetar. | `GITHUB_TOKEN`, **Actions only (write access)**. Obligatorio en una automatización que actúa sobre un repositorio. | + +Las dos filas usan el nombre de variable `GITHUB_TOKEN`, cada una para su receptor: el agente lee con uno y NaN escribe con el otro. Un repositorio público que el agente solo lee no necesita token de lectura. El detalle de cada permiso está en [Un token de GitHub para las acciones](/es/docs/automations#un-token-de-github-para-las-acciones), y un recorrido completo en la [guía del PR gatekeeper](/es/docs/pr-gatekeeper). + +### Tu propia clave de proveedor + +Una provider key no se asigna como variable: la eliges como inferencia. En un run o una automatización, pon **Inference** en **My own key**, elige la **Provider key** y escribe el **Model**. El agente de ese run recibe la clave en su entorno, como la espera ese agente, y llama con ella a tu proveedor. Desde la CLI, `nan run --byok-secret ANTHROPIC_KEY --model claude-sonnet-4-5 "..."` recibe el **nombre** del secreto, nunca la clave. + +El flujo completo, quién puede usarla, la facturación y sus propios errores están en [Usa tu propia clave de proveedor (BYOK)](/es/docs/runs#usa-tu-propia-clave-de-proveedor-byok) y, para las automatizaciones, en [Usa tu propia clave de proveedor](/es/docs/automations#usa-tu-propia-clave-de-proveedor-byok). + +## Mantén tus secretos a salvo + +### Enmascarado en logs y eventos + +Cuando arranca un run, todos los valores que recibe (sus secretos, su provider key y los secretos del propio archivo de entorno del workspace) se añaden a una máscara. Antes de guardar o mostrar nada de lo que escribe el run, cada aparición se sustituye por `[masked]`: + +- En los eventos y el log del run, su resultado y los textos libres de una propuesta: mensajes de commit, títulos y cuerpos de pull requests, comentarios, reviews y las rutas de los archivos cambiados. +- El propio valor y sus codificaciones habituales: base64, codificado para URL y escapado para JSON. +- El principio de un valor impreso por separado: si el agente imprime los primeros caracteres de un token (`GITHUB_TOKEN=ghp_6Z8A...`), también se enmascara un fragmento de 8 caracteres o más. + +El enmascarado tiene límites. Los valores de menos de 8 caracteres no se enmascaran nunca, así que usa secretos largos de verdad. Pueden escaparse los fragmentos de menos de 8 caracteres, el final de un valor o un fragmento pegado a más letras o dígitos. Un nombre de rama o una etiqueta que contendría un secreto no se reescribe: la propuesta se rechaza (`proposal_invalid`). Si el diff de un push contiene el valor de uno de los secretos del run, el portal te avisa antes de que lo apruebes. + +El enmascarado protege lo que NaN guarda y muestra. No impide que un agente que tiene un secreto lo envíe a algún sitio. + +### Qué puede hacer el agente con un secreto + +Un agente que recibe un secreto como **Agent (read access)** puede leerlo, imprimirlo, usarlo y enviarlo por la red, como cualquier programa que ejecutes con esa variable puesta. Es así a propósito: es como el agente clona, trae cosas y llama a tus servicios. Lo que haga con él es responsabilidad tuya, así que: + +- **Dale al agente solo tokens de lectura.** Deja todo token que pueda escribir como **Actions only**: esos nunca llegan al agente ni a ningún modelo de IA. +- **Limita cada token** al único repositorio o recurso para el que es, con fecha de caducidad. +- **Recuerda que al agente le llega texto escrito por otros.** Una entrega de webhook, una pull request, una issue o un test dentro de una pull request pueden intentar que el agente imprima o envíe un secreto (prompt injection). Por eso **Fresh clone per run** ignora por defecto las pull requests de forks y de colaboradores externos: consulta [Pull requests de forks y de colaboradores externos](/es/docs/automations#pull-requests-de-forks-y-de-colaboradores-externos). +- **En una provider key,** pon un límite de gasto en tu proveedor. + +### El peligro está en la escritura + +Un token que puede **escribir** vale mucho más para quien lo roba que uno que solo puede leer: puede subir código, hacer merge, borrar ramas o cambiar ajustes. + +- **Un token de escritura de GitHub** en manos del agente puede hacer todo lo que le permitan sus permisos, sin pasar por la aprobación de tu automatización. Asígnalo como **Actions only (write access)**, para que solo lo use el paso de aplicación de NaN, y solo para las acciones que permitiste. +- **Un token de plataforma de NaN** con `secrets:write` puede reemplazar cualquiera de tus secretos (por ejemplo, cambiar el token de GitHub con el que hace push una automatización). Uno con `automations:write` puede crear una automatización que reciba todos tus secretos, con las instrucciones que quiera y nada esperando tu aprobación: es como leerlos todos. Da esos scopes solo a los tokens que los necesitan, con una caducidad corta, y nunca se los des a un agente. +- **Ejecuta `nan auth login` en tu propia máquina, nunca dentro de un workspace**: los agentes que corren allí pueden leer la sesión que guarda, y una sesión de la CLI puede cambiar tus secretos. + +### Rótalos + +Mejor tokens de vida corta, y rótalos antes de que caduquen o en cuanto uno pueda haberse filtrado: + +1. Crea el token nuevo en su origen (GitHub, tu proveedor). +2. **Replace value** en el portal, o `nan secrets set NAME --value-file new.txt`. El nombre se mantiene, así que todas las automatizaciones lo siguen usando sin cambiar nada. +3. Revoca el token antiguo en su origen. NaN no puede revocar un token por ti: reemplazarlo en NaN no impide que el antiguo siga funcionando en otros sitios. + +Si un token se ha filtrado, revócalo primero en su origen y luego reemplázalo. Anotar la fecha de caducidad en la **Description** ("expires 2026-12-31") te ayuda a ver qué toca renovar. + +### Cuando reemplazas o borras un secreto + +- **Reemplazar un valor** se aplica al siguiente run que arranque, incluidos los que ya están en cola: un run lee sus secretos cuando arranca. Los runs que ya están trabajando conservan el valor con el que arrancaron. El reemplazo se rechaza con `409 secrets_too_large` si el valor nuevo haría que alguna de las automatizaciones que lo usan pasara de 48 KiB por run. +- **Borrar** se rechaza con `409 secret_in_use` mientras una automatización use el secreto, como uno de sus secretos o como su provider key. La respuesta lista esas automatizaciones: cámbialas antes y luego bórralo. Un run con tu propia clave cuya provider key se borró después de ponerlo en cola falla con `secret_missing`. + +### Quién lo cambió por última vez + +Cada secreto registra desde dónde llegó su último cambio: **Last changed from** en el portal (**Browser**, **CLI** o **API token**), `CHANGED FROM` en `nan secrets ls` y `updated_by` en la API (`browser_session`, `cli_session` o `platform_token`). Un cambio que no hiciste tú salta a la vista: si ves uno, revoca en **Settings → Tokens** los tokens que ya no usas y reemplaza el secreto. + +## Límites + +| Límite | Valor | +|---|---| +| Secretos | 100 por miembro | +| Nombre | Hasta 64 caracteres: mayúsculas, dígitos y `_`, empezando por una letra | +| Valor de un **Secret** | Hasta 16 KiB | +| Valor de una **Provider key** | De 8 a 4096 caracteres, en una línea, sin espacios | +| Descripción | Hasta 256 caracteres | +| URL base | Hasta 512 caracteres | +| Secretos por automatización | 20 | +| Todo lo que recibe un run | 48 KiB, incluida su provider key | +| Cambios en secretos | Una ráfaga de 30 y luego 120 por hora, sumando el portal, la CLI y la API | +| Cuerpo de una petición a la API | 32 KiB | + +Añadir un secreto o reemplazar un valor necesita una membresía activa. Sin ella puedes seguir listando y borrando tus secretos. + +## Si algo sale mal + +| Qué ves | Por qué, y qué hacer | +|---|---| +| `400 invalid_request` con un `param` | Ese campo incumple una regla: `name` (mayúsculas, dígitos y `_`), `value` (tamaño, caracteres de control, un salto de línea al principio o al final, o una provider key con espacios), `kind`, `provider`, `base_url` ([reglas](#reglas-de-la-url-base)), `description` o `Idempotency-Key`. | +| `403 credential_not_allowed` | Una API key (`sk-...`) o una sesión de una CLI antigua. Usa tu sesión de `nan auth login` o un token de plataforma con el scope. | +| `403 workspace_key_not_allowed` | La clave de dentro de un workspace. Nunca lee ni cambia secretos. | +| `403 token_scope_denied` | Al token le falta `secrets:read` o `secrets:write`. Crea uno con el scope en **Settings → Tokens**. | +| `403 browser_session_required`, o **Browser session needed** en el portal | Tu cuenta todavía no tiene la API de escritura, así que los cambios necesitan una sesión del navegador: vuelve a iniciar sesión con el enlace del email desde tu navegador. | +| `403 platform_access_required` | Añadir o reemplazar necesita una membresía activa. Puedes seguir listando y borrando. | +| `404` en todas las rutas de `/v1/secrets` | Tu cuenta todavía no tiene la API de escritura. Usa el portal. | +| `404 not_found` | No hay ningún secreto con ese id en tu cuenta. Compruébalo con `nan secrets ls`. | +| `409 name_taken` | Ya tienes un secreto con ese nombre. Reemplaza su valor, o reintenta la creación con la misma `Idempotency-Key`. | +| `409 too_many_secrets` | Tienes 100. Borra uno para añadir otro. | +| `409 idempotency_key_reused` | Esa `Idempotency-Key` ya creó un secreto distinto. Usa una clave nueva. | +| `409 secret_in_use` | Lo usa una automatización. Quítalo de las automatizaciones que se indican y luego bórralo. | +| `409 secrets_too_large` | El valor nuevo haría que las automatizaciones que se indican pasaran de 48 KiB por run. Usa un valor más corto o quítales otros secretos. | +| `429 rate_limited` | Más de una ráfaga de 30 cambios, o de 120 por hora. Espera y vuelve a intentarlo. | +| `503 secrets_unavailable` | Los secretos no están disponibles por un momento. Vuelve a intentarlo en unos minutos. | +| La CLI sale con código 64 y te dice que uses el portal | Tu cuenta todavía no tiene la API de escritura. Usa el portal. (La CLI también usa el código 64 para la mayoría de los demás rechazos de esta tabla: lee el mensaje que imprime.) | +| La automatización no se puede guardar sin un `GITHUB_TOKEN` | Una automatización con **GitHub repository** necesita un secreto asignado como `GITHUB_TOKEN`, **Actions only (write access)**. | +| Un run falla con `secrets_unavailable` | No se pudieron leer sus secretos al arrancar. Antes se reintenta durante un rato. Lánzalo de nuevo más tarde. | +| Un run falla con `secret_missing` | Un secreto que necesita se borró después de ponerlo en cola: su provider key, o un secreto que se quitó de la automatización y luego se borró. | +| Un run falla con `config_error` | Entre otras causas, sus secretos suman más de 48 KiB. Quita algunos de la automatización. | +| Un run falla con un error de autenticación de GitHub o de tu proveedor | El token es erróneo, ha caducado o se ha revocado, o le falta un permiso. Reemplaza su valor. | diff --git a/src/content/docs-es/webhooks.mdx b/src/content/docs-es/webhooks.mdx index b5bb55f6..cca5ff96 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: 22 +order: 23 group: Guías --- @@ -44,6 +44,8 @@ Pulsa **Add webhook**. El portal te muestra, **una sola vez**: - **Webhook URL**: `https://api.nan.builders/hooks/nan_whk_...`. La propia URL es una credencial: cualquiera que la tenga puede enviarle eventos, así que trátala como una contraseña. - **Signing secret** (GitHub y NaN): con lo que el emisor firma cada evento. Para Stripe solo se muestra la URL: el secreto es el que pegaste. +El signing secret pertenece al trigger, no a tus [Secretos](/es/docs/secrets): no aparece en tu lista de secretos ni cuenta para sus límites, y ningún run lo recibe. Si tu automatización necesita llamar a un servicio tuyo, guarda ese token como secreto y asígnaselo a la automatización. + Cópialos y pulsa **I've saved them**. Después, el portal solo muestra el principio de la URL. Si los pierdes, [rótalos](#rota-la-url-o-el-secreto) para conseguir unos nuevos. Desde la CLI, `nan automations triggers add -f webhook.yaml` imprime esos mismos valores **una sola vez** (en Stripe, solo la URL), con un aviso para que los guardes en ese momento; `--json` los imprime en JSON para un script. A través de la API, `POST /v1/automations/{id}/triggers` los devuelve una sola vez en `webhook_url` y `signing_secret`, con `Cache-Control: no-store`. Nada vuelve a devolverlos: ni el portal, ni `nan automations triggers ls`, ni la API. diff --git a/src/content/docs/apps.md b/src/content/docs/apps.md index cabbf9e1..67d713eb 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: 24 +order: 25 group: Guides --- diff --git a/src/content/docs/automations.mdx b/src/content/docs/automations.mdx index 465eb18f..c9c93a40 100644 --- a/src/content/docs/automations.mdx +++ b/src/content/docs/automations.mdx @@ -86,25 +86,13 @@ Each automation has four tabs: **Settings**, **Triggers**, **Runs** (its runs, n ## Secrets -Secrets are the tokens and keys your automations use: a GitHub token, an API key for a service of yours. You keep them in your settings and assign them to the automations that need them. +Secrets are the tokens and keys your automations use: a GitHub token, an API key for a service of yours. You keep them in your settings and assign them to the automations that need them. Everything about them (the two kinds, the portal, the CLI and the API, masking, rotation and the errors you can see) is in [Secrets](/docs/secrets). ### Add a secret -Open your settings on the portal, **Secrets**, below **Tokens**, and press **Add secret**. **Kind** is **Secret** for the tokens and keys your automations receive; **Provider key** is for your own key with an AI provider, used as the inference of runs and automations: see [Use your own provider key](#use-your-own-provider-key-byok). +Open your settings on the portal, **Secrets**, below **Tokens**, and press **Add secret**. Give it a **Name**, usually the variable name your automations see (for example `GITHUB_TOKEN`), and paste the **Value**, up to 16 KiB. **Kind** is **Secret** for the tokens your automations receive, and **Provider key** for your own key with an AI provider: see [Use your own provider key](#use-your-own-provider-key-byok). -- **Name**: the variable name your automations see, in capitals, digits and `_`, starting with a letter. For example `GITHUB_TOKEN`. Up to 64 characters. -- **Value**: saved exactly as you paste it, up to 16 KiB. Line breaks inside the value are fine (a PEM key, for example), but not at its start or end. -- **Description**, optional. - -Secrets are **write-only**: once saved, nobody can read a value back, not even you. The list shows the name, the last 4 characters when the value is long enough, the description and which automations use each one. You can: - -- **Replace value**: the new value is used by every run that starts from then on, including runs already queued. The old one cannot be recovered. -- Change its **Description**. -- **Delete** it. Deleting is refused while an automation uses it, as one of its secrets or as its provider key: remove it from those automations first. - -You can keep up to 100 secrets. Prefer short-lived tokens limited to what the automation needs. - -Each secret shows where it was last changed from (**Last changed from**: **Browser**, **CLI** or **API token**), so a change you did not make stands out. From the CLI: `nan secrets set`, `nan secrets edit` and `nan secrets rm`, in [Manage them from the CLI or the API](#manage-them-from-the-cli-or-the-api). +Secrets are **write-only**: once saved, nobody can read a value back, not even you. You can **Replace value**, change the description, or delete one that no automation uses. You can keep up to 100 secrets, and each shows where it was last changed from (**Last changed from**). See [Manage secrets in the portal](/docs/secrets#manage-secrets-in-the-portal). ### Assign secrets to an automation @@ -123,7 +111,7 @@ The same variable name can appear once per option. The usual GitHub setup gives An automation takes up to 20 secrets, and all the secrets one run receives must add up to 48 KiB at most. Secrets go only to the runs of the automations you assign them to: a run you start with `nan run` or from a workspace's **Runs** tab never receives any. -In run logs, results and proposals, every secret value is masked. A run only starts if every secret it needs is available; otherwise it fails with `secrets_unavailable` and nothing runs without them. +In run logs, results and proposals, every secret value is masked: see [Masking in logs and events](/docs/secrets#masking-in-logs-and-events). A run only starts if every secret it needs is available; otherwise it fails with `secrets_unavailable` and nothing runs without them. An agent that receives a secret can read it, and could send it somewhere if something convinces it to. Masking protects our logs, not the agent's network. Give the agent only read tokens, keep write tokens as **Actions only**, and limit every token to the repository it is for. @@ -402,13 +390,7 @@ nan secrets edit GITHUB_WRITE_TOKEN --description "expires in December" nan secrets rm GITHUB_READ_TOKEN ``` -- **The value never goes on the command line**, where it would stay in your shell history and be visible to other programs: `--value-file PATH` reads it from a file, `--value-stdin` from a pipe. `--value-stdin` refuses to read from the terminal, so a value is never typed in clear. -- One line break at the end is removed (files usually end with one). The value cannot start or end with a line break otherwise. -- `set` creates the secret, or **replaces the value** of the one with that name. The kind, the provider and the name are fixed when it is created. -- `edit` changes the description, or the base URL of an OpenAI-compatible provider key. -- `rm` asks before deleting (answering no exits with code 1); `--yes` skips the question in scripts. It is refused while an automation uses the secret, and the CLI lists which. -- With a token, naming an existing secret needs `secrets:read` to look the name up; a token with only `secrets:write` can still create a new secret. -- `--json` prints the metadata as JSON. +The value is read from a file (`--value-file PATH`) or a pipe (`--value-stdin`), never from the command line, and nothing prints it back. `set` creates a secret or replaces its value, `edit` changes its description or base URL, and `rm` deletes it once no automation uses it. Every flag, the credentials each command accepts and the API are in [Manage secrets from the CLI](/docs/secrets#manage-secrets-from-the-cli). ### Automations from the CLI @@ -478,7 +460,7 @@ jq -n --rawfile v token.txt '{name: "GITHUB_WRITE_TOKEN", value: ($v | rtrimstr( - Creating a trigger returns the webhook URL and signing secret once, with `Cache-Control: no-store`; so does rotating. Creating a trigger is not idempotent: after a lost answer, list the triggers and rotate or delete the one you did not see. - Too many changes get `429 rate_limited`: see [Limits](#limits). -Every route, body and error is in [API reference → Secrets](/docs/api#tag/secrets) and [API reference → Automations](/docs/api#tag/automations). +The secret routes are explained in [Manage secrets from the API](/docs/secrets#manage-secrets-from-the-api). Every route, body and error is in [API reference → Secrets](/docs/api#tag/secrets) and [API reference → Automations](/docs/api#tag/automations). ## Tokens for automations diff --git a/src/content/docs/examples.md b/src/content/docs/examples.md index 108f42aa..7f9878b0 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: 23 +order: 24 group: Guides --- diff --git a/src/content/docs/intro.md b/src/content/docs/intro.md index bebb08ac..dfb8d6db 100644 --- a/src/content/docs/intro.md +++ b/src/content/docs/intro.md @@ -35,5 +35,6 @@ Limits are per API key — a cap on requests per minute and a maximum number of - [Automations](/docs/automations): save an agent task and start it from a webhook, a schedule or your scripts, with the approvals you choose. - [Webhooks and schedules](/docs/webhooks): connect GitHub, Stripe or your own systems, or run on a schedule. - [PR gatekeeper](/docs/pr-gatekeeper): a step-by-step guide, with screenshots, to an automation that checks every pull request and keeps one comment with the verdict. +- [Secrets](/docs/secrets): keep the tokens and keys your automations use, and your own provider keys, encrypted and write-only, from the portal, the CLI or the API. - [Examples](/docs/examples): snippets in Python, Node.js and curl. - Support: report issues in `#support` on Discord. diff --git a/src/content/docs/nan-cli.mdx b/src/content/docs/nan-cli.mdx index a47c1951..6815cea5 100644 --- a/src/content/docs/nan-cli.mdx +++ b/src/content/docs/nan-cli.mdx @@ -196,7 +196,7 @@ nan automations disable "PR review" nan automations triggers add "PR review" -f webhook.yaml # prints the URL and secret once ``` -A secret's value is read from a file or a pipe, never from the command line, and nothing ever prints it back. Until your account has the write API, these commands say so and point you to the portal. Approving proposals always happens in the portal, signed in through your browser. +A secret's value is read from a file or a pipe, never from the command line, and nothing ever prints it back. Every `nan secrets` flag, how to rotate a secret and the errors you can see are in [Secrets](/docs/secrets#manage-secrets-from-the-cli). Until your account has the write API, these commands say so and point you to the portal. Approving proposals always happens in the portal, signed in through your browser. These commands use your `nan auth login` session first (it wins over `NAN_TOKEN`; `--token-file` wins over both), or a platform token with the scopes they need: `secrets:read` and `secrets:write` for secrets, `automations:write` (plus `automations:read` to use names) for automations; see [Tokens for automations](/docs/automations#tokens-for-automations). Sign in on your own machine, never inside a workspace: the agents there could read the session and change your secrets. Every command, the YAML files and the warnings are in [Manage them from the CLI or the API](/docs/automations#manage-them-from-the-cli-or-the-api). diff --git a/src/content/docs/pr-gatekeeper.mdx b/src/content/docs/pr-gatekeeper.mdx index 9a5b1607..c7e6a101 100644 --- a/src/content/docs/pr-gatekeeper.mdx +++ b/src/content/docs/pr-gatekeeper.mdx @@ -106,7 +106,7 @@ The automation needs to do two things on GitHub: clone the repository, if it is A public repository does not need the read token. -Save both on the portal, with these names: your settings, **Secrets**, **Add secret**. A secret is write-only: once saved, nobody can read it back, not even you. See [Add a secret](/docs/automations#add-a-secret). +Save both on the portal, with these names: your settings, **Secrets**, **Add secret**. A secret is write-only: once saved, nobody can read it back, not even you. See [Secrets](/docs/secrets) for how they are stored, masked and rotated. The agent runs the code of the pull request, tests included, and it can read every secret it receives. A test could try to send that token somewhere. Keep the token that can write out of the agent's hands: give it the read-only one, and let NaN post the comment with the other. Our own setup in the screenshots uses a single token for both rows; two tokens are the safer choice. diff --git a/src/content/docs/runs.mdx b/src/content/docs/runs.mdx index 3a2ad258..daf44c55 100644 --- a/src/content/docs/runs.mdx +++ b/src/content/docs/runs.mdx @@ -149,9 +149,9 @@ Each run works in its own process inside the workspace, with its own share of CP Runs on NaN inference use the inference key of the workspace, the same one its agents use when you work with them by hand. Their requests count against your plan's usage like any other. Runs on [your own provider key](#use-your-own-provider-key-byok) use your key instead, and never get the workspace's. -Secrets in your workspace's environment file, such as that inference key or a Telegram bot token, are shown as `***` in run logs and results, so a log does not leak them. +Secrets in your workspace's environment file, such as that inference key or a Telegram bot token, are shown as `[masked]` in run logs and results, so a log does not leak them. See [Masking in logs and events](/docs/secrets#masking-in-logs-and-events). -The secrets you keep in your settings are not given to these runs: they go only to the runs of the [automations](/docs/automations#secrets) you assign them to. +The secrets you keep in your settings are not given to these runs: they go only to the runs of the [automations](/docs/automations#secrets) you assign them to. See [Secrets](/docs/secrets). ## Use your own provider key (BYOK) @@ -179,24 +179,15 @@ Open your settings on the portal, **Secrets**, and press **Add secret**. Fill in - **Name**: how runs and automations refer to it, for example `ANTHROPIC_KEY`. Capitals, digits and `_`, starting with a letter. - **Kind**: **Provider key**. - **Provider**: **Anthropic**, **OpenAI**, **Google Gemini** or **OpenAI-compatible**. It cannot be changed later: to switch providers, add another key. -- **Base URL**, only for **OpenAI-compatible**: the root of the endpoint, usually ending in `/v1`, for example `https://api.together.xyz/v1`. See the rules below. +- **Base URL**, only for **OpenAI-compatible**: the root of the endpoint, usually ending in `/v1`, for example `https://api.together.xyz/v1`. See the [base URL rules](/docs/secrets#base-url-rules). - **Value**: the key, one line without spaces, 8 to 4096 characters. - **Description (optional)**. Like every secret, a provider key is **write-only**: nobody can read it back, not even you. The list shows it as **Provider key · Anthropic** (or its provider) usually with its last 4 characters, and, for an OpenAI-compatible key, its base URL, which you can change with **Base URL**. **Replace value** changes the key for every run that starts from then on. `nan secrets ls` shows it with **KIND** `provider_key`, its **PROVIDER** and its **BASE URL**. -Where your account has the write API, you can also save it from the CLI, reading the key from a file or a pipe: `nan secrets set ANTHROPIC_KEY --value-file key.txt --kind provider_key --provider anthropic`. See [Secrets from the CLI](/docs/automations#secrets-from-the-cli). +Where your account has the write API, you can also save it from the CLI, reading the key from a file or a pipe: `nan secrets set ANTHROPIC_KEY --value-file key.txt --kind provider_key --provider anthropic`. See [Manage secrets from the CLI](/docs/secrets#manage-secrets-from-the-cli). -**Base URL rules.** NaN checks the base URL when you save it, and never connects to it: only the agent in your workspace does. - -- `https` only, up to 512 characters. A port is fine (`https://llm.example.com:8443/v1`). -- A public host name (with at least one dot) or a public IP address. `localhost`, private and link-local addresses, and names ending in `.local`, `.internal`, `.lan`, `.home.arpa` or `.svc` are refused. -- Not NaN's own hosts (`nan.builders`, `helmcode.com` and their subdomains): for NaN models, choose **NaN** as the inference. -- No `user:password@`, no query string (`?...`) and no fragment (`#...`). - - -The key goes in **Value**, where it is stored encrypted and masked. The base URL is not a secret: the portal, `nan secrets ls` and the API show it as it is. A URL with a token inside is refused when it has `user:password@` or a query string, but any other way of hiding a token in it would be shown to whoever reads your settings. - +**Base URL rules.** NaN checks the base URL when you save it, and never connects to it: only the agent in your workspace does. It must be a public `https` address, not one of NaN's own hosts, with no `user:password@`, query string or fragment: the full list is in [Base URL rules](/docs/secrets#base-url-rules). **Never put a token in the base URL**: it is not a secret, and the portal, `nan secrets ls` and the API show it in clear. ### Choose it for a run @@ -240,7 +231,7 @@ Your provider bills every token of these runs, under your account with them. NaN ### Security -- Your key is stored like any [secret](/docs/automations#secrets): encrypted, write-only and masked in logs, results and proposals. +- Your key is stored like any [secret](/docs/secrets): encrypted, write-only and [masked](/docs/secrets#masking-in-logs-and-events) in logs, results and proposals. - It reaches only the agent of the run, inside your own workspace, as an environment variable: never on the command line, in a file or in NaN's logs. The run does not get your workspace's NaN inference key. - **NaN never calls your base URL or your provider.** The agent in your workspace does, over your workspace's network. - The run reads your agent's settings to keep your skills and preferences, but it uses its own copy of the configuration, with only your key's provider in it: your own configuration files are not changed. diff --git a/src/content/docs/secrets.mdx b/src/content/docs/secrets.mdx new file mode 100644 index 00000000..fe189b78 --- /dev/null +++ b/src/content/docs/secrets.mdx @@ -0,0 +1,325 @@ +--- +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 +group: Guides +parent: automations +--- + +import Callout from '../../components/docs/Callout.astro'; + +# Secrets. + +A **secret** is a token or a key you keep in your NaN account so your [automations](/docs/automations) can use it without you pasting it anywhere: a GitHub token, an API key for one of your services, or your own key with an AI provider. You save it once, and then pick it by name in the automations, runs and settings that need it. + +Secrets are **write-only**. Once saved, nobody can read a value back: not the portal, not the CLI, not the API, not you. You can replace a value, never see it. + +To add one right away, go to [Add a secret](#add-a-secret). + +## What a secret is + +Every secret has: + +| Part | What it is | +|---|---| +| **Name** | How you pick it, for example `GITHUB_TOKEN`. Capitals, digits and `_`, starting with a letter, up to 64 characters, unique in your account. It cannot be renamed: add a new secret and delete the old one. | +| **Kind** | **Secret** or **Provider key**. See [Two kinds](#two-kinds). Fixed when you create it. | +| **Value** | The token or key itself. Never shown again. | +| **Description** | Optional, for you: what it is for, which repository, when it expires. Up to 256 characters, one line. Never put the value here. | + +**Where it lives.** The value is encrypted with a key that belongs to your account alone before it is stored, and only the encrypted value is kept. It is decrypted only when a run that was given it starts, and only the secrets that run was given. NaN never sends a secret anywhere to check it: a wrong or expired token shows up when a run uses it. + +**What you see instead of the value.** The list in the portal, `nan secrets ls` and the API show the metadata only: + +- A **hint**: the last 4 characters, as `…a1b2`, when the value has at least 20 characters and those 4 are letters, digits, `_` or `-`. Otherwise no hint. NaN keeps no other trace of the value, not even a hash. +- The size of the value, the description and, for a provider key, its provider and base URL. +- **Used by**: the automations that use it (or **Not used by any automation**). +- **Updated**, **Last used** (or **never**) and **Last changed from**. **Last used** is updated when a run receives the secret, at most once a minute. + +## Two kinds + +| Kind | In the portal | What it is for | +|---|---|---| +| `generic` | **Secret** | A token or value your automations receive as an environment variable: a GitHub token, a Slack webhook, an API key for your own service. | +| `provider_key` | **Provider key** | Your own key with an AI provider, used as the inference of [runs](/docs/runs#use-your-own-provider-key-byok) and [automations](/docs/automations#use-your-own-provider-key-byok) instead of NaN inference (bring your own key, BYOK). | + +### Secret + +The value is saved exactly as you paste it, up to 16 KiB: it is never trimmed, so a pasted key is never silently changed. It must be text (UTF-8), with no control characters other than tabs and line breaks. Line breaks inside the value are fine (a PEM private key, for example), but not at its start or end. + +### Provider key + +A provider key also has a **Provider**, fixed when you create it (to switch providers, add another key): + +| Provider | In the portal | `provider` | +|---|---|---| +| Anthropic | **Anthropic** | `anthropic` | +| OpenAI | **OpenAI** | `openai` | +| Google Gemini | **Google Gemini** | `gemini` | +| Any endpoint that speaks the OpenAI API (Together, Groq, OpenRouter, your own server) | **OpenAI-compatible** | `openai_compatible` | + +Its value is the key: one line without spaces, 8 to 4096 characters of plain ASCII. An **OpenAI-compatible** key also has a **Base URL**: the root of the endpoint, usually ending in `/v1`, for example `https://api.together.xyz/v1`. It is the only part of a secret you can change besides the value and the description. + +The list shows it as **Provider key · Anthropic** (or its provider), with its base URL when it has one. + +### Base URL rules + +NaN checks the base URL when you save it, and never connects to it: only the agent in your workspace does. + +- `https` only, up to 512 characters. A port is fine (`https://llm.example.com:8443/v1`). +- A public host name (with at least one dot) or a public IP address. `localhost`, private and link-local addresses, and names ending in `.local`, `.internal`, `.lan`, `.home.arpa` or `.svc` are refused. +- Not NaN's own hosts (`nan.builders`, `helmcode.com` and their subdomains): for NaN models, choose NaN as the inference. +- No `user:password@`, no query string (`?...`), no fragment (`#...`) and no spaces. + + +The key goes in **Value**, where it is encrypted and masked. The base URL is not a secret: the portal, `nan secrets ls` and the API show it as it is, to you and to any credential that can read your secrets' metadata. A URL with a token inside is refused when it has `user:password@` or a query string, but any other way of hiding a token in it (a path segment, for example) would be shown in clear. + + +## Manage secrets in the portal + +Your secrets are in your settings at [cloud.nan.builders](https://cloud.nan.builders/): open **Settings**, then **Secrets**, below **Tokens**. + +### Add a secret + +1. Press **Add secret** and type the **Name**. +2. Pick the **Kind**: **Secret** for a token your automations receive, **Provider key** for your own AI provider key. For a provider key, pick the **Provider** and, for **OpenAI-compatible**, type the **Base URL**. +3. Paste the **Value**: up to 16 KiB for a secret, one line of 8 to 4096 characters for a provider key. +4. Add a **Description** if you like, and press **Save secret**. + +If saving fails, the portal clears the value for safety ("The value was cleared for safety. Paste it again to retry."): fix what it says and paste it again. + +### Replace the value + +**Replace value**, on the secret's row, asks for the new value. The old one is overwritten and cannot be recovered. The name, the description and every automation that uses the secret stay as they are: [When you replace or delete a secret](#when-you-replace-or-delete-a-secret) says which runs get the new value. + +### Change the description or the base URL + +**Description** edits the description. **Base URL** (only on an **OpenAI-compatible** provider key) edits its base URL, with the same [rules](#base-url-rules). The name, the kind and the provider cannot be changed. + +### Delete a secret + +**Delete** asks you to confirm, and the secret is gone for good. While an automation uses it, as one of its secrets or as its provider key, deleting is refused and the portal lists those automations: remove it from them first. + +## Manage secrets from the CLI + +With the [NaN CLI](/docs/nan-cli) **0.1.33** or newer, where your account has the write API (it is being opened gradually): + +```bash +nan secrets ls # names and metadata, never values +nan secrets show GITHUB_TOKEN # one secret's metadata +nan secrets set GITHUB_WRITE_TOKEN --value-file ./token.txt --description "repo acme/api, write" +gh auth token | nan secrets set GITHUB_READ_TOKEN --value-stdin +nan secrets set ANTHROPIC_KEY --value-file key.txt --kind provider_key --provider anthropic +nan secrets set TOGETHER_KEY --value-file key.txt --kind provider_key \ + --provider openai_compatible --base-url https://api.together.xyz/v1 +nan secrets edit GITHUB_WRITE_TOKEN --description "expires in December" +nan secrets rm GITHUB_READ_TOKEN +``` + +- **The value never goes on the command line**, where it would stay in your shell history and be visible to other programs on your machine. `--value-file PATH` reads it from a file, `--value-stdin` from a pipe, and `--value-stdin` refuses to read from the terminal, so a value is never typed in clear. If you typed a value on the command line by mistake, it is in your history now: revoke it where it was issued and set a new one. +- If the file can be read by other users of your machine, the CLI warns you (`chmod 600` it). +- One line break at the end is dropped (files usually end with one). +- `set` creates the secret, or **replaces the value** of the one with that name. `--kind` (`generic` or `provider_key`), `--provider` (`anthropic`, `openai`, `gemini` or `openai_compatible`) and the name are fixed when it is created. +- `edit` changes the description (`--description ""` clears it) or the base URL of an OpenAI-compatible key. Not the value: that is `set`. +- `rm` asks before deleting (answering no exits with code 1); `--yes` skips the question in scripts. It is refused while an automation uses the secret, and the CLI lists which. +- `show`, `edit` and `rm` take the secret's name or its id. `ls`, `show`, `set` and `edit` take `--json` and print the metadata as JSON. +- Until your account has the write API, the commands that change secrets tell you to use the portal and exit with code 64. + +The CLI uses the first credential it finds: `--token-file`, then your `nan auth login` session, then `NAN_TOKEN`, then a token saved with `nan auth login --api-token`. With a token, it needs `secrets:read` to list secrets or find one (`show`, `edit` and `rm` look the secret up first, even by id), and `secrets:write` to change them. A token with only `secrets:write` can still create a new secret with `set`; replacing the value of an existing one needs both. An API key (`sk-...`) is refused before anything is sent. + +## Manage secrets from the API + +The same operations are in the API, under `https://api.nan.builders/v1/secrets`: + +| Method and path | Scope | What it does | +|---|---|---| +| `GET /v1/secrets` | `secrets:read` | Lists your secrets: metadata only. | +| `GET /v1/secrets/{id}` | `secrets:read` | One secret's metadata. | +| `POST /v1/secrets` | `secrets:write` | Creates a secret: `{"name", "value", "kind"?, "provider"?, "base_url"?, "description"?}`. `201`, or `200` on an idempotent retry. | +| `PUT /v1/secrets/{id}/value` | `secrets:write` | Replaces the value: `{"value"}`. | +| `PATCH /v1/secrets/{id}` | `secrets:write` | Changes `description` and, on an OpenAI-compatible key, `base_url`. | +| `DELETE /v1/secrets/{id}` | `secrets:write` | Deletes it. `204`. | + +**Which credentials work:** + +- Your **CLI session**, from `nan auth login` on your own machine. +- A **platform token** with the scope of the route. You create it in the portal, **Settings → Tokens**, signed in through your browser: a token with a `secrets:` scope cannot be created from the CLI or by another token. The two scopes are independent: `secrets:write` does not include `secrets:read`. See [Tokens for automations](/docs/automations#tokens-for-automations). +- Never an API key (`sk-...`), the key inside a workspace or a session from an older CLI: they get `403 credential_not_allowed` (`workspace_key_not_allowed` for the workspace key). A token without the scope gets `403 token_scope_denied`. + +Where your account does not have the write API yet, every `/v1/secrets` route answers `404`. + +Create a secret from a file, so the value never appears in your shell history or in a process list: + +```bash +jq -n --rawfile v token.txt '{name: "GITHUB_WRITE_TOKEN", value: ($v | rtrimstr("\n")), description: "repo acme/api, write"}' | + curl https://api.nan.builders/v1/secrets \ + -H "Authorization: Bearer $NAN_TOKEN" \ + -H "Content-Type: application/json" \ + -H "Idempotency-Key: create-github-write-token-1" \ + --data-binary @- +``` + +The answer is the secret's metadata. It never carries a value, by any route: + +```json +{ + "id": "…", + "object": "secret", + "name": "GITHUB_WRITE_TOKEN", + "kind": "generic", + "provider": null, + "base_url": null, + "hint": "…a1b2", + "value_bytes": 93, + "description": "repo acme/api, write", + "used_by": [{ "id": "…", "name": "PR review api" }], + "updated_by": "platform_token", + "created_at": "…", + "updated_at": "…", + "last_used_at": null +} +``` + +- **Idempotency.** `Idempotency-Key` (1 to 128 characters of `A-Z a-z 0-9 . _ : -`) makes a retried create safe: the same key with the same name, kind and provider returns the secret already created (`200`), and the retried value is ignored. The same key for a different secret is `409 idempotency_key_reused`. Deleting the secret frees its key. Without the header, a retry gets `409 name_taken`. +- **Request body.** One JSON object of up to 32 KiB, with only the documented fields. +- **Errors** come as `{"error": {"message", "type", "param", "code"}}`. `param` names the field that is wrong and never repeats what you sent. `secret_in_use` and `secrets_too_large` add the `automations` involved. +- **Rate limit.** Changes to secrets (create, replace, edit, delete) share one allowance per account: a burst of 30, then 120 per hour, counting the portal, the CLI and the API together. Beyond it, `429 rate_limited`. + +Every route, body and error is in [API reference → Secrets](/docs/api#tag/secrets). + +## Use secrets in automations + +A secret does nothing until you give it to an automation. Secrets reach only the runs of the automations you assign them to: a run you start with `nan run` or from a workspace's **Runs** tab never receives any (except the provider key you choose for that run). + +### Assign a secret + +In the automation's **Secrets** card, **Add a secret** and pick, for each one: + +- **The secret**, by name. +- **The variable name** the run sees it as. It can differ from the secret's name, and it cannot be a name the runtime sets itself: `OPENAI_API_KEY`, `OPENAI_BASE_URL`, `HOME`, `USER`, `PATH`, `LANG`, `CREDENTIALS_DIRECTORY` or anything starting with `NAN_`, `PI_` or `HERMES_`. +- **Who receives it**: + +| Option | API | Who gets the value | Use it for | +|---|---|---|---| +| **Agent (read access)** | `agent` | The agent, as an environment variable, in every run. | Tokens the agent needs to read things: clone a private repository, fetch a pull request, read CI logs. | +| **Actions only (write access)** | `actions` | Only the step that applies the actions an automation proposes. That step runs no agent and no AI model. **The agent never sees an `actions` secret.** | Write tokens: push, comment, review, label, open pull requests. | + +From the CLI or the API, the same assignment is one line of the automation's `secrets`: + +```yaml +secrets: + - { secret: GITHUB_READ_TOKEN, env: GITHUB_TOKEN, exposure: agent } + - { secret: GITHUB_WRITE_TOKEN, env: GITHUB_TOKEN, exposure: actions } +``` + +The same variable name can appear once per option. An automation takes up to 20 secrets, and everything one run receives (its secrets and its provider key) must add up to 48 KiB at most. A run only starts if every secret it needs can be read; otherwise it fails with `secrets_unavailable` and nothing runs without them. The rest of the automation's settings are in [Automations](/docs/automations#assign-secrets-to-an-automation). + +### The GitHub token pattern + +The usual GitHub setup uses two [fine-grained personal access tokens](https://github.com/settings/personal-access-tokens/new), each limited to the one repository the automation works on, with an expiration date: + +| Save it as | Permissions | Assign it as | +|---|---|---| +| `GITHUB_READ_TOKEN` | Read only: **Contents**, plus **Pull requests**, **Issues** or **Actions** if the agent reads them. | `GITHUB_TOKEN`, **Agent (read access)**. NaN also uses it to clone a private repository with **Fresh clone per run**. | +| `GITHUB_WRITE_TOKEN` | What the actions need: **Contents** read and write to push, **Pull requests** read and write to open, comment on and review pull requests, **Issues** read and write to comment and label. | `GITHUB_TOKEN`, **Actions only (write access)**. Required for an automation that acts on a repository. | + +Both rows use the variable name `GITHUB_TOKEN`, each for its own receiver: the agent reads with one, NaN writes with the other. A public repository the agent only reads needs no read token. The details of each permission are in [A GitHub token for actions](/docs/automations#a-github-token-for-actions), and a complete walkthrough in the [PR gatekeeper guide](/docs/pr-gatekeeper). + +### Your own provider key + +A provider key is not assigned as a variable: you choose it as the inference. In a run or an automation, set **Inference** to **My own key**, pick the **Provider key** and type the **Model**. The agent of that run gets the key in its environment, the way its agent expects it, and calls your provider with it. From the CLI, `nan run --byok-secret ANTHROPIC_KEY --model claude-sonnet-4-5 "..."` takes the secret's **name**, never the key. + +The whole flow, who can use it, billing and its own errors are in [Use your own provider key (BYOK)](/docs/runs#use-your-own-provider-key-byok) and, for automations, [Use your own provider key](/docs/automations#use-your-own-provider-key-byok). + +## Keep your secrets safe + +### Masking in logs and events + +When a run starts, every value it receives (its secrets, its provider key and the secrets of the workspace's own environment file) is added to a mask. Before anything the run writes is stored or shown, each occurrence is replaced with `[masked]`: + +- In the run's events and log, its result and the free texts of a proposal: commit messages, pull request titles and bodies, comments, reviews and the paths of changed files. +- The value itself, and its common encodings: base64, URL-encoded and JSON-escaped. +- The start of a value printed on its own: if the agent prints the first characters of a token (`GITHUB_TOKEN=ghp_6Z8A...`), a fragment of 8 characters or more is masked too. + +Masking has limits. Values shorter than 8 characters are not masked at all, so keep real secrets long. Fragments shorter than 8 characters, the end of a value, or a fragment glued to more letters or digits can get through. A branch name or a label that would contain a secret is not rewritten: the proposal is refused (`proposal_invalid`). If the diff of a push contains the value of one of the run's secrets, the portal warns you before you approve. + +Masking protects what NaN stores and shows. It does not stop an agent that holds a secret from sending it somewhere. + +### What the agent can do with a secret + +An agent that receives a secret as **Agent (read access)** can read it, print it, use it and send it over the network, like any program you run with that variable set. That is by design: it is how the agent clones, fetches and calls your services. What it does with it is your responsibility, so: + +- **Give the agent read tokens only.** Keep every token that can write as **Actions only**: those never reach the agent or any AI model. +- **Limit every token** to the one repository or resource it is for, with an expiration date. +- **Remember that text written by others reaches the agent.** A webhook delivery, a pull request, an issue or a test in a pull request can try to make the agent print or send a secret (prompt injection). That is why **Fresh clone per run** ignores pull requests from forks and outside contributors by default: see [Pull requests from forks and outside contributors](/docs/automations#pull-requests-from-forks-and-outside-contributors). +- **For a provider key,** set a spending limit at your provider. + +### Write access is the dangerous part + +A token that can **write** is worth much more to whoever steals it than one that can only read: it can push code, merge, delete branches or change settings. + +- **A GitHub write token** in the agent's hands can do anything its permissions allow, without going through the approval of your automation. Assign it as **Actions only (write access)**, so only NaN's apply step uses it, and only for the actions you allowed. +- **A NaN platform token** with `secrets:write` can replace any of your secrets (for example, swap the GitHub token an automation pushes with). One with `automations:write` can create an automation that receives every secret you have, with instructions of its choosing and nothing waiting for your approval: that is as good as reading them all. Give these scopes only to tokens that need them, with a short expiry, and never hand them to an agent. +- **Run `nan auth login` on your own machine, never inside a workspace**: the agents that run there can read the session it saves, and a CLI session can change your secrets. + +### Rotate them + +Prefer short-lived tokens, and rotate them before they expire or as soon as one may have leaked: + +1. Create the new token at its source (GitHub, your provider). +2. **Replace value** in the portal, or `nan secrets set NAME --value-file new.txt`. The name stays, so every automation keeps using it with no change. +3. Revoke the old token at its source. NaN cannot revoke a token for you: replacing it in NaN does not stop the old one from working elsewhere. + +If a token leaked, revoke it at its source first, then replace it. Writing the expiry date in the **Description** ("expires 2026-12-31") helps you see what is due. + +### When you replace or delete a secret + +- **Replacing a value** takes effect at the next run that starts, including runs already queued: a run reads its secrets when it starts. Runs already working keep the value they started with. A replace is refused with `409 secrets_too_large` if the new value would push one of the automations that use it over 48 KiB per run. +- **Deleting** is refused with `409 secret_in_use` while an automation uses the secret, as one of its secrets or as its provider key. The answer lists those automations: change them first, then delete. A run on your own key whose provider key was deleted after it was queued fails with `secret_missing`. + +### Who changed it last + +Every secret records where its last change came from: **Last changed from** in the portal (**Browser**, **CLI** or **API token**), `CHANGED FROM` in `nan secrets ls` and `updated_by` in the API (`browser_session`, `cli_session` or `platform_token`). A change you did not make stands out: if you see one, revoke the tokens you no longer use in **Settings → Tokens** and replace the secret. + +## Limits + +| Limit | Value | +|---|---| +| Secrets | 100 per member | +| Name | Up to 64 characters: capitals, digits and `_`, starting with a letter | +| Value of a **Secret** | Up to 16 KiB | +| Value of a **Provider key** | 8 to 4096 characters, one line, no spaces | +| Description | Up to 256 characters | +| Base URL | Up to 512 characters | +| Secrets per automation | 20 | +| Everything one run receives | 48 KiB, its provider key included | +| Changes to secrets | A burst of 30, then 120 per hour, counting the portal, the CLI and the API together | +| API request body | 32 KiB | + +Adding a secret or replacing a value needs an active membership. Without one you can still list and delete your secrets. + +## If something goes wrong + +| What you see | Why, and what to do | +|---|---| +| `400 invalid_request` with a `param` | That field breaks a rule: `name` (capitals, digits and `_`), `value` (size, control characters, a line break at the start or end, or a provider key with spaces), `kind`, `provider`, `base_url` ([rules](#base-url-rules)), `description` or `Idempotency-Key`. | +| `403 credential_not_allowed` | An API key (`sk-...`) or a session from an older CLI. Use your `nan auth login` session or a platform token with the scope. | +| `403 workspace_key_not_allowed` | The key inside a workspace. It never reads or changes secrets. | +| `403 token_scope_denied` | The token lacks `secrets:read` or `secrets:write`. Create one with the scope in **Settings → Tokens**. | +| `403 browser_session_required`, or **Browser session needed** in the portal | Your account does not have the write API yet, so changes need a browser session: sign in again with the email link from your browser. | +| `403 platform_access_required` | Adding or replacing needs an active membership. You can still list and delete. | +| `404` on every `/v1/secrets` route | Your account does not have the write API yet. Use the portal. | +| `404 not_found` | No secret with that id in your account. Check with `nan secrets ls`. | +| `409 name_taken` | You already have a secret with that name. Replace its value instead, or retry a create with the same `Idempotency-Key`. | +| `409 too_many_secrets` | You have 100. Delete one to add another. | +| `409 idempotency_key_reused` | That `Idempotency-Key` already created a different secret. Use a new key. | +| `409 secret_in_use` | An automation uses it. Remove it from the automations listed, then delete. | +| `409 secrets_too_large` | The new value would push the automations listed over 48 KiB per run. Use a shorter value or remove other secrets from them. | +| `429 rate_limited` | More than a burst of 30 changes, or 120 per hour. Wait and try again. | +| `503 secrets_unavailable` | Secrets are briefly unavailable. Try again in a few minutes. | +| The CLI exits with code 64 and tells you to use the portal | Your account does not have the write API yet. Use the portal. (The CLI also uses code 64 for most other refusals in this table: read the message it prints.) | +| The automation cannot be saved without a `GITHUB_TOKEN` | An automation with a **GitHub repository** needs a secret assigned as `GITHUB_TOKEN`, **Actions only (write access)**. | +| A run fails with `secrets_unavailable` | Its secrets could not be read when it started. It is retried for a short while first. Start it again later. | +| A run fails with `secret_missing` | A secret it needs was deleted after it was queued: its provider key, or a secret removed from the automation and then deleted. | +| A run fails with `config_error` | Among other causes, its secrets add up to more than 48 KiB. Remove some from the automation. | +| A run fails with an authentication error from GitHub or your provider | The token is wrong, expired or revoked, or lacks a permission. Replace its value. | diff --git a/src/content/docs/webhooks.mdx b/src/content/docs/webhooks.mdx index 1310fc8c..a6f65166 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: 22 +order: 23 group: Guides --- @@ -44,6 +44,8 @@ Press **Add webhook**. The portal shows, **only once**: - **Webhook URL**: `https://api.nan.builders/hooks/nan_whk_...`. The URL itself is a credential: anyone who has it can send to it, so treat it like a password. - **Signing secret** (GitHub and NaN): what the sender signs each event with. For Stripe only the URL is shown: the secret is the one you pasted. +The signing secret belongs to the trigger, not to your [Secrets](/docs/secrets): it does not appear in your secrets list or count towards their limits, and no run receives it. If your automation needs to call a service of yours, save that token as a secret and assign it to the automation. + Copy them, then press **I've saved them**. Afterwards the portal only shows the start of the URL. If you lose them, [rotate](#rotate-the-url-or-the-secret) to get new ones. From the CLI, `nan automations triggers add -f webhook.yaml` prints the same values **once** (for Stripe, only the URL), with a reminder to store them now; `--json` prints them as JSON for a script. Through the API, `POST /v1/automations/{id}/triggers` returns them once in `webhook_url` and `signing_secret`, with `Cache-Control: no-store`. Nothing returns them again: not the portal, not `nan automations triggers ls`, not the API. diff --git a/src/data/openapi.json b/src/data/openapi.json index c2526233..e197f24d 100644 --- a/src/data/openapi.json +++ b/src/data/openapi.json @@ -68,7 +68,7 @@ }, { "name": "Secrets", - "description": "The tokens and keys your automations receive, and your own provider keys. The API never returns a value: it lists metadata, creates secrets, replaces values, changes descriptions and base URLs, and deletes. Reading needs a CLI session (`nan auth login`) or a platform token with `secrets:read`; writing, a CLI session or a token with `secrets:write`. API keys (`sk-...`) never read or write secrets. Where the write API is not yet available for your account, these routes answer `404`. Guide: [Secrets](https://nan.builders/docs/automations#secrets)." + "description": "The tokens and keys your automations receive, and your own provider keys. The API never returns a value: it lists metadata, creates secrets, replaces values, changes descriptions and base URLs, and deletes. Reading needs a CLI session (`nan auth login`) or a platform token with `secrets:read`; writing, a CLI session or a token with `secrets:write`. API keys (`sk-...`) never read or write secrets. Where the write API is not yet available for your account, these routes answer `404`. Guide: [Secrets](https://nan.builders/docs/secrets)." }, { "name": "Approvals", @@ -10395,7 +10395,7 @@ "Secret": { "type": "object", "additionalProperties": false, - "description": "A secret's metadata. No field ever carries the value: once saved, nobody can read it back. Guide: [Secrets](https://nan.builders/docs/automations#secrets).", + "description": "A secret's metadata. No field ever carries the value: once saved, nobody can read it back. Guide: [Secrets](https://nan.builders/docs/secrets).", "properties": { "id": { "type": "string", diff --git a/src/lib/openapiWriteApi.test.ts b/src/lib/openapiWriteApi.test.ts index 77fa0fe3..967515a5 100644 --- a/src/lib/openapiWriteApi.test.ts +++ b/src/lib/openapiWriteApi.test.ts @@ -58,7 +58,7 @@ describe('openapi.json: the write API operations', () => { const at = names.indexOf('Runs'); expect(names.slice(at, at + 5)).toEqual(['Runs', 'Automations', 'Secrets', 'Approvals', 'Workspaces']); const tags = Object.fromEntries(s.tags.map((t: any) => [t.name, t.description])); - expect(tags.Secrets).toContain('https://nan.builders/docs/automations#secrets'); + expect(tags.Secrets).toContain('https://nan.builders/docs/secrets'); expect(tags.Secrets).toMatch(/never returns a value/i); expect(tags.Secrets).toContain('`secrets:read`'); expect(tags.Secrets).toContain('`secrets:write`'); diff --git a/src/tests/layouts/automationsDocs.test.ts b/src/tests/layouts/automationsDocs.test.ts index 548e86a5..5f199725 100644 --- a/src/tests/layouts/automationsDocs.test.ts +++ b/src/tests/layouts/automationsDocs.test.ts @@ -96,9 +96,10 @@ describe.each(Object.entries(pages))('automations guides (%s)', (_locale, page) expect(a.group).toBe(page.group); expect(h.group).toBe(page.group); expect(a.order).toBe(runs.order + 1); - // The PR gatekeeper guide is nested under Automations (frontmatter - // `parent`), so it takes the slot right after it and Webhooks follows. - expect(h.order).toBe(a.order + 2); + // The PR gatekeeper guide and the Secrets page are nested under + // Automations (frontmatter `parent`), so they take the two slots right + // after it and Webhooks follows. + expect(h.order).toBe(a.order + 3); for (const later of ['examples.md', 'apps.md']) { const o = (parseFrontmatter(read(page.dir + later)).frontmatter as { order: number }).order; expect(o, later).toBeGreaterThan(h.order); @@ -170,6 +171,8 @@ describe.each(Object.entries(pages))('automations guides (%s)', (_locale, page) } expect(autoBody).toContain('**Add secret**'); expect(autoBody).toContain('**Replace value**'); + // The details live on the Secrets page; automations keeps the short version and links it. + expect(autoBody).toContain(`(${page.prefix}/docs/secrets)`); }); test('the limits of cloud-api (automations/api.go)', () => { diff --git a/src/tests/layouts/docsNavNesting.render.test.ts b/src/tests/layouts/docsNavNesting.render.test.ts index 17968be1..6bbd5434 100644 --- a/src/tests/layouts/docsNavNesting.render.test.ts +++ b/src/tests/layouts/docsNavNesting.render.test.ts @@ -104,6 +104,26 @@ for (const lang of ['en', 'es'] as const) { expect(html.match(/aria-current="page"/g)).toHaveLength(1); }); + // Owner's ask (2026-10-14): the Secrets page is the second sub-item of + // Automations, right after PR gatekeeper, in the same nested list. + it('renders Secrets in the same nested list, right after PR gatekeeper', async () => { + const html = await sidebar(lang, `${pfx}/docs`); + expect(html).toMatch( + new RegExp( + ``, + ), + ); + expect(html.match(new RegExp(`href="${pfx}/docs/secrets"`, 'g'))).toHaveLength(1); + }); + + it('marks Secrets as the current page and Automations as the trail when it is open', async () => { + const html = await sidebar(lang, `${pfx}/docs/secrets`); + expect(html).toMatch(new RegExp(` { const html = await sidebar(lang, `${pfx}/docs/webhooks`); expect(html).not.toContain('is-trail'); diff --git a/src/tests/layouts/docsNavPage.test.ts b/src/tests/layouts/docsNavPage.test.ts index f344eeba..76f38626 100644 --- a/src/tests/layouts/docsNavPage.test.ts +++ b/src/tests/layouts/docsNavPage.test.ts @@ -11,7 +11,7 @@ import { startDocsApiServer, type DocsApiServer } from '../helpers/docsApiServer */ let server: DocsApiServer; -const PATHS = ['/docs/pr-gatekeeper', '/docs/automations', '/docs/webhooks']; +const PATHS = ['/docs/pr-gatekeeper', '/docs/secrets', '/docs/automations', '/docs/webhooks']; const html = new Map(); /* @@ -46,11 +46,11 @@ const between = (html: string, start: RegExp, end: string) => { }; const LOCALES = [ - { lang: 'en', pfx: '', automations: 'Automations', webhooks: 'Webhooks and schedules' }, - { lang: 'es', pfx: '/es', automations: 'Automatizaciones', webhooks: 'Webhooks y programaciones' }, + { lang: 'en', pfx: '', automations: 'Automations', webhooks: 'Webhooks and schedules', secrets: 'Secrets' }, + { lang: 'es', pfx: '/es', automations: 'Automatizaciones', webhooks: 'Webhooks y programaciones', secrets: 'Secretos' }, ] as const; -for (const { lang, pfx, automations, webhooks } of LOCALES) { +for (const { lang, pfx, automations, webhooks, secrets } of LOCALES) { describe(`nested docs page (${lang})`, () => { it('breadcrumb: Docs / parent / current page', async () => { const body = await page(`${pfx}/docs/pr-gatekeeper`); @@ -64,21 +64,37 @@ for (const { lang, pfx, automations, webhooks } of LOCALES) { ); }); - it('prev/next of the child: its parent, then the parent\'s next sibling', async () => { + it('prev/next of the first child: its parent, then its next sibling', async () => { const body = await page(`${pfx}/docs/pr-gatekeeper`); const pn = between(body, /