-
Notifications
You must be signed in to change notification settings - Fork 2.6k
fix: add Code-Server authentication support for RooCloud #7263
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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 | ||
|
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is a critical issue - the TODO comment indicates that CloudService doesn't actually support token-based authentication yet. Without this, the manual token entry collects a token but can't use it, making the entire feature non-functional. Should we implement a temporary workaround or wait for CloudService to add token support? |
||
| // 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() | ||
|
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This limitations notice will be shown after every authentication attempt. Should we track if it's been shown before to avoid annoying users with repeated notifications? |
||
| } 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 }) | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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 | ||
|
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Should this URL be configurable or at least defined as a constant? Hardcoding URLs makes it difficult to test or use different environments. |
||
| 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<string | null> { | ||
| 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<void> { | ||
| 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<boolean> { | ||
| 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 | ||
|
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This validation only checks if the token exists, not if it's actually valid. Could we add a proper validation call to the API here, or at least check the token format? Otherwise invalid tokens will be accepted and stored. |
||
| } 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", | ||
| ) | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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) | ||
| }) | ||
| }) | ||
| }) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should we wrap this in a try-catch? If throws an error other than cancellation, it won't be handled properly.