La idea en una frase
Cada usuario corre con su propia cuenta de Anthropic, y nada se cruza
Un solo backend atiende a muchos usuarios. Por cada mensaje se lanza un proceso claude -p. Queremos que ese proceso (a) facture a la cuenta del usuario dueño del chat, y (b) no comparta nada con el de otro usuario: ni credenciales, ni archivos, ni sesiones.
Eso se logra con dos mitades: almacenamiento (la key vive cifrada en la DB) e inyección + aislamiento (en runtime se descifra a memoria y se inyecta en un proceso con su propio entorno y sus propios directorios).
Cómo leer este documento
Leyenda
en reposo, en la DB
solo en memoria, durante un run
cada uno tiene el suyo
Tres términos
| instance_id | el identificador canónico del usuario (su teléfono normalizado). Todo se keyea por acá. |
| holder | el dueño de la sesión: la conversación standalone, o el grupo (cuando varios chats se vinculan con /vincular y comparten contexto). |
| subprocess | el proceso claude -p que se lanza por mensaje. Su entorno (env) es cómo se le pasan los datos del usuario. |
Proceso A · puntual, lo hace el admin
Alta y asignación de la key
Anthropic no permite crear keys por API, así que el primer paso es siempre manual. A partir de ahí, el sistema valida, cifra y guarda.
- El admin crea la API key en la Console de Anthropic.Manual — fuera del sistema. La copia una sola vez.
- La asigna a un usuario vía panel / endpoint / CLI de admin.PUT /api/admin/users/{id}/anthropic-key · solo is_admin.
- El sistema la valida contra Anthropic antes de guardarla.Si es inválida o sin saldo → se rechaza y el admin reintenta.
- Se cifra y se persiste asociada al usuario. cifradaencrypt_str(key) (Fernet ⟵ bridge_secret) → omzg_user_anthropic_key.api_key_encrypted. Se guarda además key_hint (últimos 4, para la UI). Nunca en claro.
Proceso B · en cada mensaje del usuario
Ejecución: de un mensaje a una respuesta
- Llega el mensaje (WhatsApp / Telegram) y se resuelve el instance_id del usuario.
- Se busca su key en la DB y se aplica la precedencia:1.º key propia (asignada y activa) → 2.º key compartida → fail-closed (aborta + alerta). El login OAuth del server nunca se usa (normativa).
- Se descifra a memoria. en claro · efímerodecrypt_str(api_key_encrypted) → un string, vivo solo durante este run.
- Se arma el entorno del subprocess, con su key y sus directorios propios. aisladoVer la sección Aislamiento: HOME / CLAUDE_CONFIG_DIR / cwd por instance + ANTHROPIC_API_KEY del usuario.
- Se ejecuta claude -p con ese entorno.La key inyectada gana sobre el login OAuth del server (verificado: el CLI lo dice explícito).
- Anthropic factura a la cuenta dueña de esa key y la respuesta vuelve por el mismo canal.El puntero de sesión se guarda de nuevo en la DB para el próximo mensaje.
El punto central
Aislamiento: tres capas, ninguna fuga
El aislamiento no es una sola cosa: son tres capas que tapan fugas distintas. Juntas hacen que ni las credenciales, ni los datos, ni los archivos o sesiones de un usuario se crucen con los de otro.
CLAUDE_CONFIG_DIR = …/A/cfg
cwd = …/A/work
tokens Google / Notion = de A
CLAUDE_CONFIG_DIR = …/B/cfg
cwd = …/B/work
tokens Google / Notion = de B
Por qué se hace así
Un entorno por subprocess
Un backend único atiende a muchos usuarios en paralelo, y el env es el único canal para inyectarle a cada claude -p las credenciales de ese usuario. Hacerlo por subprocess (un dict fresco por mensaje) evita el “race” entre runs concurrentes y garantiza que la key/los datos de un usuario nunca lleguen al proceso de otro.
| Si fuera un env global | Con un env por subprocess |
|---|---|
| roto A setea KEY_A → B (concurrente) setea KEY_B → el run de A factura a B. Race y fuga. | ok env_A={…KEY_A} → proceso A · env_B={…KEY_B} → proceso B. Dos dicts, dos procesos, cero cruce. |
Además es efímero: el env vive solo mientras corre ese proceso y muere con él — la key descifrada no queda en ningún lado.
Continuidad de la conversación
Sesiones: puntero (DB) + transcript (disco)
Una sesión de claude -p son dos cosas en dos lugares. La fuente de verdad es la DB, no el disco. (Verificado con claude -p real + el código del orchestrator.)
| Pieza | Qué es | Dónde | Aislamiento |
|---|---|---|---|
| session_id (puntero) | un UUID, el “nombre” de la sesión | DB — claude_session_id | por holder |
| historial (fuente de verdad) | los mensajes de la conversación | DB — omzg_messages | por conversación |
| transcript (fast-path) | el .jsonl que el CLI usa para --resume | disco — CLAUDE_CONFIG_DIR/projects/<cwd>/<id>.jsonl | por instance |
Validación
La idea, validada de punta a punta
Probado con dos API keys reales y el modelo más barato. Las dos mitades —almacenamiento cifrado e inyección con aislamiento— quedaron demostradas.
| Qué se probó | Resultado |
|---|---|
| Las 2 keys funcionan contra la API y con claude -p | ✓ respuesta correcta |
| La key inyectada gana sobre el login OAuth del server | ✓ el CLI lo confirma; key inválida → 401 |
| Aislamiento total entre 2 instancias en paralelo | ✓ 6/6 — cada agente leyó solo su secreto, sin fuga |
| Cifrado/descifrado de la key (round-trip) | ✓ reversible, decrypt == original |