Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 9 additions & 5 deletions README.es.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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.

---

Expand Down Expand Up @@ -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)

Expand Down
11 changes: 8 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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.

---
Expand Down Expand Up @@ -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)

Expand Down
4 changes: 3 additions & 1 deletion SECURITY.es.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 |
Expand Down
4 changes: 3 additions & 1 deletion SECURITY.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 |
Expand Down
23 changes: 17 additions & 6 deletions docs/ARCHITECTURE.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
```

Expand All @@ -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
Expand All @@ -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
|
Expand All @@ -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
|
Expand Down
49 changes: 49 additions & 0 deletions docs/TROUBLESHOOTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 `<state>/commandcode-keys-state.json` for the cooldown state and
`<state>/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
Expand Down
21 changes: 15 additions & 6 deletions docs/es/architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
```

Expand All @@ -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
Expand All @@ -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
|
Expand Down
51 changes: 51 additions & 0 deletions docs/es/troubleshooting.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 `<state>/commandcode-keys-state.json` para el estado y
`<state>/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
Expand Down
Loading