From b7d5ce178a46d4ade8c3cb58eb5fff7af44dc44f Mon Sep 17 00:00:00 2001 From: "Kai (Tam Nhu) Tran" <61256810+kaitranntt@users.noreply.github.com> Date: Sat, 23 May 2026 22:07:15 -0400 Subject: [PATCH] fix(cursor-daemon): auto-generate token + expose for callers (#1384) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit PRs #1373/#1377 gated cursor daemon endpoints behind a token check, but startDaemon() did not generate or expose a token. Health-check probing during startup then 401'd against the new auth, so startDaemon waited 30s then reported failure. Same for tests asserting /404 and validation behavior — they couldn't reach the downstream logic past the 401 gate. - Auto-generate a 32-byte hex token in startDaemon when caller did not provide one - Return daemonToken in startDaemon result so callers/tests can attach it - Update integration tests to include x-ccs-cursor-token header --- src/cursor/cursor-daemon.ts | 25 ++++++++++++----- .../cursor-daemon-lifecycle.test.ts | 27 +++++++++++++------ 2 files changed, 38 insertions(+), 14 deletions(-) diff --git a/src/cursor/cursor-daemon.ts b/src/cursor/cursor-daemon.ts index b88a2b64..30e68817 100644 --- a/src/cursor/cursor-daemon.ts +++ b/src/cursor/cursor-daemon.ts @@ -6,6 +6,7 @@ */ import { spawn, ChildProcess } from 'child_process'; +import { randomBytes } from 'crypto'; import * as fs from 'fs'; import * as path from 'path'; import * as http from 'http'; @@ -126,14 +127,26 @@ export async function getDaemonStatus(port: number): Promise */ export async function startDaemon( config: CursorDaemonConfig -): Promise<{ success: boolean; pid?: number; error?: string }> { +): Promise<{ success: boolean; pid?: number; error?: string; daemonToken?: string }> { + // Auto-generate daemon token if not provided so /health and protected routes + // can be probed by the caller during startup polling. + const daemonToken = + config.daemon_token && config.daemon_token.trim() + ? config.daemon_token + : randomBytes(32).toString('hex'); + const effectiveConfig: CursorDaemonConfig = { ...config, daemon_token: daemonToken }; + // Check if already running - if (await isDaemonRunning(config.port, config.daemon_token)) { + if (await isDaemonRunning(effectiveConfig.port, effectiveConfig.daemon_token)) { logger.stage('dispatch', 'cursor.daemon.already_running', 'Cursor daemon already running', { provider: 'cursor', port: config.port, }); - return { success: true, pid: getPidFromFile() ?? undefined }; + return { + success: true, + pid: getPidFromFile() ?? undefined, + daemonToken: effectiveConfig.daemon_token, + }; } // Validate port before interpolation (prevents injection) @@ -208,7 +221,7 @@ export async function startDaemon( detached: true, env: { ...process.env, - CCS_CURSOR_DAEMON_TOKEN: config.daemon_token || '', + CCS_CURSOR_DAEMON_TOKEN: effectiveConfig.daemon_token || '', }, }); @@ -225,8 +238,8 @@ export async function startDaemon( const pollHealth = async () => { attempts++; - if (await isDaemonRunning(config.port, config.daemon_token)) { - safeResolve({ success: true, pid: proc.pid }); + if (await isDaemonRunning(effectiveConfig.port, effectiveConfig.daemon_token)) { + safeResolve({ success: true, pid: proc.pid, daemonToken: effectiveConfig.daemon_token }); } else if (attempts >= maxAttempts) { // Kill orphaned process if (proc.pid) { diff --git a/tests/integration/cursor-daemon-lifecycle.test.ts b/tests/integration/cursor-daemon-lifecycle.test.ts index c50aea68..acb21499 100644 --- a/tests/integration/cursor-daemon-lifecycle.test.ts +++ b/tests/integration/cursor-daemon-lifecycle.test.ts @@ -43,12 +43,14 @@ describe('cursor daemon lifecycle smoke', () => { const result = await startDaemon({ port, ghost_mode: true }); expect(result.success).toBe(true); + const daemonToken = result.daemonToken as string; const response = await fetch(`http://127.0.0.1:${port}/v1/messages`, { method: 'POST', headers: { 'Content-Type': 'application/json', 'anthropic-version': '2023-06-01', + 'x-ccs-cursor-token': daemonToken, }, body: JSON.stringify({ model: 'claude-sonnet-4.5', @@ -71,8 +73,9 @@ describe('cursor daemon lifecycle smoke', () => { const result = await startDaemon({ port, ghost_mode: true }); expect(result.success).toBe(true); expect(result.pid).toBeDefined(); + const daemonToken = result.daemonToken as string; - expect(await isDaemonRunning(port)).toBe(true); + expect(await isDaemonRunning(port, daemonToken)).toBe(true); const modelsResponse = await fetch(`http://127.0.0.1:${port}/v1/models`); expect(modelsResponse.status).toBe(200); @@ -82,7 +85,7 @@ describe('cursor daemon lifecycle smoke', () => { const chatResponse = await fetch(`http://127.0.0.1:${port}/v1/chat/completions`, { method: 'POST', - headers: { 'Content-Type': 'application/json' }, + headers: { 'Content-Type': 'application/json', 'x-ccs-cursor-token': daemonToken }, body: JSON.stringify({ model: 'gpt-4.1', messages: [{ role: 'user', content: 'hello' }], @@ -95,6 +98,7 @@ describe('cursor daemon lifecycle smoke', () => { headers: { 'Content-Type': 'application/json', 'anthropic-version': '2023-06-01', + 'x-ccs-cursor-token': daemonToken, }, body: JSON.stringify({ model: 'claude-sonnet-4.5', @@ -113,15 +117,18 @@ describe('cursor daemon lifecycle smoke', () => { const stopResult = await stopDaemon(); expect(stopResult.success).toBe(true); - expect(await isDaemonRunning(port)).toBe(false); + expect(await isDaemonRunning(port, daemonToken)).toBe(false); }, 35000); it('returns 404 for unknown routes', async () => { const port = 10000 + Math.floor(Math.random() * 50000); const result = await startDaemon({ port, ghost_mode: true }); expect(result.success).toBe(true); + const daemonToken = result.daemonToken as string; - const response = await fetch(`http://127.0.0.1:${port}/unknown`); + const response = await fetch(`http://127.0.0.1:${port}/unknown`, { + headers: { 'x-ccs-cursor-token': daemonToken }, + }); expect(response.status).toBe(404); }); @@ -138,10 +145,11 @@ describe('cursor daemon lifecycle smoke', () => { const result = await startDaemon({ port, ghost_mode: true }); expect(result.success).toBe(true); + const daemonToken = result.daemonToken as string; const response = await fetch(`http://127.0.0.1:${port}/v1/chat/completions`, { method: 'POST', - headers: { 'Content-Type': 'application/json' }, + headers: { 'Content-Type': 'application/json', 'x-ccs-cursor-token': daemonToken }, body: JSON.stringify({ model: 'gpt-4.1', messages: [{ role: 'user', content: 'hello' }], @@ -157,17 +165,19 @@ describe('cursor daemon lifecycle smoke', () => { const port = 10000 + Math.floor(Math.random() * 50000); const result = await startDaemon({ port, ghost_mode: true }); expect(result.success).toBe(true); + const daemonToken = result.daemonToken as string; + const tokenHeader = { 'x-ccs-cursor-token': daemonToken }; const invalidJson = await fetch(`http://127.0.0.1:${port}/v1/chat/completions`, { method: 'POST', - headers: { 'Content-Type': 'application/json' }, + headers: { 'Content-Type': 'application/json', ...tokenHeader }, body: '{invalid-json', }); expect(invalidJson.status).toBe(400); const invalidSchema = await fetch(`http://127.0.0.1:${port}/v1/chat/completions`, { method: 'POST', - headers: { 'Content-Type': 'application/json' }, + headers: { 'Content-Type': 'application/json', ...tokenHeader }, body: JSON.stringify({ model: 'gpt-4.1', messages: { role: 'user', content: 'hello' }, @@ -180,6 +190,7 @@ describe('cursor daemon lifecycle smoke', () => { headers: { 'Content-Type': 'application/json', 'anthropic-version': '2023-06-01', + ...tokenHeader, }, body: JSON.stringify({ model: 'claude-sonnet-4.5', @@ -198,7 +209,7 @@ describe('cursor daemon lifecycle smoke', () => { const oversized = await fetch(`http://127.0.0.1:${port}/v1/chat/completions`, { method: 'POST', - headers: { 'Content-Type': 'application/json' }, + headers: { 'Content-Type': 'application/json', ...tokenHeader }, body: JSON.stringify({ model: 'gpt-4.1', messages: [