From 5c10f87c5d2f20560d6fc7c82bc63b0d3399c1e1 Mon Sep 17 00:00:00 2001 From: JoshRob297 <99092299+JoshRob297@users.noreply.github.com> Date: Sat, 31 Oct 2026 12:33:46 -0600 Subject: [PATCH] docs: cover ZDR, key rotation and actionable errors Bring the prose in line with v1.2.0: the six capabilities, the rotation matrix (402/401/403/429), the actionable error table, the ZDR opt-in, the new architectural modules and the troubleshooting entries for 402, MODEL_NOT_IN_PLAN, a dead key in a pool and ZDR not being on. Validated versions refreshed to OpenCode 2.2.0 and Command Code CLI 1.88.0. --- README.es.md | 14 +++++++---- README.md | 11 +++++--- SECURITY.es.md | 4 ++- SECURITY.md | 4 ++- docs/ARCHITECTURE.md | 23 ++++++++++++----- docs/TROUBLESHOOTING.md | 49 ++++++++++++++++++++++++++++++++++++ docs/es/architecture.md | 21 +++++++++++----- docs/es/troubleshooting.md | 51 ++++++++++++++++++++++++++++++++++++++ 8 files changed, 155 insertions(+), 22 deletions(-) diff --git a/README.es.md b/README.es.md index 29a7523..64c12c3 100644 --- a/README.es.md +++ b/README.es.md @@ -7,7 +7,7 @@ Plugin de nivel produccion para [OpenCode](https://opencode.ai) con el proveedor [Command Code](https://commandcode.ai). -Hace cuatro cosas: +Hace seis cosas: 1. **Descubrimiento dinamico de modelos** - registra cada modelo de Command Code que el transporte compatible con OpenAI realmente puede servir, y etiqueta los tiers @@ -19,11 +19,15 @@ Hace cuatro cosas: la interfaz. 4. **Telemetria de cuota** - herramienta `commandcode_quota` y comando `/commandcode-quota` para balance y ventanas de rate limit. +5. **Credenciales resilientes** - rota un pool de keys con cooldowns por key, para + que una cuenta muerta no corte el turno. +6. **Fallos accionables** - opt-in de zero data retention (`CMD_ZDR=1`) y mensajes + de error que dicen que hacer en lugar de repetir el texto del proveedor. > [!NOTE] > **Estado: desarrollo privado.** El plugin esta verificado contra OpenCode -> **v2.0.32** y Command Code **1.73.4** sobre Bun 1.4.0. Todavia no esta publicado -> en ningun registro; se instala desde este repositorio. +> **v2.2.0** y el CLI de Command Code **1.88.0** sobre Bun 1.4.0. Todavia no esta +> publicado en ningun registro; se instala desde este repositorio. --- @@ -69,8 +73,8 @@ parseado de la URL es exactamente `api.commandcode.ai`** (o un host autorizado d forma explicita con `COMMANDCODE_ALLOWED_HOSTS`). Nunca se usa coincidencia por substring para credenciales: filtraria la key a hosts parecidos como `api.commandcode.ai.evil.com` o a cualquier servidor que solo lleve -`/provider/v1/chat/completions` en su URL. Una cabecera `authorization` existente -jamas se sobrescribe. +`/provider/v1/chat/completions` en su URL. En modo key unica una cabecera `authorization` existente +jamas se sobrescribe; en modo rotacion el plugin es dueno de ella (ver abajo). ### Rotacion multi-key (opt-in) diff --git a/README.md b/README.md index 7f85c15..176d979 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@ Production-grade [OpenCode](https://opencode.ai) plugin for the [Command Code](https://commandcode.ai) provider. -It does four things: +It does six things: 1. **Dynamic model discovery** - registers every Command Code model the OpenAI-compatible transport can actually serve, and tags free/promo tiers. @@ -16,10 +16,14 @@ It does four things: HTTP 400, and aborts silently dropped SSE streams before they freeze the UI. 4. **Quota telemetry** - a `commandcode_quota` tool and a `/commandcode-quota` command for balance and rate-limit windows. +5. **Resilient credentials** - rotates a pool of keys with per-key cooldowns, so + one dead account does not end the turn. +6. **Actionable failures** - a zero data retention opt-in (`CMD_ZDR=1`) and error + messages that say what to do next instead of repeating the provider text. > [!NOTE] > **Status: private development.** The plugin is verified against -> OpenCode **v2.0.32** and Command Code position **1.73.4** on Bun 1.4.0. +> OpenCode **v2.2.0** and the Command Code CLI **1.88.0** on Bun 1.4.0. > It is not yet published to a registry; install it from this repository. --- @@ -65,7 +69,8 @@ hostname is exactly `api.commandcode.ai`** (or a host explicitly allow-listed vi `COMMANDCODE_ALLOWED_HOSTS`). Substring matching is never used for credentials: it would leak the key to lookalike hosts such as `api.commandcode.ai.evil.com` or to any server that merely carries `/provider/v1/chat/completions` in its URL. -An existing `authorization` header is never overwritten. +In single-key mode an existing `authorization` header is never overwritten; in +rotation mode the plugin owns it (see below). ### Multi-key rotation (opt-in) diff --git a/SECURITY.es.md b/SECURITY.es.md index b19ee16..765dec6 100644 --- a/SECURITY.es.md +++ b/SECURITY.es.md @@ -56,7 +56,9 @@ Reglas de diseno que existen por este modelo de amenazas: | --- | --- | | Las credenciales se adjuntan solo tras una coincidencia exacta de hostname | la coincidencia por substring es trivialmente explotable (`api.commandcode.ai.evil.com`, coincidencias por path, query strings) | | `COMMANDCODE_ALLOWED_HOSTS` es la unica allow-list | un opt-in explicito y auditable en lugar de una coincidencia difusa | -| Una cabecera `authorization` existente nunca se sobrescribe | una credencial provista por el llamador no debe reemplazarse en silencio | +| En modo key unica nunca se sobrescribe una cabecera `authorization` existente | una credencial del cliente no debe reemplazarse en silencio | +| En modo rotacion el plugin es dueno de `authorization` en el host estricto de Command Code | la key que inyecto el host puede ser la que acaba de devolver 402; el pool es un opt-in explicito | +| `x-cmd-zdr` se elimina salvo que `CMD_ZDR=1` | una politica de retencion de datos no debe activarse por accidente | | Los logs nunca registran la key, la cabecera `authorization` ni los cuerpos de request | los archivos `commandcode-*.log` son texto plano en disco | | El sanitizador nunca reescribe campos distintos de `messages` | limita el radio de impacto de una modificacion de payload en vuelo | | Todos los hooks son fail-open | un atacante no debe poder desactivar los requests de modelo induciendo un error del plugin | diff --git a/SECURITY.md b/SECURITY.md index 650cd85..ff8050e 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -54,7 +54,9 @@ Design rules that exist because of this threat model: | --- | --- | | Credentials are attached only after an exact hostname match | substring matching is trivially exploitable (`api.commandcode.ai.evil.com`, path-based matches, query strings) | | `COMMANDCODE_ALLOWED_HOSTS` is the only allow-list | an explicit, auditable opt-in instead of a fuzzy match | -| An existing `authorization` header in never overwritten | a caller-provided credential must not be silently replaced | +| In single-key mode an existing `authorization` header is never overwritten | a caller-provided credential must not be silently replaced | +| In rotation mode the plugin owns `authorization` on the strict Command Code host | the key the host injected may be the one that just returned 402; the pool is an explicit opt-in | +| `x-cmd-zdr` is stripped unless `CMD_ZDR=1` | a data-retention policy must not be enabled by accident | | Logs never record the key, the `authorization` header or request bodies | `commandcode-*.log` files are plain text on disk | | The sanitizer never rewrites fields other than `messages` | limits the blast radius of an in-flight payload modification | | Every hook is fail-open | an attacker must not be able to disable model requests by inducing a plugin error | diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index a508cbc..6aa5bc5 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -8,8 +8,11 @@ index.ts entrypoint: `export default { id, setup, server }` src/index.ts registration of hooks, model catalog, tool and command src/auth/resolver.ts credential cascade (pure I/O, no network) +src/auth/pool.ts multi-key pool, cooldowns and state persistence src/client/api.ts Command Code API client + on-disk catalog cache src/hooks/resilience.ts pure helpers (no side effects except a sentinel check) +src/hooks/zdr.ts zero data retention policy (CMD_ZDR) +src/hooks/errors.ts actionable JSON error decoration src/tools/quota.ts quota payload formatting ``` @@ -26,9 +29,11 @@ OpenCode start plugin.setup(ctx) | +-- ctx.session.hook("http.request") -> connection hygiene - | -> credential injection (exact host only) + | -> ZDR policy (opt-in) + | -> credential injection / key rotation | -> payload sanitizer - +-- ctx.session.hook("http.response") -> SSE inactivity watchdog + +-- ctx.session.hook("http.response") -> key cooldown + actionable errors + | -> SSE inactivity watchdog +-- ctx.session.hook("retry") -> retry telemetry (Command Code only) +-- ctx.model.transform() -> catalog registration + promo badges +-- ctx.tool.transform() -> commandcode_quota @@ -55,11 +60,15 @@ OpenCode -> model request | set `connection: close` in place on the existing Request | (preserves AbortSignal, body and headers; never rebuilds) | - |-- B. credential injection - | only if hostname === api.commandcode.ai (or allow-listed) - | and no `authorization` header is present yet + |-- B. zero data retention + | CMD_ZDR=1 -> `x-cmd-zdr: 1`; otherwise strip a caller-supplied header | - |-- C. payload sanitizer + |-- C. credential injection / rotation + | exact host only. With one key: inject only when no header is present. + | With a pool of >=2 keys: the plugin owns the header and picks the + | first key that is not cooling down. + | + |-- D. payload sanitizer | read a clone of the body, repair it, and rebuild the Request | ONLY when something actually changed | @@ -69,6 +78,8 @@ Command Code v [http.response hook] | + |-- JSON error? -> append a resolution hint (status and envelope kept) + |-- key rotation? -> record a cooldown from 402/401/403/429 |-- SSE and Command Code? -> wrap in the inactivity watchdog |-- otherwise -> return untouched | diff --git a/docs/TROUBLESHOOTING.md b/docs/TROUBLESHOOTING.md index 5064743..fe87fb3 100644 --- a/docs/TROUBLESHOOTING.md +++ b/docs/TROUBLESHOOTING.md @@ -155,6 +155,55 @@ cat "${COMMANDCODE_STATE_DIR:-$HOME/.local/state/opencode}/commandcode-watchdog. timeout. Verify the request actually reached the network, then raise that timeout or retry. +## `HTTP 402 insufficient credits` + +The account behind the key has no balance, so Command Code rejects every request +before routing it, including paid models. The plugin appends a hint to the +message and, when a key pool is configured, records a 24h cooldown for that key. + +Actions: top up the account at commandcode.ai, or add a second key to the pool +(`COMMANDCODE_API_KEYS`, `COMMANDCODE_KEYS_FILE` or +`~/.config/commandcode/keys.txt`). See +[CONFIGURATION](CONFIGURATION.md#key-rotation). + +## `MODEL_NOT_IN_PLAN` + +The model exists but the plan does not include it (for example Claude models on +an `INDIVIDUAL-GOAT` plan, or GPT-5.5 on Goat). The plugin appends the hint +"pick another model or upgrade the plan" and leaves the status untouched. + +Check the current list at commandcode.ai/models before upgrading. + +## One key is dead and the others still work + +Configure a pool of two or more keys; rotation is opt-in and additive: + +| Status | Cooldown recorded | +| --- | --- | +| `402` | 24h | +| `401` / `403` | 24h | +| `429` | `Retry-After` up to 6h, else 5m | +| `5xx` | none | + +Inspect `/commandcode-keys-state.json` for the cooldown state and +`/commandcode-rotation.log` for the timeline. Delete the state file to +clear every cooldown immediately. + +With a single key the plugin keeps the old behaviour and never overrides an +`Authorization` header the host already set. + +## Zero data retention does not appear to be on + +ZDR is an explicit opt-in, the same one the Command Code CLI reads: + +```sh +export CMD_ZDR=1 +``` + +Any other value (including `true`) leaves it off, and a caller-supplied +`x-cmd-zdr` header is stripped so a session cannot enable it by accident. The +header is only sent to the strict Command Code host. + ## Key not found The quota tool reports `Error consultando Command Code: No se encontro ...`. Check diff --git a/docs/es/architecture.md b/docs/es/architecture.md index 25d5002..00ee730 100644 --- a/docs/es/architecture.md +++ b/docs/es/architecture.md @@ -8,8 +8,11 @@ index.ts entrypoint: `export default { id, setup, server }` src/index.ts registro de hooks, catalogo de modelos, herramienta y comando src/auth/resolver.ts cascada de credenciales (I/O puro, sin red) +src/auth/pool.ts pool multi-key, cooldowns y persistencia de estado src/client/api.ts cliente de la API de Command Code + cache del catalogo en disco src/hooks/resilience.ts helpers puros (sin efectos secundarios salvo la revision del centinela) +src/hooks/zdr.ts politica de zero data retention (CMD_ZDR) +src/hooks/errors.ts decoracion accionable de errores JSON src/tools/quota.ts formateo del payload de cuota ``` @@ -26,9 +29,11 @@ Arranque de OpenCode plugin.setup(ctx) | +-- ctx.session.hook("http.request") -> higiene de conexion - | -> inyeccion de credencial (solo host exacto) + | -> politica ZDR (opt-in) + | -> inyeccion de credencial / rotacion de keys | -> sanitizador de payload - +-- ctx.session.hook("http.response") -> watchdog de inactividad SSE + +-- ctx.session.hook("http.response") -> cooldown de keys + errores accionables + | -> watchdog de inactividad SSE +-- ctx.session.hook("retry") -> telemetria de reintentos (solo Command Code) +-- ctx.model.transform() -> registro del catalogo + etiquetas de promo +-- ctx.tool.transform() -> commandcode_quota @@ -55,11 +60,15 @@ OpenCode -> request de modelo | setea `connection: close` in-place sobre el Request existente | (preserva AbortSignal, cuerpo y cabeceras; nunca lo reconstruye) | - |-- B. inyeccion de credencial - | solo si hostname === api.commandcode.ai (o esta en la allow-list) - | y todavia no hay cabecera `authorization` + |-- B. zero data retention + | CMD_ZDR=1 -> `x-cmd-zdr: 1`; si no, elimina una cabecera del cliente | - |-- C. sanitizador de payload + |-- C. inyeccion de credencial / rotacion + | solo host exacto. Con una key: inyecta solo si no hay cabecera. + | Con un pool de >=2 keys: el plugin es dueno de la cabecera y elige + | la primera key que no este en cooldown. + | + |-- D. sanitizador de payload | lee un clon del cuerpo, lo repara y reconstruye el Request | SOLO cuando algo cambio realmente | diff --git a/docs/es/troubleshooting.md b/docs/es/troubleshooting.md index e4675b8..4d3dfa9 100644 --- a/docs/es/troubleshooting.md +++ b/docs/es/troubleshooting.md @@ -156,6 +156,57 @@ cat "${COMMANDCODE_STATE_DIR:-$HOME/.local/state/opencode}/commandcode-watchdog. el watchdog no vigila a proposito. Esa ventana pertenece al timeout del proveedor en OpenCode. Verifica que el request llego a la red, luego sube ese timeout o reintenta. +## `HTTP 402 insufficient credits` + +La cuenta de la key no tiene saldo, asi que Command Code rechaza todo request +antes de enrutarlo, incluidos los modelos de pago. El plugin agrega una pista al +mensaje y, si hay un pool de keys configurado, registra un cooldown de 24h para +esa key. + +Acciones: recarga la cuenta en commandcode.ai, o agrega una segunda key al pool +(`COMMANDCODE_API_KEYS`, `COMMANDCODE_KEYS_FILE` o +`~/.config/commandcode/keys.txt`). Ver +[CONFIGURACION](es/configuration.md#rotacion-de-keys). + +## `MODEL_NOT_IN_PLAN` + +El modelo existe pero el plan no lo incluye (por ejemplo los modelos Claude en un +plan `INDIVIDUAL-GOAT`, o GPT-5.5 en Goat). El plugin agrega la pista "pick +another model or upgrade the plan" y deja el status intacto. + +Revisa la lista vigente en commandcode.ai/models antes de subir de plan. + +## Una key esta muerta y las demas siguen funcionando + +Configura un pool de dos o mas keys; la rotacion es opt-in y aditiva: + +| Status | Cooldown registrado | +| --- | --- | +| `402` | 24h | +| `401` / `403` | 24h | +| `429` | `Retry-After` hasta 6h, si no 5m | +| `5xx` | ninguno | + +Revisa `/commandcode-keys-state.json` para el estado y +`/commandcode-rotation.log` para la linea de tiempo. Borra el archivo de +estado para limpiar todos los cooldowns de inmediato. + +Con una sola key el plugin mantiene el comportamiento anterior y nunca +sobrescribe una cabecera `Authorization` que el host ya haya puesto. + +## No parece estar activo zero data retention + +ZDR es un opt-in explicito, el mismo que lee el CLI de Command Code: + +```sh +export CMD_ZDR=1 +``` + +Cualquier otro valor (incluido `true`) lo deja apagado, y una cabecera +`x-cmd-zdr` enviada por el cliente se elimina para que una sesion no pueda +activarlo por accidente. La cabecera solo se envia al host estricto de Command +Code. + ## No se encuentra la key La herramienta de cuota responde