diff --git a/src/core/webview/webviewMessageHandler.ts b/src/core/webview/webviewMessageHandler.ts index 4dd0fee75e..67d1f84a27 100644 --- a/src/core/webview/webviewMessageHandler.ts +++ b/src/core/webview/webviewMessageHandler.ts @@ -16,6 +16,8 @@ import { import { CloudService } from "@roo-code/cloud" import { TelemetryService } from "@roo-code/telemetry" import { type ApiMessage } from "../task-persistence/apiMessages" +import { isCodeServerEnvironment } from "../../utils/environmentDetection" +import { CodeServerAuthHandler } from "../../services/cloud/codeServerAuth" import { ClineProvider } from "./ClineProvider" import { changeLanguage, t } from "../../i18n" @@ -1999,7 +2001,32 @@ export const webviewMessageHandler = async ( case "rooCloudSignIn": { try { TelemetryService.instance.captureEvent(TelemetryEventName.AUTHENTICATION_INITIATED) - await CloudService.instance.login() + + // Check if we're in Code-Server environment + if (isCodeServerEnvironment()) { + provider.log("Code-Server environment detected, using alternative authentication") + + // Use manual token authentication for Code-Server + const token = await CodeServerAuthHandler.handleCodeServerAuth(provider.context) + + if (token) { + // TODO: Pass the token to CloudService for authentication + // This would require CloudService to support token-based auth + // For now, we'll show a message about the limitation + vscode.window.showInformationMessage( + "Token authentication for Code-Server is being implemented. " + + "Please use desktop VS Code for full Roo Cloud functionality.", + ) + + // Show limitations notice + CodeServerAuthHandler.showCodeServerLimitations() + } else { + vscode.window.showWarningMessage("Authentication cancelled") + } + } else { + // Standard OAuth flow for desktop/regular VS Code + await CloudService.instance.login() + } } catch (error) { provider.log(`AuthService#login failed: ${error}`) vscode.window.showErrorMessage("Sign in failed.") @@ -2009,6 +2036,11 @@ export const webviewMessageHandler = async ( } case "rooCloudSignOut": { try { + // Clear stored token if in Code-Server environment + if (isCodeServerEnvironment()) { + await CodeServerAuthHandler.clearStoredToken(provider.context) + } + await CloudService.instance.logout() await provider.postStateToWebview() provider.postMessageToWebview({ type: "authenticatedUser", userInfo: undefined }) diff --git a/src/extension.ts b/src/extension.ts index 6cb6ea4b07..4c2e432bda 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -30,6 +30,7 @@ import { MdmService } from "./services/mdm/MdmService" import { migrateSettings } from "./utils/migrateSettings" import { autoImportSettings } from "./utils/autoImportSettings" import { isRemoteControlEnabled } from "./utils/remoteControl" +import { isCodeServerEnvironment, getEnvironmentInfo } from "./utils/environmentDetection" import { API } from "./extension/api" import { @@ -60,6 +61,20 @@ export async function activate(context: vscode.ExtensionContext) { context.subscriptions.push(outputChannel) outputChannel.appendLine(`${Package.name} extension activated - ${JSON.stringify(Package)}`) + // Log environment information for debugging + const envInfo = getEnvironmentInfo() + outputChannel.appendLine(`Environment: ${JSON.stringify(envInfo)}`) + + // Warn if running in Code-Server environment + if (isCodeServerEnvironment()) { + outputChannel.appendLine( + "⚠️ Code-Server environment detected. OAuth authentication may require special handling.", + ) + vscode.window.showInformationMessage( + "Roo Code: Running in Code-Server environment. Authentication may work differently than in desktop VS Code.", + ) + } + // Migrate old settings to new await migrateSettings(context, outputChannel) @@ -113,9 +128,16 @@ export async function activate(context: vscode.ExtensionContext) { } } - // Initialize Roo Code Cloud service. + // Initialize Roo Code Cloud service with Code-Server detection const cloudService = await CloudService.createInstance(context, cloudLogger) + // If in Code-Server environment, configure alternative authentication + if (isCodeServerEnvironment()) { + outputChannel.appendLine("[CloudService] Configuring for Code-Server environment") + // The CloudService will need to handle authentication differently + // This will be implemented in the @roo-code/cloud package + } + try { if (cloudService.telemetryClient) { TelemetryService.instance.register(cloudService.telemetryClient) diff --git a/src/services/cloud/codeServerAuth.ts b/src/services/cloud/codeServerAuth.ts new file mode 100644 index 0000000000..c4872f77c9 --- /dev/null +++ b/src/services/cloud/codeServerAuth.ts @@ -0,0 +1,133 @@ +import * as vscode from "vscode" +import { isCodeServerEnvironment } from "../../utils/environmentDetection" + +/** + * Handles authentication for Code-Server environments where OAuth redirects may not work + */ +export class CodeServerAuthHandler { + private static readonly AUTH_TOKEN_KEY = "roocloud.authToken" + private static readonly MANUAL_AUTH_INSTRUCTIONS = ` +To authenticate with Roo Code Cloud in Code-Server: + +1. Open Roo Code in a regular browser or desktop VS Code +2. Sign in to Roo Code Cloud there +3. Go to Settings > Account and copy your authentication token +4. Return here and paste the token when prompted + +Alternatively, you can: +1. Visit https://app.roo-code.com/auth/token +2. Sign in with your account +3. Copy the generated token +4. Paste it here +` + + /** + * Attempts to handle authentication in Code-Server environment + * @returns The authentication token if successful, null otherwise + */ + public static async handleCodeServerAuth(context: vscode.ExtensionContext): Promise { + if (!isCodeServerEnvironment()) { + return null + } + + // Check if we have a stored token + const storedToken = await context.secrets.get(this.AUTH_TOKEN_KEY) + if (storedToken) { + const useStored = await vscode.window.showQuickPick(["Use stored token", "Enter new token"], { + placeHolder: "A stored authentication token was found. What would you like to do?", + }) + + if (useStored === "Use stored token") { + return storedToken + } + } + + // Show instructions and prompt for manual token entry + const action = await vscode.window.showInformationMessage( + "Code-Server detected: Manual authentication required for Roo Code Cloud", + "Enter Token", + "Show Instructions", + "Cancel", + ) + + if (action === "Show Instructions") { + // Create a webview or show a document with detailed instructions + const doc = await vscode.workspace.openTextDocument({ + content: this.MANUAL_AUTH_INSTRUCTIONS, + language: "markdown", + }) + await vscode.window.showTextDocument(doc, { preview: true }) + + // After showing instructions, ask again + const proceed = await vscode.window.showQuickPick(["Enter Token", "Cancel"], { + placeHolder: "Ready to enter your authentication token?", + }) + + if (proceed !== "Enter Token") { + return null + } + } else if (action !== "Enter Token") { + return null + } + + // Prompt for token input + const token = await vscode.window.showInputBox({ + prompt: "Enter your Roo Code Cloud authentication token", + placeHolder: "Paste your token here", + password: true, // Hide the token as it's being typed + ignoreFocusOut: true, + validateInput: (value) => { + if (!value || value.trim().length === 0) { + return "Token cannot be empty" + } + // Basic validation - tokens should have a certain format + if (value.trim().length < 20) { + return "Token appears to be too short" + } + return null + }, + }) + + if (!token) { + return null + } + + // Store the token securely + await context.secrets.store(this.AUTH_TOKEN_KEY, token.trim()) + vscode.window.showInformationMessage("Authentication token saved successfully") + + return token.trim() + } + + /** + * Clears the stored authentication token + */ + public static async clearStoredToken(context: vscode.ExtensionContext): Promise { + await context.secrets.delete(this.AUTH_TOKEN_KEY) + } + + /** + * Validates if a token is still valid by making a test API call + */ + public static async validateToken(token: string, apiUrl: string): Promise { + try { + // This would need to be implemented based on the actual API + // For now, we'll assume the token is valid if it exists + return token.length > 0 + } catch (error) { + console.error("Token validation failed:", error) + return false + } + } + + /** + * Shows a notification about Code-Server limitations + */ + public static showCodeServerLimitations(): void { + vscode.window.showInformationMessage( + "Note: Some Roo Code Cloud features may be limited in Code-Server environments. " + + "For the best experience, use desktop VS Code when possible.", + "Understood", + ) + } +} diff --git a/src/utils/__tests__/environmentDetection.spec.ts b/src/utils/__tests__/environmentDetection.spec.ts new file mode 100644 index 0000000000..7f36264c46 --- /dev/null +++ b/src/utils/__tests__/environmentDetection.spec.ts @@ -0,0 +1,155 @@ +import { describe, it, expect, vi, beforeEach, afterEach } from "vitest" +import * as vscode from "vscode" +import { isCodeServerEnvironment, getEnvironmentInfo, shouldUseAlternativeAuth } from "../environmentDetection" + +// Mock vscode module +vi.mock("vscode", () => ({ + env: { + uiKind: 1, // Default to Desktop + appName: "Visual Studio Code", + remoteName: undefined, + }, + UIKind: { + Desktop: 1, + Web: 2, + }, +})) + +describe("environmentDetection", () => { + let originalEnv: NodeJS.ProcessEnv + + beforeEach(() => { + // Save original environment + originalEnv = { ...process.env } + // Clear relevant environment variables + delete process.env.CODE_SERVER_VERSION + delete process.env.DOCKER_CONTAINER + delete process.env.KUBERNETES_SERVICE_HOST + delete process.env.COOLIFY_CONTAINER_NAME + delete process.env.COOLIFY_APP_ID + // Reset vscode mock to default values + vi.mocked(vscode.env).uiKind = vscode.UIKind.Desktop + vi.mocked(vscode.env).appName = "Visual Studio Code" + vi.mocked(vscode.env).remoteName = undefined + }) + + afterEach(() => { + // Restore original environment + process.env = originalEnv + }) + + describe("isCodeServerEnvironment", () => { + it("should return true when CODE_SERVER_VERSION is set", () => { + process.env.CODE_SERVER_VERSION = "4.0.0" + expect(isCodeServerEnvironment()).toBe(true) + }) + + it("should return true when running in Web UI", () => { + vi.mocked(vscode.env).uiKind = vscode.UIKind.Web + expect(isCodeServerEnvironment()).toBe(true) + }) + + it("should return false when running in Desktop UI", () => { + vi.mocked(vscode.env).uiKind = vscode.UIKind.Desktop + expect(isCodeServerEnvironment()).toBe(false) + }) + + it("should return true when app name contains code-server", () => { + vi.mocked(vscode.env).appName = "Code-Server" + expect(isCodeServerEnvironment()).toBe(true) + }) + + it("should return true when app name contains code server (with space)", () => { + vi.mocked(vscode.env).appName = "Code Server" + expect(isCodeServerEnvironment()).toBe(true) + }) + + it("should return true when DOCKER_CONTAINER is set and UI is Web", () => { + process.env.DOCKER_CONTAINER = "true" + vi.mocked(vscode.env).uiKind = vscode.UIKind.Web + expect(isCodeServerEnvironment()).toBe(true) + }) + + it("should return false when DOCKER_CONTAINER is set but UI is Desktop", () => { + process.env.DOCKER_CONTAINER = "true" + vi.mocked(vscode.env).uiKind = vscode.UIKind.Desktop + expect(isCodeServerEnvironment()).toBe(false) + }) + + it("should return true when KUBERNETES_SERVICE_HOST is set and UI is Web", () => { + process.env.KUBERNETES_SERVICE_HOST = "10.0.0.1" + vi.mocked(vscode.env).uiKind = vscode.UIKind.Web + expect(isCodeServerEnvironment()).toBe(true) + }) + + it("should return true when COOLIFY_CONTAINER_NAME is set", () => { + process.env.COOLIFY_CONTAINER_NAME = "my-app" + expect(isCodeServerEnvironment()).toBe(true) + }) + + it("should return true when COOLIFY_APP_ID is set", () => { + process.env.COOLIFY_APP_ID = "app-123" + expect(isCodeServerEnvironment()).toBe(true) + }) + + it("should return false in regular desktop VS Code", () => { + vi.mocked(vscode.env).uiKind = vscode.UIKind.Desktop + vi.mocked(vscode.env).appName = "Visual Studio Code" + expect(isCodeServerEnvironment()).toBe(false) + }) + }) + + describe("getEnvironmentInfo", () => { + it("should return correct environment info for desktop VS Code", () => { + vi.mocked(vscode.env).uiKind = vscode.UIKind.Desktop + vi.mocked(vscode.env).appName = "Visual Studio Code" + vi.mocked(vscode.env).remoteName = undefined + + const info = getEnvironmentInfo() + expect(info).toEqual({ + isCodeServer: false, + uiKind: "Desktop", + appName: "Visual Studio Code", + isRemote: false, + }) + }) + + it("should return correct environment info for Code-Server", () => { + process.env.CODE_SERVER_VERSION = "4.0.0" + vi.mocked(vscode.env).uiKind = vscode.UIKind.Web + vi.mocked(vscode.env).appName = "Code-Server" + vi.mocked(vscode.env).remoteName = "ssh-remote" + + const info = getEnvironmentInfo() + expect(info).toEqual({ + isCodeServer: true, + uiKind: "Web", + appName: "Code-Server", + isRemote: true, + }) + }) + + it("should detect remote environment correctly", () => { + vi.mocked(vscode.env).remoteName = "wsl" + const info = getEnvironmentInfo() + expect(info.isRemote).toBe(true) + }) + }) + + describe("shouldUseAlternativeAuth", () => { + it("should return true when in Code-Server environment", () => { + process.env.CODE_SERVER_VERSION = "4.0.0" + expect(shouldUseAlternativeAuth()).toBe(true) + }) + + it("should return false when in desktop VS Code", () => { + vi.mocked(vscode.env).uiKind = vscode.UIKind.Desktop + expect(shouldUseAlternativeAuth()).toBe(false) + }) + + it("should return true when UI is Web", () => { + vi.mocked(vscode.env).uiKind = vscode.UIKind.Web + expect(shouldUseAlternativeAuth()).toBe(true) + }) + }) +}) diff --git a/src/utils/environmentDetection.ts b/src/utils/environmentDetection.ts new file mode 100644 index 0000000000..5274664153 --- /dev/null +++ b/src/utils/environmentDetection.ts @@ -0,0 +1,69 @@ +import * as vscode from "vscode" + +/** + * Detects if the extension is running in a Code-Server environment + * Code-Server is a web-based VS Code that runs on remote servers + */ +export function isCodeServerEnvironment(): boolean { + // Check for Code-Server specific environment variables + if (process.env.CODE_SERVER_VERSION) { + return true + } + + // Check if running in a browser context + // We need to check the numeric value since TypeScript may not recognize the enum comparison + const isWebUI = vscode.env.uiKind !== vscode.UIKind.Desktop + if (isWebUI) { + return true + } + + // Check for Docker/container environment indicators that might suggest Code-Server + if (process.env.DOCKER_CONTAINER || process.env.KUBERNETES_SERVICE_HOST) { + // Additional check for web UI to confirm it's Code-Server + if (isWebUI) { + return true + } + } + + // Check if the app name contains code-server + const appName = vscode.env.appName.toLowerCase() + if (appName.includes("code-server") || appName.includes("code server")) { + return true + } + + // Check for Coolify-specific environment variables + if (process.env.COOLIFY_CONTAINER_NAME || process.env.COOLIFY_APP_ID) { + return true + } + + return false +} + +/** + * Gets information about the current environment + */ +export function getEnvironmentInfo(): { + isCodeServer: boolean + uiKind: string + appName: string + isRemote: boolean +} { + // Map UIKind enum value to string + const isWebUI = vscode.env.uiKind !== vscode.UIKind.Desktop + const uiKindString = isWebUI ? "Web" : "Desktop" + + return { + isCodeServer: isCodeServerEnvironment(), + uiKind: uiKindString, + appName: vscode.env.appName, + isRemote: vscode.env.remoteName !== undefined, + } +} + +/** + * Determines if OAuth-based authentication should be used + * In Code-Server environments, OAuth redirects may not work properly + */ +export function shouldUseAlternativeAuth(): boolean { + return isCodeServerEnvironment() +}