Ayer por la noche, XoxoBot (mi chatbot axolotl 🪼) dejó de funcionar. El mensaje: "Ups, algo falló 😅 Intenta de nuevo."
Last night, XoxoBot (my axolotl chatbot 🪼) stopped working. The message: "Oops, something went wrong 😅 Try again."
No era el tipo de error que te dice qué pasó. Era el peor tipo: un catch genérico que esconde el problema real. Pero tengo un arma secreta: OscarCode9, mi agente de IA corriendo en OpenClaw. Le dije que lo arreglara, y esto es lo que encontró.
It wasn't the kind of error that tells you what happened. It was the worst kind: a generic catch that hides the real problem. But I have a secret weapon: OscarCode9, my AI agent running in OpenClaw. I told it to fix it, and this is what it found.
El ProblemaThe Problem
XoxoBot usa la API de GitHub Copilot para generar respuestas. El flujo de autenticación era así:
XoxoBot uses the GitHub Copilot API to generate responses. The authentication flow was like this:
clientId único por usuarioWidget generates a unique clientId per userread:userUser authorizes with scope read:user🎯 Problema #1: El scope read:user no tiene permisos para acceder a la API de Copilot. Necesitas una suscripción activa de GitHub Copilot.
🎯 Problem #1: The read:user scope doesn't have permissions to access the Copilot API. You need an active GitHub Copilot subscription.
🎯 Problema #2: El token OAuth (ghu_...) del servidor había expirado. Estos tokens duran ~8 horas.
🎯 Problem #2: The server's OAuth token (ghu_...) had expired. These tokens last ~8 hours.
El DiagnósticoThe Diagnosis
OscarCode9 hizo lo que haría cualquier dev decente: revisar los logs.
OscarCode9 did what any decent dev would do: check the logs.
Chat API error: Error: Copilot token exchange failed: HTTP 403
at getCopilotApiToken (.next/server/app/api/chat/route.js:170:1031)
Un 403 Forbidden. El token existía pero GitHub lo rechazaba. Confirmado: token expirado.
A 403 Forbidden. The token existed but GitHub rejected it. Confirmed: expired token.
La SoluciónThe Solution
En lugar de arreglar el device flow (que requeriría que cada usuario tenga Copilot), implementamos un fallback inteligente:
Instead of fixing the device flow (which would require every user to have Copilot), we implemented an intelligent fallback:
1. Fallback al Token del Servidor1. Fallback to the Server Token
Si el usuario no tiene auth, en lugar de pedir device flow, usamos el token del servidor:
If the user has no auth, instead of requesting device flow, we use the server token:
export async function getCopilotApiTokenForClient(clientId?: string) {
// Try user's own token if they have one
if (clientId) {
const auth = await prisma.copilotAuth.findUnique({ where: { clientId } });
if (auth) {
try {
// ... intentar usar token del usuario
} catch (e) {
console.warn(`User token failed, using server token`);
}
}
}
// Fallback to server's Copilot token (always available)
return getCopilotApiToken();
}
2. Soporte para Tokens Directos2. Support for Direct Tokens
Descubrimos que OpenClaw ya tenía un token válido guardado. El problema: era un token Copilot API (tid=...), no un token OAuth (ghu_...).
We discovered that OpenClaw already had a valid token stored. The problem: it was a Copilot API token (tid=...), not an OAuth token (ghu_...).
Agregamos detección automática:
We added automatic detection:
// If token is already a Copilot API token, use it directly
if (githubToken.startsWith("tid=")) {
// Extract expiration from token: exp=<timestamp>
const expMatch = githubToken.match(/exp=(\d+)/);
const expiresAt = expMatch ? parseInt(expMatch[1], 10) * 1000 : Date.now() + 30 * 60 * 1000;
cachedCopilotToken = { token: githubToken, expiresAt };
return {
token: githubToken,
baseUrl: deriveCopilotBaseUrl(githubToken),
};
}
✅ Resultado: El bot ahora acepta tanto tokens OAuth (ghu_...) como tokens Copilot directos (tid=...). Si uno falla, usa el otro.
✅ Result: The bot now accepts both OAuth tokens (ghu_...) and direct Copilot tokens (tid=...). If one fails, it uses the other.
El DeployThe Deploy
El fix requirió:
The fix required:
- Modificar
lib/copilot-api.ts - Modify
lib/copilot-api.ts - Actualizar el token en producción
- Update the token in production
- Rebuild del container Docker
- Rebuild the Docker container
# Subir archivo modificado
scp -i ~/TenK/truck.pem lib/copilot-api.ts ec2-user@3.145.149.229:~/ovents-landing/lib/
# Rebuild sin cache (importante!)
ssh ... "docker build --no-cache -t ovents-landing:prod ."
# Recrear container con el mapping correcto
docker run -d --name ovents-landing -p 3001:3001 \
--env-file ~/ovents-landing/.env \
--network backend_tenk-network \
ovents-landing:prod
⚠️ Nota importante: El port mapping debe ser -p 3001:3001, NO -p 3001:3000. Este error causa 502 Bad Gateway y me ha mordido múltiples veces.
⚠️ Important note: The port mapping must be -p 3001:3001, NOT -p 3001:3000. This error causes 502 Bad Gateway and has bitten me multiple times.
Lecciones AprendidasLessons Learned
- Los tokens OAuth expiran. Los
ghu_de GitHub duran ~8 horas. Implementa refresh o fallbacks. - OAuth tokens expire. GitHub's
ghu_tokens last ~8 hours. Implement refresh or fallbacks. - El scope importa.
read:userno da acceso a Copilot. Cada API tiene sus permisos. - Scope matters.
read:userdoesn't give access to Copilot. Each API has its permissions. - Fallbacks > Errores. Es mejor que el bot use un token del servidor que mostrar "algo falló".
- Fallbacks > Errors. It's better for the bot to use a server token than to show "something went wrong".
- Docker cache te puede trollear. Si cambias un archivo, usa
--no-cacheo Docker no lo detecta. - Docker cache can troll you. If you change a file, use
--no-cacheor Docker won't detect it. - Los agentes de IA son útiles para debugging. OscarCode9 encontró el problema, propuso la solución, y deployó. Yo solo aprobé.
- AI agents are useful for debugging. OscarCode9 found the problem, proposed the solution, and deployed. I just approved.
Tiempo TotalTotal Time
Hecho por OscarCode9 (agente de IA) mientras yo veía los logsDone by OscarCode9 (AI agent) while I watched the logs