From 8dd99a025421421b56ce00baaaa26350301f94c9 Mon Sep 17 00:00:00 2001 From: Matt Rubens Date: Mon, 16 Jun 2025 18:31:22 -0400 Subject: [PATCH] Support config policies that require use of cloud (#4762) * Support config policies that require use of cloud * Switch to vitest --- src/core/tools/newTaskTool.ts | 7 +- src/core/webview/ClineProvider.ts | 32 ++ src/extension.ts | 6 +- src/extension/api.ts | 8 +- src/i18n/locales/ca/common.json | 7 + src/i18n/locales/ca/tools.json | 5 + src/i18n/locales/de/common.json | 7 + src/i18n/locales/de/tools.json | 5 + src/i18n/locales/en/common.json | 7 + src/i18n/locales/en/tools.json | 5 + src/i18n/locales/es/common.json | 7 + src/i18n/locales/es/tools.json | 5 + src/i18n/locales/fr/common.json | 7 + src/i18n/locales/fr/tools.json | 5 + src/i18n/locales/hi/common.json | 7 + src/i18n/locales/hi/tools.json | 5 + src/i18n/locales/id/common.json | 7 + src/i18n/locales/id/tools.json | 5 + src/i18n/locales/it/common.json | 7 + src/i18n/locales/it/tools.json | 5 + src/i18n/locales/ja/common.json | 7 + src/i18n/locales/ja/tools.json | 5 + src/i18n/locales/ko/common.json | 7 + src/i18n/locales/ko/tools.json | 5 + src/i18n/locales/nl/common.json | 7 + src/i18n/locales/nl/tools.json | 5 + src/i18n/locales/pl/common.json | 7 + src/i18n/locales/pl/tools.json | 5 + src/i18n/locales/pt-BR/common.json | 7 + src/i18n/locales/pt-BR/tools.json | 5 + src/i18n/locales/ru/common.json | 7 + src/i18n/locales/ru/tools.json | 5 + src/i18n/locales/tr/common.json | 7 + src/i18n/locales/tr/tools.json | 5 + src/i18n/locales/vi/common.json | 7 + src/i18n/locales/vi/tools.json | 5 + src/i18n/locales/zh-CN/common.json | 7 + src/i18n/locales/zh-CN/tools.json | 5 + src/i18n/locales/zh-TW/common.json | 7 + src/i18n/locales/zh-TW/tools.json | 5 + src/services/mdm/MdmService.ts | 205 +++++++++ src/services/mdm/__tests__/MdmService.spec.ts | 420 ++++++++++++++++++ 42 files changed, 890 insertions(+), 4 deletions(-) create mode 100644 src/services/mdm/MdmService.ts create mode 100644 src/services/mdm/__tests__/MdmService.spec.ts diff --git a/src/core/tools/newTaskTool.ts b/src/core/tools/newTaskTool.ts index 25d5766d5d..ab2519e9b4 100644 --- a/src/core/tools/newTaskTool.ts +++ b/src/core/tools/newTaskTool.ts @@ -4,6 +4,7 @@ import { ToolUse, AskApproval, HandleError, PushToolResult, RemoveClosingTag } f import { Task } from "../task/Task" import { defaultModeSlug, getModeBySlug } from "../../shared/modes" import { formatResponse } from "../prompts/responses" +import { t } from "../../i18n" export async function newTaskTool( cline: Task, @@ -43,7 +44,7 @@ export async function newTaskTool( cline.consecutiveMistakeCount = 0 // Un-escape one level of backslashes before '@' for hierarchical subtasks -// Un-escape one level: \\@ -> \@ (removes one backslash for hierarchical subtasks) + // Un-escape one level: \\@ -> \@ (removes one backslash for hierarchical subtasks) const unescapedMessage = message.replace(/\\\\@/g, "\\@") // Verify the mode exists @@ -86,6 +87,10 @@ export async function newTaskTool( await delay(500) const newCline = await provider.initClineWithTask(unescapedMessage, undefined, cline) + if (!newCline) { + pushToolResult(t("tools:newTask.errors.policy_restriction")) + return + } cline.emit("taskSpawned", newCline.taskId) pushToolResult(`Successfully created new task in ${targetMode.name} mode with message: ${unescapedMessage}`) diff --git a/src/core/webview/ClineProvider.ts b/src/core/webview/ClineProvider.ts index 57fa16a848..b486015f1b 100644 --- a/src/core/webview/ClineProvider.ts +++ b/src/core/webview/ClineProvider.ts @@ -51,6 +51,7 @@ import { MarketplaceManager } from "../../services/marketplace" import { ShadowCheckpointService } from "../../services/checkpoints/ShadowCheckpointService" import { CodeIndexManager } from "../../services/code-index/manager" import type { IndexProgressUpdate } from "../../services/code-index/interfaces/manager" +import { MdmService } from "../../services/mdm/MdmService" import { fileExistsAtPath } from "../../utils/fs" import { setTtsEnabled, setTtsSpeed } from "../../utils/tts" import { ContextProxy } from "../config/ContextProxy" @@ -103,6 +104,7 @@ export class ClineProvider } protected mcpHub?: McpHub // Change from private to protected private marketplaceManager: MarketplaceManager + private mdmService?: MdmService public isViewLaunched = false public settingsImportedAt?: number @@ -116,6 +118,7 @@ export class ClineProvider private readonly renderContext: "sidebar" | "editor" = "sidebar", public readonly contextProxy: ContextProxy, public readonly codeIndexManager?: CodeIndexManager, + mdmService?: MdmService, ) { super() @@ -123,6 +126,7 @@ export class ClineProvider ClineProvider.activeInstances.add(this) this.codeIndexManager = codeIndexManager + this.mdmService = mdmService this.updateGlobalState("codebaseIndexModels", EMBEDDING_MODEL_PROFILES) // Start configuration loading (which might trigger indexing) in the background. @@ -525,6 +529,11 @@ export class ClineProvider > > = {}, ) { + // Check MDM compliance before proceeding + if (!this.checkMdmCompliance()) { + return // Block task creation if not compliant + } + const { apiConfiguration, organizationAllowList, @@ -1700,6 +1709,29 @@ export class ClineProvider return this.mcpHub } + /** + * Check if the current state is compliant with MDM policy + * @returns true if compliant, false if blocked + */ + public checkMdmCompliance(): boolean { + if (!this.mdmService) { + return true // No MDM service, allow operation + } + + const compliance = this.mdmService.isCompliant() + + if (!compliance.compliant) { + vscode.window.showErrorMessage(compliance.reason, "Sign In").then((selection) => { + if (selection === "Sign In") { + this.postMessageToWebview({ type: "action", action: "accountButtonClicked" }) + } + }) + return false + } + + return true + } + /** * Returns properties to be included in every telemetry event * This method is called by the telemetry service to get context information diff --git a/src/extension.ts b/src/extension.ts index 64963ac4d8..9e3daad662 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -26,6 +26,7 @@ import { DIFF_VIEW_URI_SCHEME } from "./integrations/editor/DiffViewProvider" import { TerminalRegistry } from "./integrations/terminal/TerminalRegistry" import { McpServerManager } from "./services/mcp/McpServerManager" import { CodeIndexManager } from "./services/code-index/manager" +import { MdmService } from "./services/mdm/MdmService" import { migrateSettings } from "./utils/migrateSettings" import { API } from "./extension/api" @@ -78,6 +79,9 @@ export async function activate(context: vscode.ExtensionContext) { log: cloudLogger, }) + // Initialize MDM service + const mdmService = await MdmService.createInstance(cloudLogger) + // Initialize i18n for internationalization support initializeI18n(context.globalState.get("language") ?? formatLanguage(vscode.env.language)) @@ -103,7 +107,7 @@ export async function activate(context: vscode.ExtensionContext) { ) } - const provider = new ClineProvider(context, outputChannel, "sidebar", contextProxy, codeIndexManager) + const provider = new ClineProvider(context, outputChannel, "sidebar", contextProxy, codeIndexManager, mdmService) TelemetryService.instance.setProvider(provider) if (codeIndexManager) { diff --git a/src/extension/api.ts b/src/extension/api.ts index e4cab41d29..021fb7e618 100644 --- a/src/extension/api.ts +++ b/src/extension/api.ts @@ -132,11 +132,15 @@ export class API extends EventEmitter implements RooCodeAPI { await provider.postMessageToWebview({ type: "action", action: "chatButtonClicked" }) await provider.postMessageToWebview({ type: "invoke", invoke: "newChat", text, images }) - const { taskId } = await provider.initClineWithTask(text, images, undefined, { + const cline = await provider.initClineWithTask(text, images, undefined, { consecutiveMistakeLimit: Number.MAX_SAFE_INTEGER, }) - return taskId + if (!cline) { + throw new Error("Failed to create task due to policy restrictions") + } + + return cline.taskId } public async resumeTask(taskId: string): Promise { diff --git a/src/i18n/locales/ca/common.json b/src/i18n/locales/ca/common.json index 31b9668a86..c94e1042fe 100644 --- a/src/i18n/locales/ca/common.json +++ b/src/i18n/locales/ca/common.json @@ -102,5 +102,12 @@ "groqApiKey": "Clau API de Groq", "getGroqApiKey": "Obté la clau API de Groq" } + }, + "mdm": { + "errors": { + "cloud_auth_required": "La teva organització requereix autenticació de Roo Code Cloud. Si us plau, inicia sessió per continuar.", + "organization_mismatch": "Has d'estar autenticat amb el compte de Roo Code Cloud de la teva organització.", + "verification_failed": "No s'ha pogut verificar l'autenticació de l'organització." + } } } diff --git a/src/i18n/locales/ca/tools.json b/src/i18n/locales/ca/tools.json index 0fe673310f..5b3a228bde 100644 --- a/src/i18n/locales/ca/tools.json +++ b/src/i18n/locales/ca/tools.json @@ -7,5 +7,10 @@ "toolRepetitionLimitReached": "Roo sembla estar atrapat en un bucle, intentant la mateixa acció ({{toolName}}) repetidament. Això podria indicar un problema amb la seva estratègia actual. Considera reformular la tasca, proporcionar instruccions més específiques o guiar-lo cap a un enfocament diferent.", "codebaseSearch": { "approval": "Cercant '{{query}}' a la base de codi..." + }, + "newTask": { + "errors": { + "policy_restriction": "No s'ha pogut crear una nova tasca a causa de restriccions de política." + } } } diff --git a/src/i18n/locales/de/common.json b/src/i18n/locales/de/common.json index ab0cd61ac9..4a37795bd6 100644 --- a/src/i18n/locales/de/common.json +++ b/src/i18n/locales/de/common.json @@ -102,5 +102,12 @@ "groqApiKey": "Groq API-Schlüssel", "getGroqApiKey": "Groq API-Schlüssel erhalten" } + }, + "mdm": { + "errors": { + "cloud_auth_required": "Deine Organisation erfordert eine Roo Code Cloud-Authentifizierung. Bitte melde dich an, um fortzufahren.", + "organization_mismatch": "Du musst mit dem Roo Code Cloud-Konto deiner Organisation authentifiziert sein.", + "verification_failed": "Die Organisationsauthentifizierung konnte nicht verifiziert werden." + } } } diff --git a/src/i18n/locales/de/tools.json b/src/i18n/locales/de/tools.json index 03c491c115..eb1afbc082 100644 --- a/src/i18n/locales/de/tools.json +++ b/src/i18n/locales/de/tools.json @@ -7,5 +7,10 @@ "toolRepetitionLimitReached": "Roo scheint in einer Schleife festzustecken und versucht wiederholt dieselbe Aktion ({{toolName}}). Dies könnte auf ein Problem mit der aktuellen Strategie hindeuten. Überlege dir, die Aufgabe umzuformulieren, genauere Anweisungen zu geben oder Roo zu einem anderen Ansatz zu führen.", "codebaseSearch": { "approval": "Suche nach '{{query}}' im Codebase..." + }, + "newTask": { + "errors": { + "policy_restriction": "Neue Aufgabe konnte aufgrund von Richtlinienbeschränkungen nicht erstellt werden." + } } } diff --git a/src/i18n/locales/en/common.json b/src/i18n/locales/en/common.json index 7d359e6586..a505933b69 100644 --- a/src/i18n/locales/en/common.json +++ b/src/i18n/locales/en/common.json @@ -96,5 +96,12 @@ "input": { "task_prompt": "What should Roo do?", "task_placeholder": "Type your task here" + }, + "mdm": { + "errors": { + "cloud_auth_required": "Your organization requires Roo Code Cloud authentication. Please sign in to continue.", + "organization_mismatch": "You must be authenticated with your organization's Roo Code Cloud account.", + "verification_failed": "Unable to verify organization authentication." + } } } diff --git a/src/i18n/locales/en/tools.json b/src/i18n/locales/en/tools.json index 9932fc4d06..0265a84398 100644 --- a/src/i18n/locales/en/tools.json +++ b/src/i18n/locales/en/tools.json @@ -7,5 +7,10 @@ "toolRepetitionLimitReached": "Roo appears to be stuck in a loop, attempting the same action ({{toolName}}) repeatedly. This might indicate a problem with its current strategy. Consider rephrasing the task, providing more specific instructions, or guiding it towards a different approach.", "codebaseSearch": { "approval": "Searching for '{{query}}' in codebase..." + }, + "newTask": { + "errors": { + "policy_restriction": "Failed to create new task due to policy restrictions." + } } } diff --git a/src/i18n/locales/es/common.json b/src/i18n/locales/es/common.json index 3fb8602131..d85e725509 100644 --- a/src/i18n/locales/es/common.json +++ b/src/i18n/locales/es/common.json @@ -102,5 +102,12 @@ "groqApiKey": "Clave API de Groq", "getGroqApiKey": "Obtener clave API de Groq" } + }, + "mdm": { + "errors": { + "cloud_auth_required": "Tu organización requiere autenticación de Roo Code Cloud. Por favor, inicia sesión para continuar.", + "organization_mismatch": "Debes estar autenticado con la cuenta de Roo Code Cloud de tu organización.", + "verification_failed": "No se pudo verificar la autenticación de la organización." + } } } diff --git a/src/i18n/locales/es/tools.json b/src/i18n/locales/es/tools.json index 0dbba751b7..303f5365ed 100644 --- a/src/i18n/locales/es/tools.json +++ b/src/i18n/locales/es/tools.json @@ -7,5 +7,10 @@ "toolRepetitionLimitReached": "Roo parece estar atrapado en un bucle, intentando la misma acción ({{toolName}}) repetidamente. Esto podría indicar un problema con su estrategia actual. Considera reformular la tarea, proporcionar instrucciones más específicas o guiarlo hacia un enfoque diferente.", "codebaseSearch": { "approval": "Buscando '{{query}}' en la base de código..." + }, + "newTask": { + "errors": { + "policy_restriction": "No se pudo crear una nueva tarea debido a restricciones de política." + } } } diff --git a/src/i18n/locales/fr/common.json b/src/i18n/locales/fr/common.json index 70f22f4f9f..bceaceb8e9 100644 --- a/src/i18n/locales/fr/common.json +++ b/src/i18n/locales/fr/common.json @@ -102,5 +102,12 @@ "groqApiKey": "Clé API Groq", "getGroqApiKey": "Obtenir la clé API Groq" } + }, + "mdm": { + "errors": { + "cloud_auth_required": "Votre organisation nécessite une authentification Roo Code Cloud. Veuillez vous connecter pour continuer.", + "organization_mismatch": "Vous devez être authentifié avec le compte Roo Code Cloud de votre organisation.", + "verification_failed": "Impossible de vérifier l'authentification de l'organisation." + } } } diff --git a/src/i18n/locales/fr/tools.json b/src/i18n/locales/fr/tools.json index bdf26fb3cb..a6c71aca33 100644 --- a/src/i18n/locales/fr/tools.json +++ b/src/i18n/locales/fr/tools.json @@ -7,5 +7,10 @@ "toolRepetitionLimitReached": "Roo semble être bloqué dans une boucle, tentant la même action ({{toolName}}) de façon répétée. Cela pourrait indiquer un problème avec sa stratégie actuelle. Envisage de reformuler la tâche, de fournir des instructions plus spécifiques ou de le guider vers une approche différente.", "codebaseSearch": { "approval": "Recherche de '{{query}}' dans la base de code..." + }, + "newTask": { + "errors": { + "policy_restriction": "Impossible de créer une nouvelle tâche en raison de restrictions de politique." + } } } diff --git a/src/i18n/locales/hi/common.json b/src/i18n/locales/hi/common.json index 7b97e41ffc..9dfcdd6f46 100644 --- a/src/i18n/locales/hi/common.json +++ b/src/i18n/locales/hi/common.json @@ -102,5 +102,12 @@ "groqApiKey": "ग्रोक एपीआई कुंजी", "getGroqApiKey": "ग्रोक एपीआई कुंजी प्राप्त करें" } + }, + "mdm": { + "errors": { + "cloud_auth_required": "आपके संगठन को Roo Code Cloud प्रमाणीकरण की आवश्यकता है। कृपया जारी रखने के लिए साइन इन करें।", + "organization_mismatch": "आपको अपने संगठन के Roo Code Cloud खाते से प्रमाणित होना होगा।", + "verification_failed": "संगठन प्रमाणीकरण सत्यापित करने में असमर्थ।" + } } } diff --git a/src/i18n/locales/hi/tools.json b/src/i18n/locales/hi/tools.json index 257fc8a531..0cb4aeb14e 100644 --- a/src/i18n/locales/hi/tools.json +++ b/src/i18n/locales/hi/tools.json @@ -7,5 +7,10 @@ "toolRepetitionLimitReached": "Roo एक लूप में फंसा हुआ लगता है, बार-बार एक ही क्रिया ({{toolName}}) को दोहरा रहा है। यह उसकी वर्तमान रणनीति में किसी समस्या का संकेत हो सकता है। कार्य को पुनः परिभाषित करने, अधिक विशिष्ट निर्देश देने, या उसे एक अलग दृष्टिकोण की ओर मार्गदर्शित करने पर विचार करें।", "codebaseSearch": { "approval": "कोडबेस में '{{query}}' खोज रहा है..." + }, + "newTask": { + "errors": { + "policy_restriction": "नीति प्रतिबंधों के कारण नया कार्य बनाने में विफल।" + } } } diff --git a/src/i18n/locales/id/common.json b/src/i18n/locales/id/common.json index 16da42750e..c53c2e0a7c 100644 --- a/src/i18n/locales/id/common.json +++ b/src/i18n/locales/id/common.json @@ -96,5 +96,12 @@ "input": { "task_prompt": "Apa yang harus Roo lakukan?", "task_placeholder": "Ketik tugas kamu di sini" + }, + "mdm": { + "errors": { + "cloud_auth_required": "Organisasi kamu memerlukan autentikasi Roo Code Cloud. Silakan masuk untuk melanjutkan.", + "organization_mismatch": "Kamu harus diautentikasi dengan akun Roo Code Cloud organisasi kamu.", + "verification_failed": "Tidak dapat memverifikasi autentikasi organisasi." + } } } diff --git a/src/i18n/locales/id/tools.json b/src/i18n/locales/id/tools.json index 74e7e738cb..2e3c4f0c22 100644 --- a/src/i18n/locales/id/tools.json +++ b/src/i18n/locales/id/tools.json @@ -10,5 +10,10 @@ }, "searchFiles": { "workspaceBoundaryError": "Tidak dapat mencari di luar workspace. Path '{{path}}' berada di luar workspace saat ini." + }, + "newTask": { + "errors": { + "policy_restriction": "Gagal membuat tugas baru karena pembatasan kebijakan." + } } } diff --git a/src/i18n/locales/it/common.json b/src/i18n/locales/it/common.json index 21946d69e5..bf89af34f4 100644 --- a/src/i18n/locales/it/common.json +++ b/src/i18n/locales/it/common.json @@ -102,5 +102,12 @@ "groqApiKey": "Chiave API Groq", "getGroqApiKey": "Ottieni chiave API Groq" } + }, + "mdm": { + "errors": { + "cloud_auth_required": "La tua organizzazione richiede l'autenticazione Roo Code Cloud. Accedi per continuare.", + "organization_mismatch": "Devi essere autenticato con l'account Roo Code Cloud della tua organizzazione.", + "verification_failed": "Impossibile verificare l'autenticazione dell'organizzazione." + } } } diff --git a/src/i18n/locales/it/tools.json b/src/i18n/locales/it/tools.json index 0dc14f94a5..ffae474f1d 100644 --- a/src/i18n/locales/it/tools.json +++ b/src/i18n/locales/it/tools.json @@ -7,5 +7,10 @@ "toolRepetitionLimitReached": "Roo sembra essere bloccato in un ciclo, tentando ripetutamente la stessa azione ({{toolName}}). Questo potrebbe indicare un problema con la sua strategia attuale. Considera di riformulare l'attività, fornire istruzioni più specifiche o guidarlo verso un approccio diverso.", "codebaseSearch": { "approval": "Ricerca di '{{query}}' nella base di codice..." + }, + "newTask": { + "errors": { + "policy_restriction": "Impossibile creare una nuova attività a causa di restrizioni di policy." + } } } diff --git a/src/i18n/locales/ja/common.json b/src/i18n/locales/ja/common.json index 2cf2d50f29..0f06670456 100644 --- a/src/i18n/locales/ja/common.json +++ b/src/i18n/locales/ja/common.json @@ -102,5 +102,12 @@ "groqApiKey": "Groq APIキー", "getGroqApiKey": "Groq APIキーを取得" } + }, + "mdm": { + "errors": { + "cloud_auth_required": "あなたの組織では Roo Code Cloud 認証が必要です。続行するにはサインインしてください。", + "organization_mismatch": "組織の Roo Code Cloud アカウントで認証する必要があります。", + "verification_failed": "組織認証の確認ができませんでした。" + } } } diff --git a/src/i18n/locales/ja/tools.json b/src/i18n/locales/ja/tools.json index ad6b7019c8..04a5fcc085 100644 --- a/src/i18n/locales/ja/tools.json +++ b/src/i18n/locales/ja/tools.json @@ -7,5 +7,10 @@ "toolRepetitionLimitReached": "Rooが同じ操作({{toolName}})を繰り返し試みるループに陥っているようです。これは現在の方法に問題がある可能性を示しています。タスクの言い換え、より具体的な指示の提供、または別のアプローチへの誘導を検討してください。", "codebaseSearch": { "approval": "コードベースで '{{query}}' を検索中..." + }, + "newTask": { + "errors": { + "policy_restriction": "ポリシー制限により新しいタスクを作成できませんでした。" + } } } diff --git a/src/i18n/locales/ko/common.json b/src/i18n/locales/ko/common.json index 52264fcdd5..0c9ff07213 100644 --- a/src/i18n/locales/ko/common.json +++ b/src/i18n/locales/ko/common.json @@ -102,5 +102,12 @@ "groqApiKey": "Groq API 키", "getGroqApiKey": "Groq API 키 받기" } + }, + "mdm": { + "errors": { + "cloud_auth_required": "조직에서 Roo Code Cloud 인증이 필요합니다. 계속하려면 로그인하세요.", + "organization_mismatch": "조직의 Roo Code Cloud 계정으로 인증해야 합니다.", + "verification_failed": "조직 인증을 확인할 수 없습니다." + } } } diff --git a/src/i18n/locales/ko/tools.json b/src/i18n/locales/ko/tools.json index c8c8deebec..e43a541794 100644 --- a/src/i18n/locales/ko/tools.json +++ b/src/i18n/locales/ko/tools.json @@ -7,5 +7,10 @@ "toolRepetitionLimitReached": "Roo가 같은 동작({{toolName}})을 반복적으로 시도하면서 루프에 갇힌 것 같습니다. 이는 현재 전략에 문제가 있을 수 있음을 나타냅니다. 작업을 다시 표현하거나, 더 구체적인 지침을 제공하거나, 다른 접근 방식으로 안내해 보세요.", "codebaseSearch": { "approval": "코드베이스에서 '{{query}}' 검색 중..." + }, + "newTask": { + "errors": { + "policy_restriction": "정책 제한으로 인해 새 작업을 생성하지 못했습니다." + } } } diff --git a/src/i18n/locales/nl/common.json b/src/i18n/locales/nl/common.json index 26a856c15f..5d24fd7206 100644 --- a/src/i18n/locales/nl/common.json +++ b/src/i18n/locales/nl/common.json @@ -96,5 +96,12 @@ "input": { "task_prompt": "Wat moet Roo doen?", "task_placeholder": "Typ hier je taak" + }, + "mdm": { + "errors": { + "cloud_auth_required": "Je organisatie vereist Roo Code Cloud-authenticatie. Log in om door te gaan.", + "organization_mismatch": "Je moet geauthenticeerd zijn met het Roo Code Cloud-account van je organisatie.", + "verification_failed": "Kan organisatie-authenticatie niet verifiëren." + } } } diff --git a/src/i18n/locales/nl/tools.json b/src/i18n/locales/nl/tools.json index 8779caaf38..56a8cdbc46 100644 --- a/src/i18n/locales/nl/tools.json +++ b/src/i18n/locales/nl/tools.json @@ -7,5 +7,10 @@ "toolRepetitionLimitReached": "Roo lijkt vast te zitten in een lus, waarbij hij herhaaldelijk dezelfde actie ({{toolName}}) probeert. Dit kan duiden op een probleem met de huidige strategie. Overweeg de taak te herformuleren, specifiekere instructies te geven of Roo naar een andere aanpak te leiden.", "codebaseSearch": { "approval": "Zoeken naar '{{query}}' in codebase..." + }, + "newTask": { + "errors": { + "policy_restriction": "Kan geen nieuwe taak aanmaken vanwege beleidsbeperkingen." + } } } diff --git a/src/i18n/locales/pl/common.json b/src/i18n/locales/pl/common.json index 1091b643e2..842bb7ecec 100644 --- a/src/i18n/locales/pl/common.json +++ b/src/i18n/locales/pl/common.json @@ -102,5 +102,12 @@ "groqApiKey": "Klucz API Groq", "getGroqApiKey": "Uzyskaj klucz API Groq" } + }, + "mdm": { + "errors": { + "cloud_auth_required": "Twoja organizacja wymaga uwierzytelnienia Roo Code Cloud. Zaloguj się, aby kontynuować.", + "organization_mismatch": "Musisz być uwierzytelniony kontem Roo Code Cloud swojej organizacji.", + "verification_failed": "Nie można zweryfikować uwierzytelnienia organizacji." + } } } diff --git a/src/i18n/locales/pl/tools.json b/src/i18n/locales/pl/tools.json index 1cfb8d59de..62568826aa 100644 --- a/src/i18n/locales/pl/tools.json +++ b/src/i18n/locales/pl/tools.json @@ -7,5 +7,10 @@ "toolRepetitionLimitReached": "Wygląda na to, że Roo utknął w pętli, wielokrotnie próbując wykonać tę samą akcję ({{toolName}}). Może to wskazywać na problem z jego obecną strategią. Rozważ przeformułowanie zadania, podanie bardziej szczegółowych instrukcji lub nakierowanie go na inne podejście.", "codebaseSearch": { "approval": "Wyszukiwanie '{{query}}' w bazie kodu..." + }, + "newTask": { + "errors": { + "policy_restriction": "Nie udało się utworzyć nowego zadania z powodu ograniczeń polityki." + } } } diff --git a/src/i18n/locales/pt-BR/common.json b/src/i18n/locales/pt-BR/common.json index 6eb8fc7708..b9a1dd6d6f 100644 --- a/src/i18n/locales/pt-BR/common.json +++ b/src/i18n/locales/pt-BR/common.json @@ -102,5 +102,12 @@ "groqApiKey": "Chave de API Groq", "getGroqApiKey": "Obter chave de API Groq" } + }, + "mdm": { + "errors": { + "cloud_auth_required": "Sua organização requer autenticação do Roo Code Cloud. Faça login para continuar.", + "organization_mismatch": "Você deve estar autenticado com a conta Roo Code Cloud da sua organização.", + "verification_failed": "Não foi possível verificar a autenticação da organização." + } } } diff --git a/src/i18n/locales/pt-BR/tools.json b/src/i18n/locales/pt-BR/tools.json index 9c03e6082f..f74e0f8196 100644 --- a/src/i18n/locales/pt-BR/tools.json +++ b/src/i18n/locales/pt-BR/tools.json @@ -7,5 +7,10 @@ "toolRepetitionLimitReached": "Roo parece estar preso em um loop, tentando a mesma ação ({{toolName}}) repetidamente. Isso pode indicar um problema com sua estratégia atual. Considere reformular a tarefa, fornecer instruções mais específicas ou guiá-lo para uma abordagem diferente.", "codebaseSearch": { "approval": "Pesquisando '{{query}}' na base de código..." + }, + "newTask": { + "errors": { + "policy_restriction": "Falha ao criar nova tarefa devido a restrições de política." + } } } diff --git a/src/i18n/locales/ru/common.json b/src/i18n/locales/ru/common.json index a8e0479da1..e71766472d 100644 --- a/src/i18n/locales/ru/common.json +++ b/src/i18n/locales/ru/common.json @@ -102,5 +102,12 @@ "groqApiKey": "Ключ API Groq", "getGroqApiKey": "Получить ключ API Groq" } + }, + "mdm": { + "errors": { + "cloud_auth_required": "Ваша организация требует аутентификации Roo Code Cloud. Войдите в систему, чтобы продолжить.", + "organization_mismatch": "Вы должны быть аутентифицированы с учетной записью Roo Code Cloud вашей организации.", + "verification_failed": "Не удается проверить аутентификацию организации." + } } } diff --git a/src/i18n/locales/ru/tools.json b/src/i18n/locales/ru/tools.json index 42705f5ec3..1e59d10499 100644 --- a/src/i18n/locales/ru/tools.json +++ b/src/i18n/locales/ru/tools.json @@ -7,5 +7,10 @@ "toolRepetitionLimitReached": "Похоже, что Roo застрял в цикле, многократно пытаясь выполнить одно и то же действие ({{toolName}}). Это может указывать на проблему с его текущей стратегией. Попробуйте переформулировать задачу, предоставить более конкретные инструкции или направить его к другому подходу.", "codebaseSearch": { "approval": "Поиск '{{query}}' в кодовой базе..." + }, + "newTask": { + "errors": { + "policy_restriction": "Не удалось создать новую задачу из-за ограничений политики." + } } } diff --git a/src/i18n/locales/tr/common.json b/src/i18n/locales/tr/common.json index 182df50d1c..8c729c5c7a 100644 --- a/src/i18n/locales/tr/common.json +++ b/src/i18n/locales/tr/common.json @@ -102,5 +102,12 @@ "groqApiKey": "Groq API Anahtarı", "getGroqApiKey": "Groq API Anahtarı Al" } + }, + "mdm": { + "errors": { + "cloud_auth_required": "Kuruluşunuz Roo Code Cloud kimlik doğrulaması gerektiriyor. Devam etmek için giriş yapın.", + "organization_mismatch": "Kuruluşunuzun Roo Code Cloud hesabıyla kimlik doğrulaması yapmalısınız.", + "verification_failed": "Kuruluş kimlik doğrulaması doğrulanamıyor." + } } } diff --git a/src/i18n/locales/tr/tools.json b/src/i18n/locales/tr/tools.json index 4dff83eac4..e4c73cdc4b 100644 --- a/src/i18n/locales/tr/tools.json +++ b/src/i18n/locales/tr/tools.json @@ -7,5 +7,10 @@ "toolRepetitionLimitReached": "Roo bir döngüye takılmış gibi görünüyor, aynı eylemi ({{toolName}}) tekrar tekrar deniyor. Bu, mevcut stratejisinde bir sorun olduğunu gösterebilir. Görevi yeniden ifade etmeyi, daha spesifik talimatlar vermeyi veya onu farklı bir yaklaşıma yönlendirmeyi düşünün.", "codebaseSearch": { "approval": "Kod tabanında '{{query}}' aranıyor..." + }, + "newTask": { + "errors": { + "policy_restriction": "Politika kısıtlamaları nedeniyle yeni görev oluşturulamadı." + } } } diff --git a/src/i18n/locales/vi/common.json b/src/i18n/locales/vi/common.json index 2f32da8f70..ae5457ef14 100644 --- a/src/i18n/locales/vi/common.json +++ b/src/i18n/locales/vi/common.json @@ -102,5 +102,12 @@ "groqApiKey": "Khóa API Groq", "getGroqApiKey": "Lấy khóa API Groq" } + }, + "mdm": { + "errors": { + "cloud_auth_required": "Tổ chức của bạn yêu cầu xác thực Roo Code Cloud. Vui lòng đăng nhập để tiếp tục.", + "organization_mismatch": "Bạn phải được xác thực bằng tài khoản Roo Code Cloud của tổ chức.", + "verification_failed": "Không thể xác minh xác thực tổ chức." + } } } diff --git a/src/i18n/locales/vi/tools.json b/src/i18n/locales/vi/tools.json index 67d83f90fc..9811ee12c9 100644 --- a/src/i18n/locales/vi/tools.json +++ b/src/i18n/locales/vi/tools.json @@ -7,5 +7,10 @@ "toolRepetitionLimitReached": "Roo dường như đang bị mắc kẹt trong một vòng lặp, liên tục cố gắng thực hiện cùng một hành động ({{toolName}}). Điều này có thể cho thấy vấn đề với chiến lược hiện tại. Hãy cân nhắc việc diễn đạt lại nhiệm vụ, cung cấp hướng dẫn cụ thể hơn, hoặc hướng Roo theo một cách tiếp cận khác.", "codebaseSearch": { "approval": "Đang tìm kiếm '{{query}}' trong cơ sở mã..." + }, + "newTask": { + "errors": { + "policy_restriction": "Không thể tạo nhiệm vụ mới do hạn chế chính sách." + } } } diff --git a/src/i18n/locales/zh-CN/common.json b/src/i18n/locales/zh-CN/common.json index 45fd6d9b58..dd2e456890 100644 --- a/src/i18n/locales/zh-CN/common.json +++ b/src/i18n/locales/zh-CN/common.json @@ -107,5 +107,12 @@ "groqApiKey": "Groq API 密钥", "getGroqApiKey": "获取 Groq API 密钥" } + }, + "mdm": { + "errors": { + "cloud_auth_required": "您的组织需要 Roo Code Cloud 身份验证。请登录以继续。", + "organization_mismatch": "您必须使用组织的 Roo Code Cloud 账户进行身份验证。", + "verification_failed": "无法验证组织身份验证。" + } } } diff --git a/src/i18n/locales/zh-CN/tools.json b/src/i18n/locales/zh-CN/tools.json index 9328251d05..13641b8d43 100644 --- a/src/i18n/locales/zh-CN/tools.json +++ b/src/i18n/locales/zh-CN/tools.json @@ -7,5 +7,10 @@ "toolRepetitionLimitReached": "Roo 似乎陷入循环,反复尝试同一操作 ({{toolName}})。这可能表明当前策略存在问题。请考虑重新描述任务、提供更具体的指示或引导其尝试不同的方法。", "codebaseSearch": { "approval": "正在搜索代码库中的 '{{query}}'..." + }, + "newTask": { + "errors": { + "policy_restriction": "由于策略限制,无法创建新任务。" + } } } diff --git a/src/i18n/locales/zh-TW/common.json b/src/i18n/locales/zh-TW/common.json index 03e83b183a..298c7aa539 100644 --- a/src/i18n/locales/zh-TW/common.json +++ b/src/i18n/locales/zh-TW/common.json @@ -102,5 +102,12 @@ "groqApiKey": "Groq API 金鑰", "getGroqApiKey": "取得 Groq API 金鑰" } + }, + "mdm": { + "errors": { + "cloud_auth_required": "您的組織需要 Roo Code Cloud 身份驗證。請登入以繼續。", + "organization_mismatch": "您必須使用組織的 Roo Code Cloud 帳戶進行身份驗證。", + "verification_failed": "無法驗證組織身份驗證。" + } } } diff --git a/src/i18n/locales/zh-TW/tools.json b/src/i18n/locales/zh-TW/tools.json index 04b16c2bc7..a726e3c919 100644 --- a/src/i18n/locales/zh-TW/tools.json +++ b/src/i18n/locales/zh-TW/tools.json @@ -7,5 +7,10 @@ "toolRepetitionLimitReached": "Roo 似乎陷入循環,反覆嘗試同一操作 ({{toolName}})。這可能表明目前策略存在問題。請考慮重新描述工作、提供更具體的指示或引導其嘗試不同的方法。", "codebaseSearch": { "approval": "正在搜尋程式碼庫中的「{{query}}」..." + }, + "newTask": { + "errors": { + "policy_restriction": "由於政策限制,無法建立新工作。" + } } } diff --git a/src/services/mdm/MdmService.ts b/src/services/mdm/MdmService.ts new file mode 100644 index 0000000000..85fe038f19 --- /dev/null +++ b/src/services/mdm/MdmService.ts @@ -0,0 +1,205 @@ +import * as fs from "fs" +import * as path from "path" +import * as os from "os" +import * as vscode from "vscode" +import { z } from "zod" + +import { CloudService } from "@roo-code/cloud" +import { Package } from "../../shared/package" + +// MDM Configuration Schema +const mdmConfigSchema = z.object({ + requireCloudAuth: z.boolean(), + organizationId: z.string().optional(), +}) + +export type MdmConfig = z.infer + +export type ComplianceResult = { compliant: true } | { compliant: false; reason: string } + +export class MdmService { + private static _instance: MdmService | null = null + private mdmConfig: MdmConfig | null = null + private log: (...args: unknown[]) => void + + private constructor(log?: (...args: unknown[]) => void) { + this.log = log || console.log + } + + /** + * Initialize the MDM service by loading configuration + */ + public async initialize(): Promise { + try { + this.mdmConfig = await this.loadMdmConfig() + if (this.mdmConfig) { + this.log("[MDM] Loaded MDM configuration:", this.mdmConfig) + // Automatically enable Roo Code Cloud when MDM config is present + await this.ensureCloudEnabled() + } else { + this.log("[MDM] No MDM configuration found") + } + } catch (error) { + this.log("[MDM] Error loading MDM configuration:", error) + // Don't throw - extension should work without MDM config + } + } + + /** + * Check if cloud authentication is required by MDM policy + */ + public requiresCloudAuth(): boolean { + return this.mdmConfig?.requireCloudAuth ?? false + } + + /** + * Get the required organization ID from MDM policy + */ + public getRequiredOrganizationId(): string | undefined { + return this.mdmConfig?.organizationId + } + + /** + * Ensure Roo Code Cloud is enabled when MDM config is present + */ + private async ensureCloudEnabled(): Promise { + try { + const config = vscode.workspace.getConfiguration(Package.name) + const currentValue = config.get("rooCodeCloudEnabled", false) + + if (!currentValue) { + this.log("[MDM] Enabling Roo Code Cloud due to MDM policy") + await config.update("rooCodeCloudEnabled", true, vscode.ConfigurationTarget.Global) + } + } catch (error) { + this.log("[MDM] Error enabling Roo Code Cloud:", error) + } + } + + /** + * Check if the current state is compliant with MDM policy + */ + public isCompliant(): ComplianceResult { + // If no MDM policy, always compliant + if (!this.requiresCloudAuth()) { + return { compliant: true } + } + + // Check if cloud service is available and authenticated + if (!CloudService.hasInstance() || !CloudService.instance.hasActiveSession()) { + return { + compliant: false, + reason: "Your organization requires Roo Code Cloud authentication. Please sign in to continue.", + } + } + + // Check organization match if specified + const requiredOrgId = this.getRequiredOrganizationId() + if (requiredOrgId) { + try { + const currentOrgId = CloudService.instance.getOrganizationId() + if (currentOrgId !== requiredOrgId) { + return { + compliant: false, + reason: "You must be authenticated with your organization's Roo Code Cloud account.", + } + } + } catch (error) { + this.log("[MDM] Error checking organization ID:", error) + return { + compliant: false, + reason: "Unable to verify organization authentication.", + } + } + } + + return { compliant: true } + } + + /** + * Load MDM configuration from system location + */ + private async loadMdmConfig(): Promise { + const configPath = this.getMdmConfigPath() + + try { + // Check if file exists + if (!fs.existsSync(configPath)) { + return null + } + + // Read and parse the configuration file + const configContent = fs.readFileSync(configPath, "utf-8") + const parsedConfig = JSON.parse(configContent) + + // Validate against schema + return mdmConfigSchema.parse(parsedConfig) + } catch (error) { + this.log(`[MDM] Error reading MDM config from ${configPath}:`, error) + return null + } + } + + /** + * Get the platform-specific MDM configuration file path + */ + private getMdmConfigPath(): string { + const platform = os.platform() + const isProduction = process.env.NODE_ENV === "production" + const configFileName = isProduction ? "mcp.json" : "mcp.dev.json" + + switch (platform) { + case "win32": { + // Windows: %ProgramData%\RooCode\mcp.json or mcp.dev.json + const programData = process.env.PROGRAMDATA || "C:\\ProgramData" + return path.join(programData, "RooCode", configFileName) + } + + case "darwin": + // macOS: /Library/Application Support/RooCode/mcp.json or mcp.dev.json + return `/Library/Application Support/RooCode/${configFileName}` + + case "linux": + default: + // Linux: /etc/roo-code/mcp.json or mcp.dev.json + return `/etc/roo-code/${configFileName}` + } + } + + /** + * Get the singleton instance + */ + public static getInstance(): MdmService { + if (!this._instance) { + throw new Error("MdmService not initialized. Call createInstance() first.") + } + return this._instance + } + + /** + * Create and initialize the singleton instance + */ + public static async createInstance(log?: (...args: unknown[]) => void): Promise { + if (this._instance) { + throw new Error("MdmService instance already exists") + } + + this._instance = new MdmService(log) + await this._instance.initialize() + return this._instance + } + + /** + * Check if instance exists + */ + public static hasInstance(): boolean { + return this._instance !== null + } + + /** + * Reset the instance (for testing) + */ + public static resetInstance(): void { + this._instance = null + } +} diff --git a/src/services/mdm/__tests__/MdmService.spec.ts b/src/services/mdm/__tests__/MdmService.spec.ts new file mode 100644 index 0000000000..c6b4365f57 --- /dev/null +++ b/src/services/mdm/__tests__/MdmService.spec.ts @@ -0,0 +1,420 @@ +import * as path from "path" +import { describe, it, expect, beforeEach, afterEach, vi } from "vitest" + +// Mock dependencies +vi.mock("fs", () => ({ + existsSync: vi.fn(), + readFileSync: vi.fn(), +})) + +vi.mock("os", () => ({ + platform: vi.fn(), +})) + +vi.mock("@roo-code/cloud", () => ({ + CloudService: { + hasInstance: vi.fn(), + instance: { + hasActiveSession: vi.fn(), + getOrganizationId: vi.fn(), + }, + }, +})) + +vi.mock("vscode", () => ({ + workspace: { + getConfiguration: vi.fn(), + }, + ConfigurationTarget: { + Global: 1, + }, +})) + +vi.mock("../../../shared/package", () => ({ + Package: { + publisher: "roo-code", + name: "roo-cline", + version: "1.0.0", + outputChannel: "Roo-Code", + sha: undefined, + }, +})) + +import * as fs from "fs" +import * as os from "os" +import * as vscode from "vscode" +import { MdmService } from "../MdmService" +import { CloudService } from "@roo-code/cloud" + +const mockFs = fs as any +const mockOs = os as any +const mockCloudService = CloudService as any +const mockVscode = vscode as any + +describe("MdmService", () => { + let originalPlatform: string + + beforeEach(() => { + // Reset singleton + MdmService.resetInstance() + + // Store original platform + originalPlatform = process.platform + + // Set default platform for tests + mockOs.platform.mockReturnValue("darwin") + + // Setup VSCode mocks + const mockConfig = { + get: vi.fn().mockReturnValue(false), + update: vi.fn().mockResolvedValue(undefined), + } + mockVscode.workspace.getConfiguration.mockReturnValue(mockConfig) + + // Reset mocks + vi.clearAllMocks() + }) + + afterEach(() => { + // Restore original platform + Object.defineProperty(process, "platform", { + value: originalPlatform, + }) + }) + + describe("initialization", () => { + it("should create instance successfully", async () => { + mockFs.existsSync.mockReturnValue(false) + + const service = await MdmService.createInstance() + expect(service).toBeInstanceOf(MdmService) + }) + + it("should load MDM config if file exists", async () => { + const mockConfig = { + requireCloudAuth: true, + organizationId: "test-org-123", + } + + mockFs.existsSync.mockReturnValue(true) + mockFs.readFileSync.mockReturnValue(JSON.stringify(mockConfig)) + + const service = await MdmService.createInstance() + + expect(service.requiresCloudAuth()).toBe(true) + expect(service.getRequiredOrganizationId()).toBe("test-org-123") + }) + + it("should handle missing MDM config file gracefully", async () => { + mockFs.existsSync.mockReturnValue(false) + + const service = await MdmService.createInstance() + + expect(service.requiresCloudAuth()).toBe(false) + expect(service.getRequiredOrganizationId()).toBeUndefined() + }) + + it("should handle invalid JSON gracefully", async () => { + mockFs.existsSync.mockReturnValue(true) + mockFs.readFileSync.mockReturnValue("invalid json") + + const service = await MdmService.createInstance() + + expect(service.requiresCloudAuth()).toBe(false) + }) + }) + + describe("platform-specific config paths", () => { + let originalNodeEnv: string | undefined + + beforeEach(() => { + originalNodeEnv = process.env.NODE_ENV + }) + + afterEach(() => { + if (originalNodeEnv !== undefined) { + process.env.NODE_ENV = originalNodeEnv + } else { + delete process.env.NODE_ENV + } + }) + + it("should use correct path for Windows in production", async () => { + mockOs.platform.mockReturnValue("win32") + process.env.PROGRAMDATA = "C:\\ProgramData" + process.env.NODE_ENV = "production" + + mockFs.existsSync.mockReturnValue(false) + + await MdmService.createInstance() + + expect(mockFs.existsSync).toHaveBeenCalledWith(path.join("C:\\ProgramData", "RooCode", "mcp.json")) + }) + + it("should use correct path for Windows in development", async () => { + mockOs.platform.mockReturnValue("win32") + process.env.PROGRAMDATA = "C:\\ProgramData" + process.env.NODE_ENV = "development" + + mockFs.existsSync.mockReturnValue(false) + + await MdmService.createInstance() + + expect(mockFs.existsSync).toHaveBeenCalledWith(path.join("C:\\ProgramData", "RooCode", "mcp.dev.json")) + }) + + it("should use correct path for macOS in production", async () => { + mockOs.platform.mockReturnValue("darwin") + process.env.NODE_ENV = "production" + + mockFs.existsSync.mockReturnValue(false) + + await MdmService.createInstance() + + expect(mockFs.existsSync).toHaveBeenCalledWith("/Library/Application Support/RooCode/mcp.json") + }) + + it("should use correct path for macOS in development", async () => { + mockOs.platform.mockReturnValue("darwin") + process.env.NODE_ENV = "development" + + mockFs.existsSync.mockReturnValue(false) + + await MdmService.createInstance() + + expect(mockFs.existsSync).toHaveBeenCalledWith("/Library/Application Support/RooCode/mcp.dev.json") + }) + + it("should use correct path for Linux in production", async () => { + mockOs.platform.mockReturnValue("linux") + process.env.NODE_ENV = "production" + + mockFs.existsSync.mockReturnValue(false) + + await MdmService.createInstance() + + expect(mockFs.existsSync).toHaveBeenCalledWith("/etc/roo-code/mcp.json") + }) + + it("should use correct path for Linux in development", async () => { + mockOs.platform.mockReturnValue("linux") + process.env.NODE_ENV = "development" + + mockFs.existsSync.mockReturnValue(false) + + await MdmService.createInstance() + + expect(mockFs.existsSync).toHaveBeenCalledWith("/etc/roo-code/mcp.dev.json") + }) + + it("should default to dev config when NODE_ENV is not set", async () => { + mockOs.platform.mockReturnValue("darwin") + delete process.env.NODE_ENV + + mockFs.existsSync.mockReturnValue(false) + + await MdmService.createInstance() + + expect(mockFs.existsSync).toHaveBeenCalledWith("/Library/Application Support/RooCode/mcp.dev.json") + }) + }) + + describe("compliance checking", () => { + it("should be compliant when no MDM policy exists", async () => { + mockFs.existsSync.mockReturnValue(false) + + const service = await MdmService.createInstance() + const compliance = service.isCompliant() + + expect(compliance.compliant).toBe(true) + }) + + it("should be compliant when authenticated and no org requirement", async () => { + const mockConfig = { requireCloudAuth: true } + mockFs.existsSync.mockReturnValue(true) + mockFs.readFileSync.mockReturnValue(JSON.stringify(mockConfig)) + + mockCloudService.hasInstance.mockReturnValue(true) + mockCloudService.instance.hasActiveSession.mockReturnValue(true) + + const service = await MdmService.createInstance() + const compliance = service.isCompliant() + + expect(compliance.compliant).toBe(true) + }) + + it("should be non-compliant when not authenticated", async () => { + const mockConfig = { requireCloudAuth: true } + mockFs.existsSync.mockReturnValue(true) + mockFs.readFileSync.mockReturnValue(JSON.stringify(mockConfig)) + + mockCloudService.hasInstance.mockReturnValue(false) + + const service = await MdmService.createInstance() + const compliance = service.isCompliant() + + expect(compliance.compliant).toBe(false) + if (!compliance.compliant) { + expect(compliance.reason).toContain("requires Roo Code Cloud authentication") + } + }) + + it("should be non-compliant when wrong organization", async () => { + const mockConfig = { + requireCloudAuth: true, + organizationId: "required-org-123", + } + mockFs.existsSync.mockReturnValue(true) + mockFs.readFileSync.mockReturnValue(JSON.stringify(mockConfig)) + + mockCloudService.hasInstance.mockReturnValue(true) + mockCloudService.instance.hasActiveSession.mockReturnValue(true) + mockCloudService.instance.getOrganizationId.mockReturnValue("different-org-456") + + const service = await MdmService.createInstance() + const compliance = service.isCompliant() + + expect(compliance.compliant).toBe(false) + if (!compliance.compliant) { + expect(compliance.reason).toContain("organization's Roo Code Cloud account") + } + }) + + it("should be compliant when correct organization", async () => { + const mockConfig = { + requireCloudAuth: true, + organizationId: "correct-org-123", + } + mockFs.existsSync.mockReturnValue(true) + mockFs.readFileSync.mockReturnValue(JSON.stringify(mockConfig)) + + mockCloudService.hasInstance.mockReturnValue(true) + mockCloudService.instance.hasActiveSession.mockReturnValue(true) + mockCloudService.instance.getOrganizationId.mockReturnValue("correct-org-123") + + const service = await MdmService.createInstance() + const compliance = service.isCompliant() + + expect(compliance.compliant).toBe(true) + }) + }) + + describe("cloud enablement", () => { + it("should enable Roo Code Cloud when MDM config is present and setting is disabled", async () => { + const mockConfig = { + requireCloudAuth: true, + organizationId: "test-org-123", + } + + mockFs.existsSync.mockReturnValue(true) + mockFs.readFileSync.mockReturnValue(JSON.stringify(mockConfig)) + + const mockVsCodeConfig = { + get: vi.fn().mockReturnValue(false), // rooCodeCloudEnabled is false + update: vi.fn().mockResolvedValue(undefined), + } + mockVscode.workspace.getConfiguration.mockReturnValue(mockVsCodeConfig) + + await MdmService.createInstance() + + expect(mockVscode.workspace.getConfiguration).toHaveBeenCalledWith("roo-cline") + expect(mockVsCodeConfig.get).toHaveBeenCalledWith("rooCodeCloudEnabled", false) + expect(mockVsCodeConfig.update).toHaveBeenCalledWith("rooCodeCloudEnabled", true, 1) // ConfigurationTarget.Global + }) + + it("should not update setting when Roo Code Cloud is already enabled", async () => { + const mockConfig = { + requireCloudAuth: true, + organizationId: "test-org-123", + } + + mockFs.existsSync.mockReturnValue(true) + mockFs.readFileSync.mockReturnValue(JSON.stringify(mockConfig)) + + const mockVsCodeConfig = { + get: vi.fn().mockReturnValue(true), // rooCodeCloudEnabled is already true + update: vi.fn().mockResolvedValue(undefined), + } + mockVscode.workspace.getConfiguration.mockReturnValue(mockVsCodeConfig) + + await MdmService.createInstance() + + expect(mockVsCodeConfig.get).toHaveBeenCalledWith("rooCodeCloudEnabled", false) + expect(mockVsCodeConfig.update).not.toHaveBeenCalled() + }) + + it("should enable cloud even when requireCloudAuth is false", async () => { + const mockConfig = { + requireCloudAuth: false, // Cloud auth not required, but config file exists + } + + mockFs.existsSync.mockReturnValue(true) + mockFs.readFileSync.mockReturnValue(JSON.stringify(mockConfig)) + + const mockVsCodeConfig = { + get: vi.fn().mockReturnValue(false), + update: vi.fn().mockResolvedValue(undefined), + } + mockVscode.workspace.getConfiguration.mockReturnValue(mockVsCodeConfig) + + await MdmService.createInstance() + + expect(mockVsCodeConfig.update).toHaveBeenCalledWith("rooCodeCloudEnabled", true, 1) + }) + + it("should not enable cloud when no MDM config exists", async () => { + mockFs.existsSync.mockReturnValue(false) + + const mockVsCodeConfig = { + get: vi.fn().mockReturnValue(false), + update: vi.fn().mockResolvedValue(undefined), + } + mockVscode.workspace.getConfiguration.mockReturnValue(mockVsCodeConfig) + + await MdmService.createInstance() + + expect(mockVsCodeConfig.update).not.toHaveBeenCalled() + }) + + it("should handle VSCode configuration errors gracefully", async () => { + const mockConfig = { + requireCloudAuth: true, + } + + mockFs.existsSync.mockReturnValue(true) + mockFs.readFileSync.mockReturnValue(JSON.stringify(mockConfig)) + + const mockVsCodeConfig = { + get: vi.fn().mockReturnValue(false), + update: vi.fn().mockRejectedValue(new Error("Configuration update failed")), + } + mockVscode.workspace.getConfiguration.mockReturnValue(mockVsCodeConfig) + + // Should not throw + await expect(MdmService.createInstance()).resolves.toBeInstanceOf(MdmService) + }) + }) + + describe("singleton pattern", () => { + it("should throw error when accessing instance before creation", () => { + expect(() => MdmService.getInstance()).toThrow("MdmService not initialized") + }) + + it("should throw error when creating instance twice", async () => { + mockFs.existsSync.mockReturnValue(false) + + await MdmService.createInstance() + + await expect(MdmService.createInstance()).rejects.toThrow("instance already exists") + }) + + it("should return same instance", async () => { + mockFs.existsSync.mockReturnValue(false) + + const service1 = await MdmService.createInstance() + const service2 = MdmService.getInstance() + + expect(service1).toBe(service2) + }) + }) +})