colana 1.0.0-beta.49 → 1.0.0-beta.51

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "colana",
3
- "version": "1.0.0-beta.49",
3
+ "version": "1.0.0-beta.51",
4
4
  "description": "Agent-First. Multiplied. Multi-agent command center for AI coding agents.",
5
5
  "type": "module",
6
6
  "main": "server/index.js",
@@ -1,7 +1,7 @@
1
1
  import { getOpenClawAuthToken, getOpenClawGatewayPort, getOpenClawDefaultModel, getOpenClawConfig, isChatCompletionsEnabled, ensureChatCompletionsEnabled, ensureAuthProfile, readAuthStore } from './openclaw-config.js';
2
2
  import { validate, personalChatSchema, openclawModelSetSchema, openclawModelAuthSchema } from './validation.js';
3
3
  import { setSetting } from './settings-store.js';
4
- import { restartOpenClawGateway } from './pty-manager.js';
4
+ import { restartOpenClawGateway, isGatewaySpawnedByColana } from './pty-manager.js';
5
5
  import { addChatMessage, getChatHistory, clearChatHistory } from './personal-chat-store.js';
6
6
  import config from './config.js';
7
7
  import { trackEvent } from './analytics.js';
@@ -102,12 +102,6 @@ export function registerPersonalAgentRoutes(app, { sensitiveLimiter }) {
102
102
  const authToken = getOpenClawAuthToken();
103
103
  headers['Authorization'] = `Bearer ${authToken}`;
104
104
 
105
- // Diagnostic: log token source for debugging auth failures
106
- const cfg = getOpenClawConfig();
107
- const nativeToken = cfg?.gateway?.auth?.token;
108
- const tokenSource = nativeToken ? 'native-config' : 'colana-managed';
109
- console.log(`[personal-agent] Auth: ${tokenSource}, token=${authToken?.slice(0, 8)}..., port=${port}`);
110
-
111
105
  // 60-second timeout
112
106
  const controller = new AbortController();
113
107
  const timeout = setTimeout(() => controller.abort(), 60000);
@@ -157,15 +151,25 @@ export function registerPersonalAgentRoutes(app, { sensitiveLimiter }) {
157
151
  const providerLabel = provider || null;
158
152
  const envVar = provider ? PROVIDER_ENV_MAP[provider] : null;
159
153
 
160
- // Check if the error body hints at provider-level auth failure
154
+ // Distinguish gateway-level 401 (our token rejected) from upstream
155
+ // provider 401 (API key invalid/missing). The gateway proxies upstream
156
+ // 401s as-is, so we can't rely on the error body format alone.
157
+ //
158
+ // Strategy: check if Colana spawned this gateway with our current token
159
+ // (marker file). If yes, gateway auth is fine → upstream provider issue.
160
+ // Also detect known provider error patterns in the response body.
161
161
  const isProviderAuth = errText.includes('api_key') || errText.includes('API key')
162
- || errText.includes('invalid_api_key') || errText.includes('authentication_error');
162
+ || errText.includes('invalid_api_key') || errText.includes('authentication_error')
163
+ || errText.includes('"type":"unauthorized"') || errText.includes('"type": "unauthorized"');
164
+
165
+ // If we spawned this gateway with our token, gateway auth is guaranteed OK
166
+ const gatewayAuthOk = isProviderAuth || isGatewaySpawnedByColana();
163
167
 
164
- if (isProviderAuth) {
168
+ if (gatewayAuthOk) {
165
169
  return res.status(401).json({
166
170
  error: providerLabel
167
171
  ? `Authentication failed — your ${providerLabel} API key may be missing or invalid.`
168
- : 'Authentication failed — your API key may be missing or invalid.',
172
+ : 'Authentication failed — your model provider API key may be missing or invalid.',
169
173
  code: 'GATEWAY_AUTH_FAILED',
170
174
  fix: envVar
171
175
  ? `Add or update your ${envVar} in Settings > Personal AI Agents.`
@@ -174,7 +178,7 @@ export function registerPersonalAgentRoutes(app, { sensitiveLimiter }) {
174
178
  });
175
179
  }
176
180
 
177
- // Default: gateway token mismatch (token exists locally but doesn't match gateway)
181
+ // Gateway itself rejected our token needs restart to resync
178
182
  return res.status(401).json({
179
183
  error: 'Gateway authentication failed — the auth token may be stale.',
180
184
  code: 'GATEWAY_TOKEN_STALE',
@@ -315,41 +315,49 @@ async function ensureOpenClawConfigured() {
315
315
  });
316
316
  }
317
317
 
318
+ // Marker file: tracks which gateway instance Colana spawned and with which token.
319
+ // On startup, if the running gateway wasn't spawned by us (no marker / token mismatch),
320
+ // we kill it and restart with our AUTH_TOKEN env var.
321
+ const GATEWAY_MARKER_PATH = path.join(os.homedir(), '.openclaw', '.colana-gateway-marker');
322
+
318
323
  /**
319
- * Verify that the running gateway accepts our bearer token.
320
- * Uses POST /v1/chat/completions with a minimal body the same endpoint
321
- * the chat route uses to ensure auth is truly valid end-to-end.
322
- * Returns true if token is accepted, false if 401.
324
+ * Verify that the running gateway was spawned by Colana with our current token.
325
+ * The OpenClaw gateway's /v1/models endpoint does NOT require auth, so HTTP
326
+ * probing is unreliable. Instead, we use a marker file written at spawn time.
327
+ *
328
+ * Returns true if gateway was spawned by us with the matching token.
323
329
  */
324
- async function verifyGatewayToken(port, token) {
330
+ function verifyGatewayToken(_port, token) {
325
331
  try {
326
- const controller = new AbortController();
327
- const timeout = setTimeout(() => controller.abort(), 5000);
328
- // Use the actual chat endpoint — /v1/models may not require auth
329
- const res = await fetch(`http://127.0.0.1:${port}/v1/chat/completions`, {
330
- method: 'POST',
331
- headers: {
332
- 'Content-Type': 'application/json',
333
- 'Authorization': `Bearer ${token}`,
334
- 'x-openclaw-agent-id': 'main',
335
- },
336
- body: JSON.stringify({
337
- messages: [{ role: 'user', content: 'ping' }],
338
- max_tokens: 1,
339
- }),
340
- signal: controller.signal,
341
- });
342
- clearTimeout(timeout);
343
- // 401 = token rejected. Anything else (200, 400, 500, etc.) = token accepted.
344
- const accepted = res.status !== 401;
345
- if (!accepted) {
346
- const body = await res.text().catch(() => '');
347
- console.log(`[pty-manager] Gateway token probe: 401 — ${body.slice(0, 200)}`);
332
+ if (fs.existsSync(GATEWAY_MARKER_PATH)) {
333
+ const marker = JSON.parse(fs.readFileSync(GATEWAY_MARKER_PATH, 'utf-8'));
334
+ if (marker.token === token) {
335
+ return true;
336
+ }
337
+ console.log(`[pty-manager] Gateway marker token mismatch (marker=${marker.token?.slice(0, 8)}..., ours=${token?.slice(0, 8)}...)`);
338
+ } else {
339
+ console.log('[pty-manager] No gateway marker file — gateway was not started by Colana');
348
340
  }
349
- return accepted;
350
341
  } catch (err) {
351
- console.log(`[pty-manager] Gateway token probe error: ${err.message}`);
352
- return true; // network error = assume gateway is fine, will fail later with better error
342
+ console.log(`[pty-manager] Failed to read gateway marker: ${err.message}`);
343
+ }
344
+ return false;
345
+ }
346
+
347
+ /**
348
+ * Write a marker file after successfully spawning the gateway.
349
+ * Records the token and timestamp so subsequent startups can verify.
350
+ */
351
+ function writeGatewayMarker(token) {
352
+ try {
353
+ const dir = path.dirname(GATEWAY_MARKER_PATH);
354
+ if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
355
+ fs.writeFileSync(GATEWAY_MARKER_PATH, JSON.stringify({
356
+ token,
357
+ timestamp: Date.now(),
358
+ }), 'utf-8');
359
+ } catch (err) {
360
+ console.warn(`[pty-manager] Failed to write gateway marker: ${err.message}`);
353
361
  }
354
362
  }
355
363
 
@@ -399,17 +407,17 @@ async function ensureOpenClawGateway(port) {
399
407
  }
400
408
 
401
409
  if (await tcpProbe('127.0.0.1', port)) {
402
- // Gateway is listening — verify our token works.
403
- // The gateway may be from a previous Colana session (different token),
404
- // started manually, or started by a pre-token Colana version.
410
+ // Gateway is listening — check if we spawned it with our current token.
411
+ // The gateway's /v1/models endpoint does NOT require auth, so HTTP probing
412
+ // is unreliable. We use a marker file written at spawn time instead.
405
413
  const token = getOrCreateGatewayToken();
406
- const tokenValid = await verifyGatewayToken(port, token);
414
+ const tokenValid = verifyGatewayToken(port, token);
407
415
  if (tokenValid) {
408
- console.log('[pty-manager] OpenClaw gateway already running, token valid');
416
+ console.log('[pty-manager] OpenClaw gateway already running, token valid (marker match)');
409
417
  return;
410
418
  }
411
- // Token mismatch: kill existing gateway and restart with our token
412
- console.log('[pty-manager] Gateway running but token mismatch — restarting with correct token...');
419
+ // Gateway was not started by us or token changed — kill and restart with our token
420
+ console.log('[pty-manager] Gateway running but not started by Colana (or token changed) — restarting with AUTH_TOKEN...');
413
421
  await killGatewayOnPort(port);
414
422
  // Fall through to spawn new gateway
415
423
  }
@@ -425,13 +433,14 @@ async function ensureOpenClawGateway(port) {
425
433
  const logFile = path.join(logDir, 'openclaw-gateway.log');
426
434
  const logFd = fs.openSync(logFile, 'a');
427
435
 
436
+ // Hoist token so it's accessible in the poll loop (for marker file write)
437
+ const gatewayToken = getOrCreateGatewayToken();
428
438
  let gwProcess;
429
439
  try {
430
440
  const { buildSafeSpawnEnv: buildGwEnv } = await import('./spawn-env.js');
431
441
  // Pass AUTH_TOKEN so the gateway uses a known bearer token for auth.
432
442
  // On platforms where OpenClaw config has gateway.auth: false (Windows),
433
443
  // this env var is the only way to enable token auth on the gateway.
434
- const gatewayToken = getOrCreateGatewayToken();
435
444
  const gwEnv = buildGwEnv({
436
445
  GOG_KEYRING_PASSWORD: process.env.GOG_KEYRING_PASSWORD || config.deriveGogKeyringPassword(),
437
446
  AUTH_TOKEN: gatewayToken,
@@ -502,6 +511,7 @@ async function ensureOpenClawGateway(port) {
502
511
 
503
512
  if (await tcpProbe('127.0.0.1', port)) {
504
513
  fs.closeSync(logFd);
514
+ writeGatewayMarker(gatewayToken);
505
515
  console.log('[pty-manager] OpenClaw gateway is now running');
506
516
  return;
507
517
  }
@@ -2099,6 +2109,8 @@ export async function restartOpenClawGateway() {
2099
2109
 
2100
2110
  if (isRunning) {
2101
2111
  console.log('[pty-manager] Restarting OpenClaw gateway to reload MCP config...');
2112
+ // Clear marker before kill so ensureOpenClawGateway knows to respawn
2113
+ try { fs.unlinkSync(GATEWAY_MARKER_PATH); } catch { /* may not exist */ }
2102
2114
  await killGatewayOnPort(gwPort);
2103
2115
  } else {
2104
2116
  console.log('[pty-manager] OpenClaw gateway not running — starting fresh...');
@@ -2118,6 +2130,15 @@ export async function restartOpenClawGateway() {
2118
2130
  // Start idle cleanup on module load
2119
2131
  startIdleCleanup();
2120
2132
 
2133
+ /**
2134
+ * Check if the running gateway was spawned by Colana with the current token.
2135
+ * Used by personal-agent-routes to distinguish gateway-level 401 from provider-level 401.
2136
+ */
2137
+ export function isGatewaySpawnedByColana() {
2138
+ const token = getOrCreateGatewayToken();
2139
+ return verifyGatewayToken(null, token);
2140
+ }
2141
+
2121
2142
  export { ensureOpenClawGateway };
2122
2143
 
2123
2144
  export default {