Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 33 additions & 1 deletion src/core/webview/webviewMessageHandler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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)
Copy link
Contributor Author

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.


if (token) {
// TODO: Pass the token to CloudService for authentication
Copy link
Contributor Author

Choose a reason for hiding this comment

The 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()
Copy link
Contributor Author

Choose a reason for hiding this comment

The 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.")
Expand All @@ -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 })
Expand Down
24 changes: 23 additions & 1 deletion src/extension.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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)

Expand Down Expand Up @@ -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)
Expand Down
133 changes: 133 additions & 0 deletions src/services/cloud/codeServerAuth.ts
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
Copy link
Contributor Author

Choose a reason for hiding this comment

The 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
Copy link
Contributor Author

Choose a reason for hiding this comment

The 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",
)
}
}
155 changes: 155 additions & 0 deletions src/utils/__tests__/environmentDetection.spec.ts
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)
})
})
})
Loading
Loading