Explorar o código

Support config policies that require use of cloud (#4762)

* Support config policies that require use of cloud

* Switch to vitest
Matt Rubens hai 6 meses
pai
achega
8dd99a0254
Modificáronse 42 ficheiros con 890 adicións e 4 borrados
  1. 6 1
      src/core/tools/newTaskTool.ts
  2. 32 0
      src/core/webview/ClineProvider.ts
  3. 5 1
      src/extension.ts
  4. 6 2
      src/extension/api.ts
  5. 7 0
      src/i18n/locales/ca/common.json
  6. 5 0
      src/i18n/locales/ca/tools.json
  7. 7 0
      src/i18n/locales/de/common.json
  8. 5 0
      src/i18n/locales/de/tools.json
  9. 7 0
      src/i18n/locales/en/common.json
  10. 5 0
      src/i18n/locales/en/tools.json
  11. 7 0
      src/i18n/locales/es/common.json
  12. 5 0
      src/i18n/locales/es/tools.json
  13. 7 0
      src/i18n/locales/fr/common.json
  14. 5 0
      src/i18n/locales/fr/tools.json
  15. 7 0
      src/i18n/locales/hi/common.json
  16. 5 0
      src/i18n/locales/hi/tools.json
  17. 7 0
      src/i18n/locales/id/common.json
  18. 5 0
      src/i18n/locales/id/tools.json
  19. 7 0
      src/i18n/locales/it/common.json
  20. 5 0
      src/i18n/locales/it/tools.json
  21. 7 0
      src/i18n/locales/ja/common.json
  22. 5 0
      src/i18n/locales/ja/tools.json
  23. 7 0
      src/i18n/locales/ko/common.json
  24. 5 0
      src/i18n/locales/ko/tools.json
  25. 7 0
      src/i18n/locales/nl/common.json
  26. 5 0
      src/i18n/locales/nl/tools.json
  27. 7 0
      src/i18n/locales/pl/common.json
  28. 5 0
      src/i18n/locales/pl/tools.json
  29. 7 0
      src/i18n/locales/pt-BR/common.json
  30. 5 0
      src/i18n/locales/pt-BR/tools.json
  31. 7 0
      src/i18n/locales/ru/common.json
  32. 5 0
      src/i18n/locales/ru/tools.json
  33. 7 0
      src/i18n/locales/tr/common.json
  34. 5 0
      src/i18n/locales/tr/tools.json
  35. 7 0
      src/i18n/locales/vi/common.json
  36. 5 0
      src/i18n/locales/vi/tools.json
  37. 7 0
      src/i18n/locales/zh-CN/common.json
  38. 5 0
      src/i18n/locales/zh-CN/tools.json
  39. 7 0
      src/i18n/locales/zh-TW/common.json
  40. 5 0
      src/i18n/locales/zh-TW/tools.json
  41. 205 0
      src/services/mdm/MdmService.ts
  42. 420 0
      src/services/mdm/__tests__/MdmService.spec.ts

+ 6 - 1
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}`)

+ 32 - 0
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

+ 5 - 1
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) {

+ 6 - 2
src/extension/api.ts

@@ -132,11 +132,15 @@ export class API extends EventEmitter<RooCodeEvents> 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<void> {

+ 7 - 0
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ó."
+		}
 	}
 }

+ 5 - 0
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."
+		}
 	}
 }

+ 7 - 0
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."
+		}
 	}
 }

+ 5 - 0
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."
+		}
 	}
 }

+ 7 - 0
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."
+		}
 	}
 }

+ 5 - 0
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."
+		}
 	}
 }

+ 7 - 0
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."
+		}
 	}
 }

+ 5 - 0
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."
+		}
 	}
 }

+ 7 - 0
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."
+		}
 	}
 }

+ 5 - 0
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."
+		}
 	}
 }

+ 7 - 0
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": "संगठन प्रमाणीकरण सत्यापित करने में असमर्थ।"
+		}
 	}
 }

+ 5 - 0
src/i18n/locales/hi/tools.json

@@ -7,5 +7,10 @@
 	"toolRepetitionLimitReached": "Roo एक लूप में फंसा हुआ लगता है, बार-बार एक ही क्रिया ({{toolName}}) को दोहरा रहा है। यह उसकी वर्तमान रणनीति में किसी समस्या का संकेत हो सकता है। कार्य को पुनः परिभाषित करने, अधिक विशिष्ट निर्देश देने, या उसे एक अलग दृष्टिकोण की ओर मार्गदर्शित करने पर विचार करें।",
 	"codebaseSearch": {
 		"approval": "कोडबेस में '{{query}}' खोज रहा है..."
+	},
+	"newTask": {
+		"errors": {
+			"policy_restriction": "नीति प्रतिबंधों के कारण नया कार्य बनाने में विफल।"
+		}
 	}
 }

+ 7 - 0
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."
+		}
 	}
 }

+ 5 - 0
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."
+		}
 	}
 }

+ 7 - 0
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."
+		}
 	}
 }

+ 5 - 0
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."
+		}
 	}
 }

+ 7 - 0
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": "組織認証の確認ができませんでした。"
+		}
 	}
 }

+ 5 - 0
src/i18n/locales/ja/tools.json

@@ -7,5 +7,10 @@
 	"toolRepetitionLimitReached": "Rooが同じ操作({{toolName}})を繰り返し試みるループに陥っているようです。これは現在の方法に問題がある可能性を示しています。タスクの言い換え、より具体的な指示の提供、または別のアプローチへの誘導を検討してください。",
 	"codebaseSearch": {
 		"approval": "コードベースで '{{query}}' を検索中..."
+	},
+	"newTask": {
+		"errors": {
+			"policy_restriction": "ポリシー制限により新しいタスクを作成できませんでした。"
+		}
 	}
 }

+ 7 - 0
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": "조직 인증을 확인할 수 없습니다."
+		}
 	}
 }

+ 5 - 0
src/i18n/locales/ko/tools.json

@@ -7,5 +7,10 @@
 	"toolRepetitionLimitReached": "Roo가 같은 동작({{toolName}})을 반복적으로 시도하면서 루프에 갇힌 것 같습니다. 이는 현재 전략에 문제가 있을 수 있음을 나타냅니다. 작업을 다시 표현하거나, 더 구체적인 지침을 제공하거나, 다른 접근 방식으로 안내해 보세요.",
 	"codebaseSearch": {
 		"approval": "코드베이스에서 '{{query}}' 검색 중..."
+	},
+	"newTask": {
+		"errors": {
+			"policy_restriction": "정책 제한으로 인해 새 작업을 생성하지 못했습니다."
+		}
 	}
 }

+ 7 - 0
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."
+		}
 	}
 }

+ 5 - 0
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."
+		}
 	}
 }

+ 7 - 0
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."
+		}
 	}
 }

+ 5 - 0
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."
+		}
 	}
 }

+ 7 - 0
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."
+		}
 	}
 }

+ 5 - 0
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."
+		}
 	}
 }

+ 7 - 0
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": "Не удается проверить аутентификацию организации."
+		}
 	}
 }

+ 5 - 0
src/i18n/locales/ru/tools.json

@@ -7,5 +7,10 @@
 	"toolRepetitionLimitReached": "Похоже, что Roo застрял в цикле, многократно пытаясь выполнить одно и то же действие ({{toolName}}). Это может указывать на проблему с его текущей стратегией. Попробуйте переформулировать задачу, предоставить более конкретные инструкции или направить его к другому подходу.",
 	"codebaseSearch": {
 		"approval": "Поиск '{{query}}' в кодовой базе..."
+	},
+	"newTask": {
+		"errors": {
+			"policy_restriction": "Не удалось создать новую задачу из-за ограничений политики."
+		}
 	}
 }

+ 7 - 0
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."
+		}
 	}
 }

+ 5 - 0
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ı."
+		}
 	}
 }

+ 7 - 0
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."
+		}
 	}
 }

+ 5 - 0
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."
+		}
 	}
 }

+ 7 - 0
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": "无法验证组织身份验证。"
+		}
 	}
 }

+ 5 - 0
src/i18n/locales/zh-CN/tools.json

@@ -7,5 +7,10 @@
 	"toolRepetitionLimitReached": "Roo 似乎陷入循环,反复尝试同一操作 ({{toolName}})。这可能表明当前策略存在问题。请考虑重新描述任务、提供更具体的指示或引导其尝试不同的方法。",
 	"codebaseSearch": {
 		"approval": "正在搜索代码库中的 '{{query}}'..."
+	},
+	"newTask": {
+		"errors": {
+			"policy_restriction": "由于策略限制,无法创建新任务。"
+		}
 	}
 }

+ 7 - 0
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": "無法驗證組織身份驗證。"
+		}
 	}
 }

+ 5 - 0
src/i18n/locales/zh-TW/tools.json

@@ -7,5 +7,10 @@
 	"toolRepetitionLimitReached": "Roo 似乎陷入循環,反覆嘗試同一操作 ({{toolName}})。這可能表明目前策略存在問題。請考慮重新描述工作、提供更具體的指示或引導其嘗試不同的方法。",
 	"codebaseSearch": {
 		"approval": "正在搜尋程式碼庫中的「{{query}}」..."
+	},
+	"newTask": {
+		"errors": {
+			"policy_restriction": "由於政策限制,無法建立新工作。"
+		}
 	}
 }

+ 205 - 0
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<typeof mdmConfigSchema>
+
+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<void> {
+		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<void> {
+		try {
+			const config = vscode.workspace.getConfiguration(Package.name)
+			const currentValue = config.get<boolean>("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<MdmConfig | null> {
+		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<MdmService> {
+		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
+	}
+}

+ 420 - 0
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)
+		})
+	})
+})