Skip to content

Commit 0b89932

Browse files
authored
docs: cover ZDR, key rotation and actionable errors (#11)
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.
1 parent 757376e commit 0b89932

8 files changed

Lines changed: 155 additions & 22 deletions

File tree

‎README.es.md‎

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
Plugin de nivel produccion para [OpenCode](https://opencode.ai) con el proveedor
88
[Command Code](https://commandcode.ai).
99

10-
Hace cuatro cosas:
10+
Hace seis cosas:
1111

1212
1. **Descubrimiento dinamico de modelos** - registra cada modelo de Command Code que
1313
el transporte compatible con OpenAI realmente puede servir, y etiqueta los tiers
@@ -19,11 +19,15 @@ Hace cuatro cosas:
1919
la interfaz.
2020
4. **Telemetria de cuota** - herramienta `commandcode_quota` y comando
2121
`/commandcode-quota` para balance y ventanas de rate limit.
22+
5. **Credenciales resilientes** - rota un pool de keys con cooldowns por key, para
23+
que una cuenta muerta no corte el turno.
24+
6. **Fallos accionables** - opt-in de zero data retention (`CMD_ZDR=1`) y mensajes
25+
de error que dicen que hacer en lugar de repetir el texto del proveedor.
2226

2327
> [!NOTE]
2428
> **Estado: desarrollo privado.** El plugin esta verificado contra OpenCode
25-
> **v2.0.32** y Command Code **1.73.4** sobre Bun 1.4.0. Todavia no esta publicado
26-
> en ningun registro; se instala desde este repositorio.
29+
> **v2.2.0** y el CLI de Command Code **1.88.0** sobre Bun 1.4.0. Todavia no esta
30+
> publicado en ningun registro; se instala desde este repositorio.
2731
2832
---
2933

@@ -69,8 +73,8 @@ parseado de la URL es exactamente `api.commandcode.ai`** (o un host autorizado d
6973
forma explicita con `COMMANDCODE_ALLOWED_HOSTS`). Nunca se usa coincidencia por
7074
substring para credenciales: filtraria la key a hosts parecidos como
7175
`api.commandcode.ai.evil.com` o a cualquier servidor que solo lleve
72-
`/provider/v1/chat/completions` en su URL. Una cabecera `authorization` existente
73-
jamas se sobrescribe.
76+
`/provider/v1/chat/completions` en su URL. En modo key unica una cabecera `authorization` existente
77+
jamas se sobrescribe; en modo rotacion el plugin es dueno de ella (ver abajo).
7478

7579
### Rotacion multi-key (opt-in)
7680

‎README.md‎

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66

77
Production-grade [OpenCode](https://opencode.ai) plugin for the [Command Code](https://commandcode.ai) provider.
88

9-
It does four things:
9+
It does six things:
1010

1111
1. **Dynamic model discovery** - registers every Command Code model the
1212
OpenAI-compatible transport can actually serve, and tags free/promo tiers.
@@ -16,10 +16,14 @@ It does four things:
1616
HTTP 400, and aborts silently dropped SSE streams before they freeze the UI.
1717
4. **Quota telemetry** - a `commandcode_quota` tool and a `/commandcode-quota`
1818
command for balance and rate-limit windows.
19+
5. **Resilient credentials** - rotates a pool of keys with per-key cooldowns, so
20+
one dead account does not end the turn.
21+
6. **Actionable failures** - a zero data retention opt-in (`CMD_ZDR=1`) and error
22+
messages that say what to do next instead of repeating the provider text.
1923

2024
> [!NOTE]
2125
> **Status: private development.** The plugin is verified against
22-
> OpenCode **v2.0.32** and Command Code position **1.73.4** on Bun 1.4.0.
26+
> OpenCode **v2.2.0** and the Command Code CLI **1.88.0** on Bun 1.4.0.
2327
> It is not yet published to a registry; install it from this repository.
2428
2529
---
@@ -65,7 +69,8 @@ hostname is exactly `api.commandcode.ai`** (or a host explicitly allow-listed vi
6569
`COMMANDCODE_ALLOWED_HOSTS`). Substring matching is never used for credentials:
6670
it would leak the key to lookalike hosts such as `api.commandcode.ai.evil.com`
6771
or to any server that merely carries `/provider/v1/chat/completions` in its URL.
68-
An existing `authorization` header is never overwritten.
72+
In single-key mode an existing `authorization` header is never overwritten; in
73+
rotation mode the plugin owns it (see below).
6974

7075
### Multi-key rotation (opt-in)
7176

‎SECURITY.es.md‎

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,9 @@ Reglas de diseno que existen por este modelo de amenazas:
5656
| --- | --- |
5757
| 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) |
5858
| `COMMANDCODE_ALLOWED_HOSTS` es la unica allow-list | un opt-in explicito y auditable en lugar de una coincidencia difusa |
59-
| Una cabecera `authorization` existente nunca se sobrescribe | una credencial provista por el llamador no debe reemplazarse en silencio |
59+
| En modo key unica nunca se sobrescribe una cabecera `authorization` existente | una credencial del cliente no debe reemplazarse en silencio |
60+
| 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 |
61+
| `x-cmd-zdr` se elimina salvo que `CMD_ZDR=1` | una politica de retencion de datos no debe activarse por accidente |
6062
| Los logs nunca registran la key, la cabecera `authorization` ni los cuerpos de request | los archivos `commandcode-*.log` son texto plano en disco |
6163
| El sanitizador nunca reescribe campos distintos de `messages` | limita el radio de impacto de una modificacion de payload en vuelo |
6264
| Todos los hooks son fail-open | un atacante no debe poder desactivar los requests de modelo induciendo un error del plugin |

‎SECURITY.md‎

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,9 @@ Design rules that exist because of this threat model:
5454
| --- | --- |
5555
| Credentials are attached only after an exact hostname match | substring matching is trivially exploitable (`api.commandcode.ai.evil.com`, path-based matches, query strings) |
5656
| `COMMANDCODE_ALLOWED_HOSTS` is the only allow-list | an explicit, auditable opt-in instead of a fuzzy match |
57-
| An existing `authorization` header in never overwritten | a caller-provided credential must not be silently replaced |
57+
| In single-key mode an existing `authorization` header is never overwritten | a caller-provided credential must not be silently replaced |
58+
| 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 |
59+
| `x-cmd-zdr` is stripped unless `CMD_ZDR=1` | a data-retention policy must not be enabled by accident |
5860
| Logs never record the key, the `authorization` header or request bodies | `commandcode-*.log` files are plain text on disk |
5961
| The sanitizer never rewrites fields other than `messages` | limits the blast radius of an in-flight payload modification |
6062
| Every hook is fail-open | an attacker must not be able to disable model requests by inducing a plugin error |

‎docs/ARCHITECTURE.md‎

Lines changed: 17 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,11 @@
88
index.ts entrypoint: `export default { id, setup, server }`
99
src/index.ts registration of hooks, model catalog, tool and command
1010
src/auth/resolver.ts credential cascade (pure I/O, no network)
11+
src/auth/pool.ts multi-key pool, cooldowns and state persistence
1112
src/client/api.ts Command Code API client + on-disk catalog cache
1213
src/hooks/resilience.ts pure helpers (no side effects except a sentinel check)
14+
src/hooks/zdr.ts zero data retention policy (CMD_ZDR)
15+
src/hooks/errors.ts actionable JSON error decoration
1316
src/tools/quota.ts quota payload formatting
1417
```
1518

@@ -26,9 +29,11 @@ OpenCode start
2629
plugin.setup(ctx)
2730
|
2831
+-- ctx.session.hook("http.request") -> connection hygiene
29-
| -> credential injection (exact host only)
32+
| -> ZDR policy (opt-in)
33+
| -> credential injection / key rotation
3034
| -> payload sanitizer
31-
+-- ctx.session.hook("http.response") -> SSE inactivity watchdog
35+
+-- ctx.session.hook("http.response") -> key cooldown + actionable errors
36+
| -> SSE inactivity watchdog
3237
+-- ctx.session.hook("retry") -> retry telemetry (Command Code only)
3338
+-- ctx.model.transform() -> catalog registration + promo badges
3439
+-- ctx.tool.transform() -> commandcode_quota
@@ -55,11 +60,15 @@ OpenCode -> model request
5560
| set `connection: close` in place on the existing Request
5661
| (preserves AbortSignal, body and headers; never rebuilds)
5762
|
58-
|-- B. credential injection
59-
| only if hostname === api.commandcode.ai (or allow-listed)
60-
| and no `authorization` header is present yet
63+
|-- B. zero data retention
64+
| CMD_ZDR=1 -> `x-cmd-zdr: 1`; otherwise strip a caller-supplied header
6165
|
62-
|-- C. payload sanitizer
66+
|-- C. credential injection / rotation
67+
| exact host only. With one key: inject only when no header is present.
68+
| With a pool of >=2 keys: the plugin owns the header and picks the
69+
| first key that is not cooling down.
70+
|
71+
|-- D. payload sanitizer
6372
| read a clone of the body, repair it, and rebuild the Request
6473
| ONLY when something actually changed
6574
|
@@ -69,6 +78,8 @@ Command Code
6978
v
7079
[http.response hook]
7180
|
81+
|-- JSON error? -> append a resolution hint (status and envelope kept)
82+
|-- key rotation? -> record a cooldown from 402/401/403/429
7283
|-- SSE and Command Code? -> wrap in the inactivity watchdog
7384
|-- otherwise -> return untouched
7485
|

‎docs/TROUBLESHOOTING.md‎

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -155,6 +155,55 @@ cat "${COMMANDCODE_STATE_DIR:-$HOME/.local/state/opencode}/commandcode-watchdog.
155155
timeout. Verify the request actually reached the network, then raise that timeout
156156
or retry.
157157

158+
## `HTTP 402 insufficient credits`
159+
160+
The account behind the key has no balance, so Command Code rejects every request
161+
before routing it, including paid models. The plugin appends a hint to the
162+
message and, when a key pool is configured, records a 24h cooldown for that key.
163+
164+
Actions: top up the account at commandcode.ai, or add a second key to the pool
165+
(`COMMANDCODE_API_KEYS`, `COMMANDCODE_KEYS_FILE` or
166+
`~/.config/commandcode/keys.txt`). See
167+
[CONFIGURATION](CONFIGURATION.md#key-rotation).
168+
169+
## `MODEL_NOT_IN_PLAN`
170+
171+
The model exists but the plan does not include it (for example Claude models on
172+
an `INDIVIDUAL-GOAT` plan, or GPT-5.5 on Goat). The plugin appends the hint
173+
"pick another model or upgrade the plan" and leaves the status untouched.
174+
175+
Check the current list at commandcode.ai/models before upgrading.
176+
177+
## One key is dead and the others still work
178+
179+
Configure a pool of two or more keys; rotation is opt-in and additive:
180+
181+
| Status | Cooldown recorded |
182+
| --- | --- |
183+
| `402` | 24h |
184+
| `401` / `403` | 24h |
185+
| `429` | `Retry-After` up to 6h, else 5m |
186+
| `5xx` | none |
187+
188+
Inspect `<state>/commandcode-keys-state.json` for the cooldown state and
189+
`<state>/commandcode-rotation.log` for the timeline. Delete the state file to
190+
clear every cooldown immediately.
191+
192+
With a single key the plugin keeps the old behaviour and never overrides an
193+
`Authorization` header the host already set.
194+
195+
## Zero data retention does not appear to be on
196+
197+
ZDR is an explicit opt-in, the same one the Command Code CLI reads:
198+
199+
```sh
200+
export CMD_ZDR=1
201+
```
202+
203+
Any other value (including `true`) leaves it off, and a caller-supplied
204+
`x-cmd-zdr` header is stripped so a session cannot enable it by accident. The
205+
header is only sent to the strict Command Code host.
206+
158207
## Key not found
159208

160209
The quota tool reports `Error consultando Command Code: No se encontro ...`. Check

‎docs/es/architecture.md‎

Lines changed: 15 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,11 @@
88
index.ts entrypoint: `export default { id, setup, server }`
99
src/index.ts registro de hooks, catalogo de modelos, herramienta y comando
1010
src/auth/resolver.ts cascada de credenciales (I/O puro, sin red)
11+
src/auth/pool.ts pool multi-key, cooldowns y persistencia de estado
1112
src/client/api.ts cliente de la API de Command Code + cache del catalogo en disco
1213
src/hooks/resilience.ts helpers puros (sin efectos secundarios salvo la revision del centinela)
14+
src/hooks/zdr.ts politica de zero data retention (CMD_ZDR)
15+
src/hooks/errors.ts decoracion accionable de errores JSON
1316
src/tools/quota.ts formateo del payload de cuota
1417
```
1518

@@ -26,9 +29,11 @@ Arranque de OpenCode
2629
plugin.setup(ctx)
2730
|
2831
+-- ctx.session.hook("http.request") -> higiene de conexion
29-
| -> inyeccion de credencial (solo host exacto)
32+
| -> politica ZDR (opt-in)
33+
| -> inyeccion de credencial / rotacion de keys
3034
| -> sanitizador de payload
31-
+-- ctx.session.hook("http.response") -> watchdog de inactividad SSE
35+
+-- ctx.session.hook("http.response") -> cooldown de keys + errores accionables
36+
| -> watchdog de inactividad SSE
3237
+-- ctx.session.hook("retry") -> telemetria de reintentos (solo Command Code)
3338
+-- ctx.model.transform() -> registro del catalogo + etiquetas de promo
3439
+-- ctx.tool.transform() -> commandcode_quota
@@ -55,11 +60,15 @@ OpenCode -> request de modelo
5560
| setea `connection: close` in-place sobre el Request existente
5661
| (preserva AbortSignal, cuerpo y cabeceras; nunca lo reconstruye)
5762
|
58-
|-- B. inyeccion de credencial
59-
| solo si hostname === api.commandcode.ai (o esta en la allow-list)
60-
| y todavia no hay cabecera `authorization`
63+
|-- B. zero data retention
64+
| CMD_ZDR=1 -> `x-cmd-zdr: 1`; si no, elimina una cabecera del cliente
6165
|
62-
|-- C. sanitizador de payload
66+
|-- C. inyeccion de credencial / rotacion
67+
| solo host exacto. Con una key: inyecta solo si no hay cabecera.
68+
| Con un pool de >=2 keys: el plugin es dueno de la cabecera y elige
69+
| la primera key que no este en cooldown.
70+
|
71+
|-- D. sanitizador de payload
6372
| lee un clon del cuerpo, lo repara y reconstruye el Request
6473
| SOLO cuando algo cambio realmente
6574
|

‎docs/es/troubleshooting.md‎

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -156,6 +156,57 @@ cat "${COMMANDCODE_STATE_DIR:-$HOME/.local/state/opencode}/commandcode-watchdog.
156156
el watchdog no vigila a proposito. Esa ventana pertenece al timeout del proveedor en
157157
OpenCode. Verifica que el request llego a la red, luego sube ese timeout o reintenta.
158158

159+
## `HTTP 402 insufficient credits`
160+
161+
La cuenta de la key no tiene saldo, asi que Command Code rechaza todo request
162+
antes de enrutarlo, incluidos los modelos de pago. El plugin agrega una pista al
163+
mensaje y, si hay un pool de keys configurado, registra un cooldown de 24h para
164+
esa key.
165+
166+
Acciones: recarga la cuenta en commandcode.ai, o agrega una segunda key al pool
167+
(`COMMANDCODE_API_KEYS`, `COMMANDCODE_KEYS_FILE` o
168+
`~/.config/commandcode/keys.txt`). Ver
169+
[CONFIGURACION](es/configuration.md#rotacion-de-keys).
170+
171+
## `MODEL_NOT_IN_PLAN`
172+
173+
El modelo existe pero el plan no lo incluye (por ejemplo los modelos Claude en un
174+
plan `INDIVIDUAL-GOAT`, o GPT-5.5 en Goat). El plugin agrega la pista "pick
175+
another model or upgrade the plan" y deja el status intacto.
176+
177+
Revisa la lista vigente en commandcode.ai/models antes de subir de plan.
178+
179+
## Una key esta muerta y las demas siguen funcionando
180+
181+
Configura un pool de dos o mas keys; la rotacion es opt-in y aditiva:
182+
183+
| Status | Cooldown registrado |
184+
| --- | --- |
185+
| `402` | 24h |
186+
| `401` / `403` | 24h |
187+
| `429` | `Retry-After` hasta 6h, si no 5m |
188+
| `5xx` | ninguno |
189+
190+
Revisa `<state>/commandcode-keys-state.json` para el estado y
191+
`<state>/commandcode-rotation.log` para la linea de tiempo. Borra el archivo de
192+
estado para limpiar todos los cooldowns de inmediato.
193+
194+
Con una sola key el plugin mantiene el comportamiento anterior y nunca
195+
sobrescribe una cabecera `Authorization` que el host ya haya puesto.
196+
197+
## No parece estar activo zero data retention
198+
199+
ZDR es un opt-in explicito, el mismo que lee el CLI de Command Code:
200+
201+
```sh
202+
export CMD_ZDR=1
203+
```
204+
205+
Cualquier otro valor (incluido `true`) lo deja apagado, y una cabecera
206+
`x-cmd-zdr` enviada por el cliente se elimina para que una sesion no pueda
207+
activarlo por accidente. La cabecera solo se envia al host estricto de Command
208+
Code.
209+
159210
## No se encuentra la key
160211

161212
La herramienta de cuota responde

0 commit comments

Comments
 (0)