diff --git a/package.json b/package.json index d52fc1f..d0b4d65 100644 --- a/package.json +++ b/package.json @@ -39,7 +39,7 @@ "icons:providers": "node script/copy-provider-icons.mjs", "smoke:shared": "esbuild test/shared.ts --bundle --platform=node --format=cjs --packages=external --outfile=test/.out/shared.cjs && node test/.out/shared.cjs", "smoke:app": "esbuild test/smoke.ts --bundle --platform=node --format=cjs --packages=external --outfile=test/.out/smoke.cjs && electron test/.out/smoke.cjs", - "smoke": "npm run smoke:shared && npm run smoke:i18n && npm run smoke:store && npm run smoke:multirepo && npm run smoke:live && npm run smoke:cookies && npm run smoke:app", + "smoke": "npm run smoke:shared && npm run smoke:i18n && npm run smoke:store && npm run smoke:multirepo && npm run smoke:live && npm run smoke:cookies && npm run smoke:kernel && npm run smoke:app", "smoke:multirepo": "esbuild test/multirepo.ts --bundle --platform=node --format=cjs --packages=external --outfile=test/.out/multirepo.cjs && node test/.out/multirepo.cjs", "smoke:live": "esbuild test/live-multirepo.ts --bundle --platform=node --format=cjs --packages=external --outfile=test/.out/live.cjs && electron test/.out/live.cjs", "smoke:cliproxy": "esbuild test/cliproxy.ts --bundle --platform=node --format=cjs --packages=external --outfile=test/.out/cliproxy.cjs && electron test/.out/cliproxy.cjs", @@ -47,6 +47,7 @@ "smoke:store": "node test/store-guard.mjs", "smoke:cookies": "esbuild test/cookies.ts --bundle --platform=node --format=cjs --packages=external --outfile=test/.out/cookies.cjs && electron test/.out/cookies.cjs", "smoke:i18n": "esbuild test/i18n.ts --bundle --platform=node --format=cjs --outfile=test/.out/i18n.cjs && node test/.out/i18n.cjs", + "smoke:kernel": "esbuild test/kernel.ts --bundle --platform=node --format=cjs --outfile=test/.out/kernel.cjs && node test/.out/kernel.cjs", "i18n:translate": "node script/i18n-translate.mjs", "canvas": "vite --config test/canvas/vite.config.mjs", "smoke:canvas": "electron test/canvas/smoke.cjs", diff --git a/src/main/index.ts b/src/main/index.ts index 06d0ee2..6aca141 100644 --- a/src/main/index.ts +++ b/src/main/index.ts @@ -4,6 +4,7 @@ import { electronApp, optimizer, is } from '@electron-toolkit/utils' import icon from '../../resources/icon.png?asset' import macDockIcon from '../../resources/icon-mac.png?asset' import { registerIpc } from './ipc' +import { registerKernelIpc } from './ipc/kernel' import { getDb } from './db/database' import { startLoopScheduler } from './services/loops' import { listModels } from './services/models' @@ -30,9 +31,11 @@ import { import { resolveThemeById } from './services/themes' import * as repo from './db/repo' +let mainWindow: BrowserWindow | null = null + function createWindow(): BrowserWindow { const isMac = process.platform === 'darwin' - const mainWindow = new BrowserWindow({ + const window = new BrowserWindow({ width: 1100, height: 720, minWidth: 760, @@ -54,30 +57,39 @@ function createWindow(): BrowserWindow { } }) - mainWindow.on('ready-to-show', () => { + mainWindow = window + window.on('closed', () => { + if (mainWindow === window) mainWindow = null + }) + + window.on('ready-to-show', () => { // Repaint the native window controls from the active theme before the // window is first shown. The constructor can only reach built-in themes // synchronously; this covers a user theme, whose file has to be read. void resolveThemeById(repo.getSettings().activeThemeId, chromePlatform()) - .then((theme) => applyWindowChrome(mainWindow, theme)) + .then((theme) => applyWindowChrome(window, theme)) .catch(() => undefined) - mainWindow.show() + window.show() }) // Open external links in the user's browser instead of a new Electron window. - mainWindow.webContents.setWindowOpenHandler((details) => { + window.webContents.setWindowOpenHandler((details) => { shell.openExternal(details.url) return { action: 'deny' } }) + window.webContents.on('will-navigate', (event, url) => { + const currentUrl = window.webContents.getURL() + if (currentUrl && url !== currentUrl) event.preventDefault() + }) // Load the Vite dev server in development, or the built HTML in production. if (is.dev && process.env['ELECTRON_RENDERER_URL']) { - mainWindow.loadURL(process.env['ELECTRON_RENDERER_URL']) + window.loadURL(process.env['ELECTRON_RENDERER_URL']) } else { - mainWindow.loadFile(join(__dirname, '../renderer/index.html')) + window.loadFile(join(__dirname, '../renderer/index.html')) } - return mainWindow + return window } /** @@ -121,6 +133,7 @@ app.whenReady().then(() => { // Open the database (runs migrations) and wire up IPC before the first window. getDb() registerIpc() + registerKernelIpc(() => mainWindow) // Anonymous usage tracking (opt-out in Settings). Deliberately after the DB // and IPC are up so nothing here can delay the first window, and it owns its // own storage - a failure in it can't touch either. @@ -133,8 +146,7 @@ app.whenReady().then(() => { // backfilled rows can be priced (else they'd all cost $0). Best-effort + async. void warmCatalogThenBackfill() - const mainWindow = createWindow() - initAutoUpdater(mainWindow) + initAutoUpdater(createWindow()) app.on('activate', () => { if (BrowserWindow.getAllWindows().length === 0) createWindow() diff --git a/src/main/ipc/kernel.ts b/src/main/ipc/kernel.ts new file mode 100644 index 0000000..9c7a994 --- /dev/null +++ b/src/main/ipc/kernel.ts @@ -0,0 +1,72 @@ +import { dialog, ipcMain, type BrowserWindow, type IpcMainInvokeEvent } from 'electron' +import { CHANNELS } from '../../shared/ipc' +import { + getKernelStatus, + installKernelTools, + setKernelAgentAccess, + startKernelDriver, + toggleTestSigning, + uninstallKernelTools +} from '../services/kernel' + +function requireMainWindow( + event: IpcMainInvokeEvent, + getMainWindow: () => BrowserWindow | null +): BrowserWindow { + const mainWindow = getMainWindow() + if ( + !mainWindow || + mainWindow.isDestroyed() || + event.sender !== mainWindow.webContents || + event.senderFrame !== mainWindow.webContents.mainFrame + ) { + throw new Error('Kernel Tools requests are only accepted from the main Roxy window.') + } + return mainWindow +} + +export function registerKernelIpc(getMainWindow: () => BrowserWindow | null): void { + ipcMain.handle(CHANNELS.kernelStatus, (event) => { + requireMainWindow(event, getMainWindow) + return getKernelStatus() + }) + ipcMain.handle(CHANNELS.kernelInstall, (event) => { + requireMainWindow(event, getMainWindow) + return installKernelTools() + }) + ipcMain.handle(CHANNELS.kernelStart, (event) => { + requireMainWindow(event, getMainWindow) + return startKernelDriver() + }) + ipcMain.handle(CHANNELS.kernelSetAgentAccess, async (event, enable: boolean) => { + const mainWindow = requireMainWindow(event, getMainWindow) + if (typeof enable !== 'boolean') throw new TypeError('enable must be a boolean.') + if (enable) { + const { response } = await dialog.showMessageBox(mainWindow, { + type: 'warning', + title: 'Enable Kernel Tools agent access?', + message: 'This gives agents elevated access to protected system resources.', + detail: 'Only continue in an isolated test environment with no sensitive data.', + buttons: ['Cancel', 'Enable agent access'], + defaultId: 0, + cancelId: 0, + noLink: true + }) + if (response !== 1) { + return { ok: false, error: 'Agent access was not enabled.', steps: [] } + } + } + return setKernelAgentAccess(enable) + }) + ipcMain.handle(CHANNELS.kernelUninstall, (event, disableSigning: boolean) => { + requireMainWindow(event, getMainWindow) + if (typeof disableSigning !== 'boolean') + throw new TypeError('disableSigning must be a boolean.') + return uninstallKernelTools(disableSigning) + }) + ipcMain.handle(CHANNELS.kernelToggleTestSigning, (event, enable: boolean) => { + requireMainWindow(event, getMainWindow) + if (typeof enable !== 'boolean') throw new TypeError('enable must be a boolean.') + return toggleTestSigning(enable) + }) +} diff --git a/src/main/services/kernel.ts b/src/main/services/kernel.ts new file mode 100644 index 0000000..2fb61f2 --- /dev/null +++ b/src/main/services/kernel.ts @@ -0,0 +1,607 @@ +import { createHash, randomUUID } from 'node:crypto' +import { execFile } from 'node:child_process' +import { existsSync, mkdirSync, readFileSync } from 'node:fs' +import { open, rm } from 'node:fs/promises' +import path from 'node:path' +import { promisify } from 'node:util' +import { app, net as electronNet } from 'electron' +import type { KernelInstallResult, KernelStatus } from '../../shared/kernel' +import { deleteMcpServer, listMcpServers, upsertMcpServer } from '../db/repo' +import { disposeConnection } from './mcp' + +const execFileAsync = promisify(execFile) + +const RELEASE_TAG = 'v1.0.2' +const RELEASE_ASSET = 'kernel-tools-windows-x64.zip' +const RELEASE_URL = `https://github.com/roxy-gg/kernel-tools/releases/download/${RELEASE_TAG}/${RELEASE_ASSET}` +const RELEASE_SHA256 = 'c3e0b19f69c13d4bb4ffb55987f13b3f4e6232a92245645e98acccf5cee2c4f5' +const RELEASE_COMMIT = '62126a86e335caee1a04fdfc8db08986638112d7' +const SIGNER_THUMBPRINT = 'f1b5ab3fa912b6bc5ae30301dd36b64708f293cf' +const BRIDGE_SHA256 = '48e24b722c77394416801bc1c6a19dcff109561942fe03f6590056b2b730563a' +const MAX_RELEASE_BYTES = 5 * 1024 * 1024 +const SERVICE_NAME = 'RoxyKernelToolsAIBridge' +const MCP_SERVER_ID = 'roxy-kernel-tools' +const MCP_SERVER_IDS = [MCP_SERVER_ID, 'kernel'] as const + +const INSTALL_DIR = path.join( + process.env.LOCALAPPDATA ?? path.join(process.env.USERPROFILE ?? '', 'AppData', 'Local'), + 'roxy', + 'kernel-tools' +) +const ARCHIVE_PATH = path.join(INSTALL_DIR, RELEASE_ASSET) +const PRIVILEGED_INSTALL_DIR = path.join( + process.env.ProgramFiles ?? 'C:\\Program Files', + 'Roxy', + 'KernelTools' +) +const DRIVER_PATH = path.join(PRIVILEGED_INSTALL_DIR, 'aibridge.sys') +const BRIDGE_PATH = path.join(PRIVILEGED_INSTALL_DIR, 'roxy-kernel-bridge.exe') +const INSTALL_MANIFEST_PATH = path.join(PRIVILEGED_INSTALL_DIR, 'install-manifest.json') +let mutationInProgress = false + +function isWindows(): boolean { + return process.platform === 'win32' +} + +function isSupportedPlatform(): boolean { + return isWindows() && process.arch === 'x64' +} + +async function runMutation( + operation: () => Promise +): Promise { + if (mutationInProgress) { + return { ok: false, error: 'Another Kernel Tools operation is already in progress.', steps: [] } + } + mutationInProgress = true + try { + return await operation() + } finally { + mutationInProgress = false + } +} + +function errorMessage(error: unknown): string { + if (!(error instanceof Error)) return String(error) + const output = error as Error & { stdout?: string; stderr?: string } + const details = [output.stderr, output.stdout] + .map((value) => value?.trim()) + .filter(Boolean) + .join('\n') + return details ? `${error.message}\n${details}` : error.message +} + +function powershellLiteral(value: string): string { + return `'${value.replace(/'/g, "''")}'` +} + +function sha256(file: string): string { + return createHash('sha256').update(readFileSync(file)).digest('hex') +} + +function systemExecutable(name: string): string { + const windowsDirectory = process.env.SystemRoot ?? 'C:\\Windows' + return path.join(windowsDirectory, 'System32', name) +} + +async function run( + file: string, + args: string[], + timeout = 30_000 +): Promise<{ stdout: string; stderr: string }> { + return execFileAsync(file, args, { timeout, windowsHide: true, maxBuffer: 10 * 1024 * 1024 }) +} + +/** Run one executable through the standard Windows UAC consent prompt. */ +async function runElevated(file: string, args: string[]): Promise { + const argumentList = args.map(powershellLiteral).join(', ') + const resultPath = path.join(INSTALL_DIR, `elevated-${randomUUID()}.txt`) + const command = [ + `$resultPath = ${powershellLiteral(resultPath)}`, + 'try {', + ` $process = Start-Process -FilePath ${powershellLiteral(file)} -ArgumentList @(${argumentList}) -Verb RunAs -Wait -PassThru`, + " $result = if ($null -eq $process) { '1`nThe elevated process did not start.' } elseif ($process.ExitCode -eq 0) { '0' } else { \"$($process.ExitCode)`nThe elevated command failed.\" }", + '} catch { $result = "1`n$($_.Exception.Message)" }', + 'Set-Content -LiteralPath $resultPath -Value $result -Encoding UTF8' + ].join('; ') + + mkdirSync(INSTALL_DIR, { recursive: true }) + try { + await run( + systemExecutable('WindowsPowerShell\\v1.0\\powershell.exe'), + ['-NoProfile', '-NonInteractive', '-ExecutionPolicy', 'Bypass', '-Command', command], + 0 + ) + const [exitCode, ...details] = readFileSync(resultPath, 'utf8') + .replace(/^\uFEFF/, '') + .trim() + .split(/\r?\n/) + if (exitCode !== '0') { + throw new Error(details.join('\n') || `Elevated command exited with code ${exitCode}.`) + } + } finally { + await rm(resultPath, { force: true }) + } +} +export async function checkTestSigning(): Promise { + if (!isWindows()) return false + try { + const { stdout } = await run( + systemExecutable('reg.exe'), + ['query', 'HKLM\\SYSTEM\\CurrentControlSet\\Control', '/v', 'SystemStartOptions'], + 5_000 + ) + return /\bTESTSIGNING\b/i.test(stdout) + } catch { + return false + } +} + +async function setTestSigning(enable: boolean): Promise { + await runElevated(systemExecutable('bcdedit.exe'), ['/set', 'testsigning', enable ? 'on' : 'off']) +} + +function installedDriverScript(startDriver: boolean): string { + return [ + "$ErrorActionPreference = 'Stop'", + `$archivePath = ${powershellLiteral(ARCHIVE_PATH)}`, + `$expectedArchiveHash = ${powershellLiteral(RELEASE_SHA256)}`, + `$expectedVersion = ${powershellLiteral(RELEASE_TAG)}`, + `$expectedCommit = ${powershellLiteral(RELEASE_COMMIT)}`, + `$expectedSignerThumbprint = ${powershellLiteral(SIGNER_THUMBPRINT)}`, + `$expectedBridgeHash = ${powershellLiteral(BRIDGE_SHA256)}`, + `$installDirectory = ${powershellLiteral(PRIVILEGED_INSTALL_DIR)}`, + `$driverTarget = ${powershellLiteral(DRIVER_PATH)}`, + `$bridgeTarget = ${powershellLiteral(BRIDGE_PATH)}`, + `$installManifestPath = ${powershellLiteral(INSTALL_MANIFEST_PATH)}`, + `$stagingDirectory = Join-Path $installDirectory ('.staging-' + [guid]::NewGuid().ToString('N'))`, + 'try {', + ' New-Item -ItemType Directory -Force -Path $installDirectory | Out-Null', + ' $archive = [IO.File]::Open($archivePath, [IO.FileMode]::Open, [IO.FileAccess]::Read, [IO.FileShare]::None)', + ' try {', + ' $archiveHash = [Security.Cryptography.SHA256]::Create().ComputeHash($archive)', + " $archiveHashHex = (($archiveHash | ForEach-Object { $_.ToString('x2') }) -join '')", + " if ($archiveHashHex -ne $expectedArchiveHash) { throw 'Release archive hash mismatch.' }", + ' $archive.Position = 0', + ' New-Item -ItemType Directory -Force -Path $stagingDirectory | Out-Null', + " Add-Type -AssemblyName 'System.IO.Compression'", + " Add-Type -AssemblyName 'System.IO.Compression.FileSystem'", + ' $zip = New-Object IO.Compression.ZipArchive($archive, [IO.Compression.ZipArchiveMode]::Read, $true)', + ' try {', + " $allowedNames = @('aibridge-test.cer', 'aibridge.sys', 'manifest.json', 'roxy-kernel-bridge.exe')", + " if ($zip.Entries.Count -ne $allowedNames.Count -or @($zip.Entries | Where-Object { $_.FullName -notin $allowedNames }).Count -ne 0) { throw 'Unexpected release archive contents.' }", + ' foreach ($entry in $zip.Entries) {', + ' $destination = Join-Path $stagingDirectory $entry.FullName', + ' [IO.Compression.ZipFileExtensions]::ExtractToFile($entry, $destination, $true)', + ' }', + ' } finally { $zip.Dispose() }', + ' } finally { $archive.Dispose() }', + '$manifest = Get-Content -Raw -LiteralPath (Join-Path $stagingDirectory "manifest.json") | ConvertFrom-Json', + "if ($manifest.version -ne $expectedVersion -or $manifest.architecture -ne 'x64' -or $manifest.sourceCommit -ne $expectedCommit -or -not $manifest.testSigned -or $manifest.signerThumbprint.ToLowerInvariant() -ne $expectedSignerThumbprint) { throw 'Release manifest mismatch.' }", + "$expectedNames = @('aibridge-test.cer', 'aibridge.sys', 'roxy-kernel-bridge.exe')", + "if ($manifest.files.Count -ne $expectedNames.Count -or @($manifest.files | Where-Object { $_.name -notin $expectedNames }).Count -ne 0) { throw 'Unexpected release manifest file set.' }", + 'foreach ($file in $manifest.files) {', + ' $filePath = Join-Path $stagingDirectory $file.name', + " if ((Get-Item -LiteralPath $filePath).Length -ne $file.size -or (Get-FileHash -Algorithm SHA256 -LiteralPath $filePath).Hash.ToLowerInvariant() -ne $file.sha256.ToLowerInvariant()) { throw ('Integrity check failed for ' + $file.name + '.') }", + '}', + '$driverSource = Join-Path $stagingDirectory "aibridge.sys"', + '$bridgeSource = Join-Path $stagingDirectory "roxy-kernel-bridge.exe"', + '$bridgeRecord = @($manifest.files | Where-Object { $_.name -eq "roxy-kernel-bridge.exe" })', + "if ($bridgeRecord.Count -ne 1 -or $bridgeRecord[0].sha256.ToLowerInvariant() -ne $expectedBridgeHash -or (Get-FileHash -Algorithm SHA256 -LiteralPath $bridgeSource).Hash.ToLowerInvariant() -ne $expectedBridgeHash) { throw 'Bridge executable hash mismatch.' }", + '$certificateSource = Join-Path $stagingDirectory "aibridge-test.cer"', + '$certificate = New-Object Security.Cryptography.X509Certificates.X509Certificate2($certificateSource)', + "if ($certificate.Thumbprint.ToLowerInvariant() -ne $expectedSignerThumbprint) { throw 'Certificate thumbprint mismatch.' }", + '$signature = Get-AuthenticodeSignature -LiteralPath $driverSource', + "if (-not $signature.SignerCertificate -or $signature.SignerCertificate.Thumbprint.ToLowerInvariant() -ne $expectedSignerThumbprint) { throw 'Driver signer mismatch.' }", + `$serviceKey = ${powershellLiteral(`Registry::HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Services\${SERVICE_NAME}`)}`, + '$service = Get-ItemProperty -LiteralPath $serviceKey -ErrorAction SilentlyContinue', + 'if ($service) {', + ` $registeredPath = [Environment]::ExpandEnvironmentVariables([string]$service.ImagePath).Trim('"')`, + " if ($registeredPath.StartsWith('\\??\\')) { $registeredPath = $registeredPath.Substring(4) }", + " if ($registeredPath.StartsWith('\\\\?\\')) { $registeredPath = $registeredPath.Substring(4) }", + " if ([IO.Path]::GetFullPath($registeredPath) -ne [IO.Path]::GetFullPath($driverTarget)) { throw 'Refusing to replace an AIBridge service not owned by Roxy.' }", + ` & sc.exe stop ${SERVICE_NAME} | Out-Null`, + " for ($attempt = 0; $attempt -lt 30 -and (Get-Service -Name ${SERVICE_NAME} -ErrorAction SilentlyContinue).Status -ne 'Stopped'; $attempt++) { Start-Sleep -Milliseconds 500 }", + " if ((Get-Service -Name ${SERVICE_NAME} -ErrorAction SilentlyContinue).Status -ne 'Stopped') { throw 'The AIBridge service did not stop.' }", + '}', + 'Copy-Item -Force -LiteralPath $driverSource -Destination $driverTarget', + 'Copy-Item -Force -LiteralPath $bridgeSource -Destination $bridgeTarget', + '$previousInstallManifest = if (Test-Path $installManifestPath) { Get-Content -Raw -LiteralPath $installManifestPath | ConvertFrom-Json } else { $null }', + '$rootCertificateManagedByRoxy = [bool]$previousInstallManifest.rootCertificateManagedByRoxy -or -not (Test-Path ("Cert:\\LocalMachine\\Root\\" + $expectedSignerThumbprint))', + '$publisherCertificateManagedByRoxy = [bool]$previousInstallManifest.publisherCertificateManagedByRoxy -or -not (Test-Path ("Cert:\\LocalMachine\\TrustedPublisher\\" + $expectedSignerThumbprint))', + '@{ version = $expectedVersion; signerThumbprint = $expectedSignerThumbprint; rootCertificateManagedByRoxy = $rootCertificateManagedByRoxy; publisherCertificateManagedByRoxy = $publisherCertificateManagedByRoxy } | ConvertTo-Json | Set-Content -Encoding UTF8 -LiteralPath $installManifestPath', + 'try {', + ' if ($rootCertificateManagedByRoxy) { Import-Certificate -FilePath $certificateSource -CertStoreLocation Cert:\\LocalMachine\\Root | Out-Null }', + ' if ($publisherCertificateManagedByRoxy) { Import-Certificate -FilePath $certificateSource -CertStoreLocation Cert:\\LocalMachine\\TrustedPublisher | Out-Null }', + '} catch {', + ' if ($rootCertificateManagedByRoxy) { Remove-Item -Force ("Cert:\\LocalMachine\\Root\\" + $expectedSignerThumbprint) -ErrorAction SilentlyContinue }', + ' if ($publisherCertificateManagedByRoxy) { Remove-Item -Force ("Cert:\\LocalMachine\\TrustedPublisher\\" + $expectedSignerThumbprint) -ErrorAction SilentlyContinue }', + ' throw', + '}', + 'if (-not $service) {', + ` & sc.exe create ${SERVICE_NAME} 'type=' 'kernel' 'start=' 'demand' 'binPath=' $driverTarget | Out-Null`, + ' if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE }', + '}', + startDriver ? `& sc.exe start ${SERVICE_NAME} | Out-Null` : 'exit 0', + startDriver ? 'exit $LASTEXITCODE' : '', + '} finally { Remove-Item -Recurse -Force $stagingDirectory -ErrorAction SilentlyContinue }' + ].join('; ') +} +function uninstallDriverScript(): string { + return [ + "$ErrorActionPreference = 'Stop'", + `$installManifestPath = ${powershellLiteral(INSTALL_MANIFEST_PATH)}`, + `$serviceKey = ${powershellLiteral(`Registry::HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Services\${SERVICE_NAME}`)}`, + '$service = Get-ItemProperty -LiteralPath $serviceKey -ErrorAction SilentlyContinue', + 'if ($service) {', + ` $registeredPath = [Environment]::ExpandEnvironmentVariables([string]$service.ImagePath).Trim('"')`, + " if ($registeredPath.StartsWith('\\??\\')) { $registeredPath = $registeredPath.Substring(4) }", + " if ($registeredPath.StartsWith('\\\\?\\')) { $registeredPath = $registeredPath.Substring(4) }", + ` if ([IO.Path]::GetFullPath($registeredPath) -ne [IO.Path]::GetFullPath(${powershellLiteral(DRIVER_PATH)})) { throw 'Refusing to remove an AIBridge service not owned by Roxy.' }`, + ` & sc.exe stop ${SERVICE_NAME} | Out-Null`, + ' Start-Sleep -Seconds 1', + ` & sc.exe delete ${SERVICE_NAME} | Out-Null`, + ' if ($LASTEXITCODE -ne 0 -and $LASTEXITCODE -ne 1060) { exit $LASTEXITCODE }', + '}', + `Remove-Item -Force ${powershellLiteral(DRIVER_PATH)} -ErrorAction SilentlyContinue`, + `Remove-Item -Force ${powershellLiteral(BRIDGE_PATH)} -ErrorAction SilentlyContinue`, + '$installManifest = if (Test-Path $installManifestPath) { Get-Content -Raw -LiteralPath $installManifestPath | ConvertFrom-Json } else { $null }', + `if ($installManifest.signerThumbprint -eq ${powershellLiteral(SIGNER_THUMBPRINT)} -and $installManifest.rootCertificateManagedByRoxy) { Remove-Item -Force ${powershellLiteral(`Cert:\LocalMachine\Root\${SIGNER_THUMBPRINT}`)} -ErrorAction SilentlyContinue }`, + `if ($installManifest.signerThumbprint -eq ${powershellLiteral(SIGNER_THUMBPRINT)} -and $installManifest.publisherCertificateManagedByRoxy) { Remove-Item -Force ${powershellLiteral(`Cert:\LocalMachine\TrustedPublisher\${SIGNER_THUMBPRINT}`)} -ErrorAction SilentlyContinue }`, + `if (Test-Path ${powershellLiteral(DRIVER_PATH)}) { throw 'The installed driver file could not be removed.' }`, + `if (Test-Path ${powershellLiteral(BRIDGE_PATH)}) { throw 'The installed bridge file could not be removed.' }`, + `if (Test-Path ${powershellLiteral(PRIVILEGED_INSTALL_DIR)}) { Remove-Item -Recurse -Force ${powershellLiteral(PRIVILEGED_INSTALL_DIR)} -ErrorAction SilentlyContinue }` + ].join('; ') +} + +async function getDriverServiceState(): Promise { + try { + const { stdout } = await run( + systemExecutable('WindowsPowerShell\\v1.0\\powershell.exe'), + [ + '-NoProfile', + '-NonInteractive', + '-Command', + [ + `$serviceKey = ${powershellLiteral(`Registry::HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Services\${SERVICE_NAME}`)}`, + '$serviceConfig = Get-ItemProperty -LiteralPath $serviceKey -ErrorAction SilentlyContinue', + 'if (-not $serviceConfig) { 0; exit }', + `$expectedPath = ${powershellLiteral(DRIVER_PATH)}`, + `$registeredPath = [Environment]::ExpandEnvironmentVariables([string]$serviceConfig.ImagePath).Trim('"')`, + "if ($registeredPath.StartsWith('\\??\\')) { $registeredPath = $registeredPath.Substring(4) }", + "if ($registeredPath.StartsWith('\\\\?\\')) { $registeredPath = $registeredPath.Substring(4) }", + 'if ([IO.Path]::GetFullPath($registeredPath) -ne [IO.Path]::GetFullPath($expectedPath)) { 9; exit }', + `$service = Get-Service -Name ${powershellLiteral(SERVICE_NAME)} -ErrorAction SilentlyContinue`, + 'if ($service) { [int]$service.Status } else { 0 }' + ].join('; ') + ], + 5_000 + ) + if (stdout.trim() === '4') return 'running' + if (stdout.trim() === '1') return 'stopped' + if (stdout.trim() === '0') return 'not-installed' + return 'unknown' + } catch { + return 'unknown' + } +} + +async function isProcessElevated(): Promise { + try { + const { stdout } = await run( + systemExecutable('WindowsPowerShell\\v1.0\\powershell.exe'), + [ + '-NoProfile', + '-NonInteractive', + '-Command', + '([Security.Principal.WindowsPrincipal]::new([Security.Principal.WindowsIdentity]::GetCurrent())).IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)' + ], + 5_000 + ) + return stdout.trim().toLowerCase() === 'true' + } catch { + return false + } +} +function hasKernelMcpServer(): boolean { + return listMcpServers().some( + (server) => + MCP_SERVER_IDS.includes(server.id as (typeof MCP_SERVER_IDS)[number]) && + server.enabled && + server.config.type === 'local' && + path.resolve(server.config.command[0] ?? '') === path.resolve(BRIDGE_PATH) + ) +} + +async function removeKernelMcpServers(): Promise { + for (const id of MCP_SERVER_IDS) { + const server = listMcpServers().find((record) => record.id === id) + if ( + server?.config.type === 'local' && + path.resolve(server.config.command[0] ?? '') === path.resolve(BRIDGE_PATH) + ) { + await disposeConnection(id) + deleteMcpServer(id) + } + } +} + +async function setKernelAgentAccessImpl(enable: boolean): Promise { + if (!isWindows()) return { ok: false, error: 'Kernel tools require Windows.', steps: [] } + const steps: string[] = [] + + try { + await removeKernelMcpServers() + if (!enable) { + steps.push('Agent access disabled.') + return { ok: true, steps } + } + + const status = await getKernelStatus() + if (!status.installed || !status.bridgePath || status.driverState !== 'running') { + throw new Error('Install and start Kernel Tools before enabling agent access.') + } + + if (!(await isProcessElevated())) { + throw new Error( + 'Restart Roxy as administrator before enabling agent access. Roxy must remain elevated while using these tools.' + ) + } + + const existing = listMcpServers().find((record) => record.id === MCP_SERVER_ID) + if (existing) throw new Error(`An MCP server named ${MCP_SERVER_ID} already exists.`) + + upsertMcpServer({ + id: MCP_SERVER_ID, + config: { type: 'local', command: [status.bridgePath], cwd: path.dirname(status.bridgePath) }, + enabled: true + }) + steps.push('Agent access enabled through the roxy-kernel-tools MCP server.') + return { ok: true, steps } + } catch (error) { + return { ok: false, error: errorMessage(error), steps } + } +} + +export async function getKernelStatus(): Promise { + if (!isWindows()) { + return { + isWindows: false, + installed: false, + hasArtifacts: false, + driverPath: null, + testSigning: false, + bridgePath: null, + mcpRegistered: false, + driverState: 'not-installed' + } + } + + const [testSigning, driverState] = await Promise.all([ + checkTestSigning(), + getDriverServiceState() + ]) + const driverPath = existsSync(DRIVER_PATH) ? DRIVER_PATH : null + const bridgePath = existsSync(BRIDGE_PATH) ? BRIDGE_PATH : null + + let mcpRegistered = false + try { + mcpRegistered = hasKernelMcpServer() + } catch { + // The database may still be opening during app startup. + } + + const serviceExists = driverState === 'running' || driverState === 'stopped' + return { + isWindows: true, + installed: driverPath !== null && bridgePath !== null && serviceExists, + hasArtifacts: driverPath !== null || bridgePath !== null || serviceExists, + driverPath, + testSigning, + bridgePath, + mcpRegistered, + driverState + } +} + +async function fetchRelease(): Promise { + try { + const response = app.isReady() ? await electronNet.fetch(RELEASE_URL) : await fetch(RELEASE_URL) + if (response.ok) return response + } catch { + // Fall through to Node's network stack for environments where Chromium is blocked. + } + return fetch(RELEASE_URL) +} + +export async function setKernelAgentAccess(enable: boolean): Promise { + return runMutation(() => setKernelAgentAccessImpl(enable)) +} + +async function downloadPinnedRelease(steps: string[]): Promise { + steps.push(`Downloading pinned Kernel Tools ${RELEASE_TAG}...`) + await rm(ARCHIVE_PATH, { force: true }) + mkdirSync(INSTALL_DIR, { recursive: true }) + + const response = await fetchRelease() + if (!response.ok) throw new Error(`Kernel Tools download failed (${response.status}).`) + const contentLength = Number(response.headers.get('content-length') || 0) + if (contentLength > MAX_RELEASE_BYTES) throw new Error('The Kernel Tools download is too large.') + if (!response.body) throw new Error('The Kernel Tools download returned no body.') + const archive = await open(ARCHIVE_PATH, 'wx') + let downloadedBytes = 0 + try { + for await (const chunk of response.body) { + downloadedBytes += chunk.byteLength + if (downloadedBytes > MAX_RELEASE_BYTES) { + throw new Error('The Kernel Tools download is too large.') + } + await archive.write(chunk) + } + } finally { + await archive.close() + } + if (downloadedBytes === 0) throw new Error('The Kernel Tools download has an invalid size.') + if (sha256(ARCHIVE_PATH) !== RELEASE_SHA256) { + throw new Error('The Kernel Tools download failed its pinned SHA-256 integrity check.') + } + + steps.push(`Verified the pinned Kernel Tools ${RELEASE_TAG} archive.`) +} + +async function installKernelToolsImpl(): Promise { + if (!isSupportedPlatform()) { + return { ok: false, error: 'Kernel tools require 64-bit x64 Windows.', steps: [] } + } + const steps: string[] = [] + let enabledTestSigning = false + + try { + await removeKernelMcpServers() + await downloadPinnedRelease(steps) + + const testSigningWasEnabled = await checkTestSigning() + if (!testSigningWasEnabled) { + steps.push('Requesting administrator approval to enable Windows test signing...') + await setTestSigning(true) + enabledTestSigning = true + steps.push('Test signing enabled. Restart Windows before starting the driver.') + } else { + steps.push('Windows test signing is already enabled.') + } + + steps.push('Requesting administrator approval to install the driver service...') + await runElevated(systemExecutable('WindowsPowerShell\\v1.0\\powershell.exe'), [ + '-NoProfile', + '-NonInteractive', + '-Command', + installedDriverScript(testSigningWasEnabled) + ]) + const driverState = await getDriverServiceState() + if (driverState === 'running') { + steps.push('Driver started. Agent access is not enabled automatically.') + return { ok: true, steps } + } + if (!testSigningWasEnabled && driverState === 'stopped') { + steps.push('Driver service installed. Restart Windows, then return here and start it.') + return { ok: true, steps } + } + throw new Error('The driver service was not installed successfully.') + } catch (error) { + if (enabledTestSigning) { + try { + await setTestSigning(false) + steps.push('Test signing was disabled again because installation failed.') + } catch { + steps.push( + 'Installation failed after enabling test signing; disable it manually if needed.' + ) + } + } + return { ok: false, error: errorMessage(error), steps } + } +} + +export async function installKernelTools(): Promise { + return runMutation(installKernelToolsImpl) +} + +async function startKernelDriverImpl(): Promise { + if (!isSupportedPlatform()) { + return { ok: false, error: 'Kernel tools require 64-bit x64 Windows.', steps: [] } + } + const steps: string[] = [] + + if (!existsSync(DRIVER_PATH) || !existsSync(BRIDGE_PATH)) { + return { ok: false, error: 'Install kernel tools before starting the driver.', steps } + } + + const state = await getDriverServiceState() + if (state === 'unknown' || state === 'not-installed') { + return { ok: false, error: 'The AIBridge service is missing or is not owned by Roxy.', steps } + } + + try { + steps.push('Requesting administrator approval to start the driver...') + await runElevated(systemExecutable('sc.exe'), ['start', SERVICE_NAME]) + if ((await getDriverServiceState()) !== 'running') { + throw new Error('The driver did not enter the running state.') + } + steps.push('Driver started. Agent access remains disabled until you enable it.') + return { ok: true, steps } + } catch (error) { + return { ok: false, error: errorMessage(error), steps } + } +} + +export async function startKernelDriver(): Promise { + return runMutation(startKernelDriverImpl) +} + +async function toggleTestSigningImpl(enable: boolean): Promise { + if (!isWindows()) return { ok: false, error: 'Kernel tools require Windows.', steps: [] } + const steps: string[] = [] + + try { + steps.push( + `Requesting administrator approval to ${enable ? 'enable' : 'disable'} test signing...` + ) + await setTestSigning(enable) + steps.push(`Test signing ${enable ? 'enabled' : 'disabled'}. Restart Windows to apply.`) + return { ok: true, steps } + } catch (error) { + return { ok: false, error: errorMessage(error), steps } + } +} + +export async function toggleTestSigning(enable: boolean): Promise { + return runMutation(() => toggleTestSigningImpl(enable)) +} + +async function uninstallKernelToolsImpl(disableSigning: boolean): Promise { + if (!isWindows()) return { ok: false, error: 'Kernel tools require Windows.', steps: [] } + const steps: string[] = [] + + try { + await removeKernelMcpServers() + steps.push('Agent access disabled.') + + if ( + existsSync(DRIVER_PATH) || + existsSync(BRIDGE_PATH) || + existsSync(INSTALL_MANIFEST_PATH) || + (await getDriverServiceState()) !== 'not-installed' + ) { + steps.push('Requesting administrator approval to remove the driver service...') + await runElevated(systemExecutable('WindowsPowerShell\\v1.0\\powershell.exe'), [ + '-NoProfile', + '-NonInteractive', + '-Command', + uninstallDriverScript() + ]) + if ( + (await getDriverServiceState()) !== 'not-installed' || + existsSync(DRIVER_PATH) || + existsSync(BRIDGE_PATH) + ) { + throw new Error('The driver service could not be fully removed.') + } + steps.push('Driver service and bridge removed.') + } + + if (disableSigning) { + await setTestSigning(false) + steps.push('Test signing disabled. Restart Windows to apply.') + } + + await rm(INSTALL_DIR, { recursive: true, force: true }) + steps.push('Downloaded kernel-tools files removed.') + return { ok: true, steps } + } catch (error) { + return { ok: false, error: errorMessage(error), steps } + } +} + +export async function uninstallKernelTools(disableSigning: boolean): Promise { + return runMutation(() => uninstallKernelToolsImpl(disableSigning)) +} diff --git a/src/preload/index.ts b/src/preload/index.ts index 9637d3f..cf36710 100644 --- a/src/preload/index.ts +++ b/src/preload/index.ts @@ -310,6 +310,14 @@ const roxy: RoxyApi = { ipcRenderer.on(CHANNELS.remoteDelta, handler) return () => ipcRenderer.removeListener(CHANNELS.remoteDelta, handler) } + }, + kernel: { + status: () => ipcRenderer.invoke(CHANNELS.kernelStatus), + install: () => ipcRenderer.invoke(CHANNELS.kernelInstall), + start: () => ipcRenderer.invoke(CHANNELS.kernelStart), + setAgentAccess: (enable) => ipcRenderer.invoke(CHANNELS.kernelSetAgentAccess, enable), + uninstall: (disableSigning) => ipcRenderer.invoke(CHANNELS.kernelUninstall, disableSigning), + toggleTestSigning: (enable) => ipcRenderer.invoke(CHANNELS.kernelToggleTestSigning, enable) } } diff --git a/src/renderer/src/components/KernelToolsSection.tsx b/src/renderer/src/components/KernelToolsSection.tsx new file mode 100644 index 0000000..e2f0f7f --- /dev/null +++ b/src/renderer/src/components/KernelToolsSection.tsx @@ -0,0 +1,270 @@ +import { useEffect, useState } from 'react' +import { CircleCheck, ShieldAlert } from 'lucide-react' +import { useTranslation } from 'react-i18next' +import type { KernelInstallResult, KernelStatus } from '@shared/kernel' +import { api } from '../lib/api' +import { Button } from './ui' + +type ConfirmAction = 'install' | 'uninstall' | 'disableTestSigning' | 'enableAgentAccess' +type KernelAction = ConfirmAction | 'refresh' | 'start' | 'disableAgentAccess' + +export function KernelToolsSection(): JSX.Element { + const { t } = useTranslation() + const [status, setStatus] = useState(null) + const [action, setAction] = useState(null) + const [confirming, setConfirming] = useState(null) + const [result, setResult] = useState(null) + + const refresh = async (): Promise => { + try { + setStatus(await api.kernel.status()) + } catch (error) { + setResult({ + ok: false, + error: error instanceof Error ? error.message : String(error), + steps: [] + }) + } + } + + useEffect(() => { + void refresh() + }, []) + + const run = async ( + nextAction: KernelAction, + operation: () => Promise + ): Promise => { + setAction(nextAction) + setConfirming(null) + setResult(null) + try { + setResult(await operation()) + await refresh() + } catch (error) { + setResult({ + ok: false, + error: error instanceof Error ? error.message : String(error), + steps: [] + }) + } finally { + setAction(null) + } + } + + const performConfirmedAction = (): void => { + if (confirming === 'install') void run('install', () => api.kernel.install()) + if (confirming === 'uninstall') void run('uninstall', () => api.kernel.uninstall(false)) + if (confirming === 'disableTestSigning') { + void run('disableTestSigning', () => api.kernel.toggleTestSigning(false)) + } + if (confirming === 'enableAgentAccess') { + void run('enableAgentAccess', () => api.kernel.setAgentAccess(true)) + } + } + + const busy = action !== null + const isWindows = status?.isWindows ?? true + const installed = status?.installed ?? false + const hasArtifacts = status?.hasArtifacts ?? false + const driverRunning = status?.driverState === 'running' + const agentAccess = status?.mcpRegistered ?? false + + return ( +
+

+ {t('settings.kernel.heading')} +

+
+
+
+
+
+

+ {t('settings.kernel.description')} +

+ + {status && isWindows && ( +
+ {installed && } + {status.testSigning && ( + + )} + {installed && status.driverState === 'running' && ( + + )} + {installed && status.driverState === 'stopped' && ( + + )} + {installed && status.driverState === 'unknown' && ( + + )} + {agentAccess && } +
+ )} + + {result && ( +
+ {result.error &&

{t('settings.kernel.failed', { error: result.error })}

} + {result.steps.length > 0 && ( +
    + {result.steps.map((step, index) => ( +
  • {step}
  • + ))} +
+ )} +
+ )} +
+ +
+ {!isWindows && ( + + {t('settings.kernel.windowsOnly')} + + )} + + {isWindows && !installed && !confirming && ( + + )} + + {isWindows && hasArtifacts && !confirming && ( +
+ {installed && !driverRunning && ( + + )} + {installed && !agentAccess && ( + + )} + {agentAccess && ( + + )} + +
+ )} + + {isWindows && status?.testSigning && !hasArtifacts && !confirming && ( + + )} + + {isWindows && confirming && ( +
+

+ {confirming === 'install' && t('settings.kernel.confirmInstallDescription')} + {confirming === 'uninstall' && t('settings.kernel.confirmUninstallDescription')} + {confirming === 'disableTestSigning' && + t('settings.kernel.confirmDisableTestSigningDescription')} + {confirming === 'enableAgentAccess' && + t('settings.kernel.confirmEnableAgentAccessDescription')} +

+
+ + +
+
+ )} + + +
+
+
+
+ ) +} + +function StatusLine({ + label, + tone = 'success' +}: { + label: string + tone?: 'success' | 'warning' +}): JSX.Element { + const Icon = tone === 'success' ? CircleCheck : ShieldAlert + return ( +

+

+ ) +} diff --git a/src/renderer/src/locales/ar.json b/src/renderer/src/locales/ar.json index 8412fdd..a2ce17c 100644 --- a/src/renderer/src/locales/ar.json +++ b/src/renderer/src/locales/ar.json @@ -387,6 +387,37 @@ "resetTitle": "إعادة تعيين كل شيء", "wiping": "جارٍ المسح…" }, + "kernel": { + "confirmDisableTestSigningDescription": "Turn off Windows test signing? This removes support for test-signed drivers after the next reboot.", + "confirmEnableAgentAccessDescription": "Allow Roxy sessions to call the installed kernel bridge tools? These tools run with elevated system access. Only enable this on a dedicated research machine or VM.", + "confirmInstallDescription": "This downloads and builds the external kernel-tools project, enables Windows test signing, and installs its driver. Administrator approval and a reboot are required; agent access is not enabled automatically.", + "confirmUninstallDescription": "This stops and removes the kernel driver and any related agent registration. Test signing remains enabled until you disable it separately.", + "description": "Install the separately maintained Windows driver and bridge for privileged security research. Nothing runs until you explicitly install it, and agent access is not enabled automatically.", + "disableAgentAccess": "Disable agent access", + "disableTestSigning": "Disable test signing", + "disablingAgentAccess": "Disabling...", + "disablingTestSigning": "Disabling...", + "enableAgentAccess": "Enable agent access", + "enablingAgentAccess": "Enabling...", + "failed": "The operation failed: {{error}}", + "heading": "Kernel tools", + "install": "Install kernel tools", + "installing": "Installing...", + "refresh": "Refresh", + "start": "Start driver", + "starting": "Starting...", + "statusDriverRunning": "Driver service running", + "statusDriverStopped": "Driver service stopped; a reboot may be required", + "statusDriverUnknown": "Driver service state could not be determined", + "statusInstalled": "Driver and bridge installed", + "statusMCPRegistered": "Agent access registered", + "statusNotInstalled": "Not installed", + "statusTestSigning": "Windows test signing enabled", + "title": "Windows kernel bridge", + "windowsOnly": "Kernel Tools are only available on Windows.", + "uninstall": "Uninstall", + "uninstalling": "Uninstalling..." + }, "language": { "description": "اللغة التي كُتبت بها واجهة Roxy. لا تُغير هذه اللغة طريقة تحدث الوكيل معك — اسأله بأي لغة تريدها في الدردشة.", "heading": "اللغة", diff --git a/src/renderer/src/locales/de.json b/src/renderer/src/locales/de.json index 01c5d46..8af7303 100644 --- a/src/renderer/src/locales/de.json +++ b/src/renderer/src/locales/de.json @@ -387,6 +387,37 @@ "resetTitle": "Alles zurücksetzen", "wiping": "Lösche…" }, + "kernel": { + "confirmDisableTestSigningDescription": "Turn off Windows test signing? This removes support for test-signed drivers after the next reboot.", + "confirmEnableAgentAccessDescription": "Allow Roxy sessions to call the installed kernel bridge tools? These tools run with elevated system access. Only enable this on a dedicated research machine or VM.", + "confirmInstallDescription": "This downloads and builds the external kernel-tools project, enables Windows test signing, and installs its driver. Administrator approval and a reboot are required; agent access is not enabled automatically.", + "confirmUninstallDescription": "This stops and removes the kernel driver and any related agent registration. Test signing remains enabled until you disable it separately.", + "description": "Install the separately maintained Windows driver and bridge for privileged security research. Nothing runs until you explicitly install it, and agent access is not enabled automatically.", + "disableAgentAccess": "Disable agent access", + "disableTestSigning": "Disable test signing", + "disablingAgentAccess": "Disabling...", + "disablingTestSigning": "Disabling...", + "enableAgentAccess": "Enable agent access", + "enablingAgentAccess": "Enabling...", + "failed": "The operation failed: {{error}}", + "heading": "Kernel tools", + "install": "Install kernel tools", + "installing": "Installing...", + "refresh": "Refresh", + "start": "Start driver", + "starting": "Starting...", + "statusDriverRunning": "Driver service running", + "statusDriverStopped": "Driver service stopped; a reboot may be required", + "statusDriverUnknown": "Driver service state could not be determined", + "statusInstalled": "Driver and bridge installed", + "statusMCPRegistered": "Agent access registered", + "statusNotInstalled": "Not installed", + "statusTestSigning": "Windows test signing enabled", + "title": "Windows kernel bridge", + "windowsOnly": "Kernel Tools are only available on Windows.", + "uninstall": "Uninstall", + "uninstalling": "Uninstalling..." + }, "language": { "description": "Die Sprache, in der Roxys eigene Oberfläche geschrieben ist. Sie ändert nicht, wie der Agent mit Ihnen spricht – fragen Sie ihn im Chat in jeder beliebigen Sprache.", "heading": "Sprache", diff --git a/src/renderer/src/locales/default.json b/src/renderer/src/locales/default.json index 9d5a189..5115dd0 100644 --- a/src/renderer/src/locales/default.json +++ b/src/renderer/src/locales/default.json @@ -420,6 +420,37 @@ "idle": "Updates install automatically from GitHub." } }, + "kernel": { + "heading": "Kernel tools", + "title": "Windows kernel bridge", + "windowsOnly": "Kernel Tools are only available on Windows.", + "description": "Install the separately maintained Windows driver and bridge for privileged security research. Nothing runs until you explicitly install it, and agent access is not enabled automatically.", + "statusInstalled": "Driver and bridge installed", + "statusTestSigning": "Windows test signing enabled", + "statusDriverRunning": "Driver service running", + "statusDriverStopped": "Driver service stopped; a reboot may be required", + "statusDriverUnknown": "Driver service state could not be determined", + "statusMCPRegistered": "Agent access registered", + "enableAgentAccess": "Enable agent access", + "enablingAgentAccess": "Enabling...", + "disableAgentAccess": "Disable agent access", + "disablingAgentAccess": "Disabling...", + "statusNotInstalled": "Not installed", + "install": "Install kernel tools", + "installing": "Installing...", + "uninstall": "Uninstall", + "uninstalling": "Uninstalling...", + "start": "Start driver", + "starting": "Starting...", + "refresh": "Refresh", + "disableTestSigning": "Disable test signing", + "disablingTestSigning": "Disabling...", + "confirmInstallDescription": "This downloads a pinned, test-signed Kernel Tools release, trusts its pinned test certificate, enables Windows test signing, and installs its driver. Administrator approval and a reboot are required; Secure Boot may block test signing, and agent access is not enabled automatically.", + "confirmUninstallDescription": "This stops and removes the kernel driver and any related agent registration. Test signing remains enabled until you disable it separately.", + "confirmDisableTestSigningDescription": "Turn off Windows test signing? This removes support for test-signed drivers after the next reboot.", + "confirmEnableAgentAccessDescription": "Allow Roxy sessions to call the installed kernel bridge tools? These tools run with elevated system access. Only enable this on a dedicated research machine or VM.", + "failed": "The operation failed: {{error}}" + }, "danger": { "heading": "Danger zone", "resetTitle": "Reset everything", diff --git a/src/renderer/src/locales/es.json b/src/renderer/src/locales/es.json index 08cb689..becfbc5 100644 --- a/src/renderer/src/locales/es.json +++ b/src/renderer/src/locales/es.json @@ -387,6 +387,37 @@ "resetTitle": "Restablecer todo", "wiping": "Borrando…" }, + "kernel": { + "confirmDisableTestSigningDescription": "Turn off Windows test signing? This removes support for test-signed drivers after the next reboot.", + "confirmEnableAgentAccessDescription": "Allow Roxy sessions to call the installed kernel bridge tools? These tools run with elevated system access. Only enable this on a dedicated research machine or VM.", + "confirmInstallDescription": "This downloads and builds the external kernel-tools project, enables Windows test signing, and installs its driver. Administrator approval and a reboot are required; agent access is not enabled automatically.", + "confirmUninstallDescription": "This stops and removes the kernel driver and any related agent registration. Test signing remains enabled until you disable it separately.", + "description": "Install the separately maintained Windows driver and bridge for privileged security research. Nothing runs until you explicitly install it, and agent access is not enabled automatically.", + "disableAgentAccess": "Disable agent access", + "disableTestSigning": "Disable test signing", + "disablingAgentAccess": "Disabling...", + "disablingTestSigning": "Disabling...", + "enableAgentAccess": "Enable agent access", + "enablingAgentAccess": "Enabling...", + "failed": "The operation failed: {{error}}", + "heading": "Kernel tools", + "install": "Install kernel tools", + "installing": "Installing...", + "refresh": "Refresh", + "start": "Start driver", + "starting": "Starting...", + "statusDriverRunning": "Driver service running", + "statusDriverStopped": "Driver service stopped; a reboot may be required", + "statusDriverUnknown": "Driver service state could not be determined", + "statusInstalled": "Driver and bridge installed", + "statusMCPRegistered": "Agent access registered", + "statusNotInstalled": "Not installed", + "statusTestSigning": "Windows test signing enabled", + "title": "Windows kernel bridge", + "windowsOnly": "Kernel Tools are only available on Windows.", + "uninstall": "Uninstall", + "uninstalling": "Uninstalling..." + }, "language": { "description": "El idioma en el que está escrita la interfaz de Roxy. No cambia cómo te habla el agente: escríbele en el idioma que prefieras, en el chat.", "heading": "Idioma", diff --git a/src/renderer/src/locales/fr.json b/src/renderer/src/locales/fr.json index 08e2401..85d8389 100644 --- a/src/renderer/src/locales/fr.json +++ b/src/renderer/src/locales/fr.json @@ -387,6 +387,37 @@ "resetTitle": "Tout réinitialiser", "wiping": "Effacement…" }, + "kernel": { + "confirmDisableTestSigningDescription": "Turn off Windows test signing? This removes support for test-signed drivers after the next reboot.", + "confirmEnableAgentAccessDescription": "Allow Roxy sessions to call the installed kernel bridge tools? These tools run with elevated system access. Only enable this on a dedicated research machine or VM.", + "confirmInstallDescription": "This downloads and builds the external kernel-tools project, enables Windows test signing, and installs its driver. Administrator approval and a reboot are required; agent access is not enabled automatically.", + "confirmUninstallDescription": "This stops and removes the kernel driver and any related agent registration. Test signing remains enabled until you disable it separately.", + "description": "Install the separately maintained Windows driver and bridge for privileged security research. Nothing runs until you explicitly install it, and agent access is not enabled automatically.", + "disableAgentAccess": "Disable agent access", + "disableTestSigning": "Disable test signing", + "disablingAgentAccess": "Disabling...", + "disablingTestSigning": "Disabling...", + "enableAgentAccess": "Enable agent access", + "enablingAgentAccess": "Enabling...", + "failed": "The operation failed: {{error}}", + "heading": "Kernel tools", + "install": "Install kernel tools", + "installing": "Installing...", + "refresh": "Refresh", + "start": "Start driver", + "starting": "Starting...", + "statusDriverRunning": "Driver service running", + "statusDriverStopped": "Driver service stopped; a reboot may be required", + "statusDriverUnknown": "Driver service state could not be determined", + "statusInstalled": "Driver and bridge installed", + "statusMCPRegistered": "Agent access registered", + "statusNotInstalled": "Not installed", + "statusTestSigning": "Windows test signing enabled", + "title": "Windows kernel bridge", + "windowsOnly": "Kernel Tools are only available on Windows.", + "uninstall": "Uninstall", + "uninstalling": "Uninstalling..." + }, "language": { "description": "La langue de l'interface de Roxy. Cela ne change pas la façon dont l'agent vous parle — demandez-lui ce que vous voulez dans la langue de votre choix, dans le chat.", "heading": "Langue", diff --git a/src/renderer/src/locales/hi.json b/src/renderer/src/locales/hi.json index 64e953e..0e76376 100644 --- a/src/renderer/src/locales/hi.json +++ b/src/renderer/src/locales/hi.json @@ -387,6 +387,37 @@ "resetTitle": "सब कुछ रीसेट करें", "wiping": "मिटाया जा रहा है…" }, + "kernel": { + "confirmDisableTestSigningDescription": "Turn off Windows test signing? This removes support for test-signed drivers after the next reboot.", + "confirmEnableAgentAccessDescription": "Allow Roxy sessions to call the installed kernel bridge tools? These tools run with elevated system access. Only enable this on a dedicated research machine or VM.", + "confirmInstallDescription": "This downloads and builds the external kernel-tools project, enables Windows test signing, and installs its driver. Administrator approval and a reboot are required; agent access is not enabled automatically.", + "confirmUninstallDescription": "This stops and removes the kernel driver and any related agent registration. Test signing remains enabled until you disable it separately.", + "description": "Install the separately maintained Windows driver and bridge for privileged security research. Nothing runs until you explicitly install it, and agent access is not enabled automatically.", + "disableAgentAccess": "Disable agent access", + "disableTestSigning": "Disable test signing", + "disablingAgentAccess": "Disabling...", + "disablingTestSigning": "Disabling...", + "enableAgentAccess": "Enable agent access", + "enablingAgentAccess": "Enabling...", + "failed": "The operation failed: {{error}}", + "heading": "Kernel tools", + "install": "Install kernel tools", + "installing": "Installing...", + "refresh": "Refresh", + "start": "Start driver", + "starting": "Starting...", + "statusDriverRunning": "Driver service running", + "statusDriverStopped": "Driver service stopped; a reboot may be required", + "statusDriverUnknown": "Driver service state could not be determined", + "statusInstalled": "Driver and bridge installed", + "statusMCPRegistered": "Agent access registered", + "statusNotInstalled": "Not installed", + "statusTestSigning": "Windows test signing enabled", + "title": "Windows kernel bridge", + "windowsOnly": "Kernel Tools are only available on Windows.", + "uninstall": "Uninstall", + "uninstalling": "Uninstalling..." + }, "language": { "description": "वह भाषा जिसमें Roxy का अपना इंटरफ़ेस लिखा गया है। यह इस बात को नहीं बदलता कि एजेंट आपसे कैसे बात करता है — चैट में आप जिस भी भाषा में चाहें, उससे पूछें।", "heading": "भाषा", diff --git a/src/renderer/src/locales/ja.json b/src/renderer/src/locales/ja.json index 287c671..efa7efc 100644 --- a/src/renderer/src/locales/ja.json +++ b/src/renderer/src/locales/ja.json @@ -387,6 +387,37 @@ "resetTitle": "すべてリセット", "wiping": "消去中…" }, + "kernel": { + "confirmDisableTestSigningDescription": "Turn off Windows test signing? This removes support for test-signed drivers after the next reboot.", + "confirmEnableAgentAccessDescription": "Allow Roxy sessions to call the installed kernel bridge tools? These tools run with elevated system access. Only enable this on a dedicated research machine or VM.", + "confirmInstallDescription": "This downloads and builds the external kernel-tools project, enables Windows test signing, and installs its driver. Administrator approval and a reboot are required; agent access is not enabled automatically.", + "confirmUninstallDescription": "This stops and removes the kernel driver and any related agent registration. Test signing remains enabled until you disable it separately.", + "description": "Install the separately maintained Windows driver and bridge for privileged security research. Nothing runs until you explicitly install it, and agent access is not enabled automatically.", + "disableAgentAccess": "Disable agent access", + "disableTestSigning": "Disable test signing", + "disablingAgentAccess": "Disabling...", + "disablingTestSigning": "Disabling...", + "enableAgentAccess": "Enable agent access", + "enablingAgentAccess": "Enabling...", + "failed": "The operation failed: {{error}}", + "heading": "Kernel tools", + "install": "Install kernel tools", + "installing": "Installing...", + "refresh": "Refresh", + "start": "Start driver", + "starting": "Starting...", + "statusDriverRunning": "Driver service running", + "statusDriverStopped": "Driver service stopped; a reboot may be required", + "statusDriverUnknown": "Driver service state could not be determined", + "statusInstalled": "Driver and bridge installed", + "statusMCPRegistered": "Agent access registered", + "statusNotInstalled": "Not installed", + "statusTestSigning": "Windows test signing enabled", + "title": "Windows kernel bridge", + "windowsOnly": "Kernel Tools are only available on Windows.", + "uninstall": "Uninstall", + "uninstalling": "Uninstalling..." + }, "language": { "description": "Roxyのインターフェースの表示言語です。エージェントとの会話方法には影響しません。チャットでは好きな言語で質問してください。", "heading": "言語", diff --git a/src/renderer/src/locales/pt.json b/src/renderer/src/locales/pt.json index 5171c79..0baa46a 100644 --- a/src/renderer/src/locales/pt.json +++ b/src/renderer/src/locales/pt.json @@ -387,6 +387,37 @@ "resetTitle": "Redefinir tudo", "wiping": "Apagando…" }, + "kernel": { + "confirmDisableTestSigningDescription": "Turn off Windows test signing? This removes support for test-signed drivers after the next reboot.", + "confirmEnableAgentAccessDescription": "Allow Roxy sessions to call the installed kernel bridge tools? These tools run with elevated system access. Only enable this on a dedicated research machine or VM.", + "confirmInstallDescription": "This downloads and builds the external kernel-tools project, enables Windows test signing, and installs its driver. Administrator approval and a reboot are required; agent access is not enabled automatically.", + "confirmUninstallDescription": "This stops and removes the kernel driver and any related agent registration. Test signing remains enabled until you disable it separately.", + "description": "Install the separately maintained Windows driver and bridge for privileged security research. Nothing runs until you explicitly install it, and agent access is not enabled automatically.", + "disableAgentAccess": "Disable agent access", + "disableTestSigning": "Disable test signing", + "disablingAgentAccess": "Disabling...", + "disablingTestSigning": "Disabling...", + "enableAgentAccess": "Enable agent access", + "enablingAgentAccess": "Enabling...", + "failed": "The operation failed: {{error}}", + "heading": "Kernel tools", + "install": "Install kernel tools", + "installing": "Installing...", + "refresh": "Refresh", + "start": "Start driver", + "starting": "Starting...", + "statusDriverRunning": "Driver service running", + "statusDriverStopped": "Driver service stopped; a reboot may be required", + "statusDriverUnknown": "Driver service state could not be determined", + "statusInstalled": "Driver and bridge installed", + "statusMCPRegistered": "Agent access registered", + "statusNotInstalled": "Not installed", + "statusTestSigning": "Windows test signing enabled", + "title": "Windows kernel bridge", + "windowsOnly": "Kernel Tools are only available on Windows.", + "uninstall": "Uninstall", + "uninstalling": "Uninstalling..." + }, "language": { "description": "O idioma da interface do Roxy. Isso não altera a forma como o agente fala com você — pergunte o que quiser, no idioma que preferir, no chat.", "heading": "Idioma", diff --git a/src/renderer/src/locales/ru.json b/src/renderer/src/locales/ru.json index b5d0e14..395a8ca 100644 --- a/src/renderer/src/locales/ru.json +++ b/src/renderer/src/locales/ru.json @@ -387,6 +387,37 @@ "resetTitle": "Сбросить всё", "wiping": "Удаление…" }, + "kernel": { + "confirmDisableTestSigningDescription": "Turn off Windows test signing? This removes support for test-signed drivers after the next reboot.", + "confirmEnableAgentAccessDescription": "Allow Roxy sessions to call the installed kernel bridge tools? These tools run with elevated system access. Only enable this on a dedicated research machine or VM.", + "confirmInstallDescription": "This downloads and builds the external kernel-tools project, enables Windows test signing, and installs its driver. Administrator approval and a reboot are required; agent access is not enabled automatically.", + "confirmUninstallDescription": "This stops and removes the kernel driver and any related agent registration. Test signing remains enabled until you disable it separately.", + "description": "Install the separately maintained Windows driver and bridge for privileged security research. Nothing runs until you explicitly install it, and agent access is not enabled automatically.", + "disableAgentAccess": "Disable agent access", + "disableTestSigning": "Disable test signing", + "disablingAgentAccess": "Disabling...", + "disablingTestSigning": "Disabling...", + "enableAgentAccess": "Enable agent access", + "enablingAgentAccess": "Enabling...", + "failed": "The operation failed: {{error}}", + "heading": "Kernel tools", + "install": "Install kernel tools", + "installing": "Installing...", + "refresh": "Refresh", + "start": "Start driver", + "starting": "Starting...", + "statusDriverRunning": "Driver service running", + "statusDriverStopped": "Driver service stopped; a reboot may be required", + "statusDriverUnknown": "Driver service state could not be determined", + "statusInstalled": "Driver and bridge installed", + "statusMCPRegistered": "Agent access registered", + "statusNotInstalled": "Not installed", + "statusTestSigning": "Windows test signing enabled", + "title": "Windows kernel bridge", + "windowsOnly": "Kernel Tools are only available on Windows.", + "uninstall": "Uninstall", + "uninstalling": "Uninstalling..." + }, "language": { "description": "Язык интерфейса Roxy. Это не влияет на то, как агент общается с вами — задавайте ему вопросы на любом языке в чате.", "heading": "Язык", diff --git a/src/renderer/src/locales/zh.json b/src/renderer/src/locales/zh.json index 0332627..4d39d24 100644 --- a/src/renderer/src/locales/zh.json +++ b/src/renderer/src/locales/zh.json @@ -387,6 +387,37 @@ "resetTitle": "重置所有内容", "wiping": "正在清除…" }, + "kernel": { + "confirmDisableTestSigningDescription": "Turn off Windows test signing? This removes support for test-signed drivers after the next reboot.", + "confirmEnableAgentAccessDescription": "Allow Roxy sessions to call the installed kernel bridge tools? These tools run with elevated system access. Only enable this on a dedicated research machine or VM.", + "confirmInstallDescription": "This downloads and builds the external kernel-tools project, enables Windows test signing, and installs its driver. Administrator approval and a reboot are required; agent access is not enabled automatically.", + "confirmUninstallDescription": "This stops and removes the kernel driver and any related agent registration. Test signing remains enabled until you disable it separately.", + "description": "Install the separately maintained Windows driver and bridge for privileged security research. Nothing runs until you explicitly install it, and agent access is not enabled automatically.", + "disableAgentAccess": "Disable agent access", + "disableTestSigning": "Disable test signing", + "disablingAgentAccess": "Disabling...", + "disablingTestSigning": "Disabling...", + "enableAgentAccess": "Enable agent access", + "enablingAgentAccess": "Enabling...", + "failed": "The operation failed: {{error}}", + "heading": "Kernel tools", + "install": "Install kernel tools", + "installing": "Installing...", + "refresh": "Refresh", + "start": "Start driver", + "starting": "Starting...", + "statusDriverRunning": "Driver service running", + "statusDriverStopped": "Driver service stopped; a reboot may be required", + "statusDriverUnknown": "Driver service state could not be determined", + "statusInstalled": "Driver and bridge installed", + "statusMCPRegistered": "Agent access registered", + "statusNotInstalled": "Not installed", + "statusTestSigning": "Windows test signing enabled", + "title": "Windows kernel bridge", + "windowsOnly": "Kernel Tools are only available on Windows.", + "uninstall": "Uninstall", + "uninstalling": "Uninstalling..." + }, "language": { "description": "Roxy 界面本身的显示语言。它不会改变代理与您交流的方式 — 您可以在聊天中用任何您喜欢的语言提问。", "heading": "语言", diff --git a/src/renderer/src/routes/Settings.tsx b/src/renderer/src/routes/Settings.tsx index b9709b1..7d20ad5 100644 --- a/src/renderer/src/routes/Settings.tsx +++ b/src/renderer/src/routes/Settings.tsx @@ -27,6 +27,7 @@ import { SubscriptionAccounts } from '../components/SubscriptionSetup' import { ModelVisibility } from '../components/ModelVisibility' import { useRoxyStore } from '../lib/store' import { MotionSettings } from '../components/MotionSettings' +import { KernelToolsSection } from '../components/KernelToolsSection' /** The section heading repeated down the page. */ const SECTION_HEADING = 'mb-3 text-xs font-semibold uppercase tracking-wide text-text-subtle' @@ -402,6 +403,8 @@ export default function Settings(): JSX.Element { + +

{t('settings.danger.heading')} diff --git a/src/shared/api.ts b/src/shared/api.ts index 142a31a..9b5af92 100644 --- a/src/shared/api.ts +++ b/src/shared/api.ts @@ -33,6 +33,7 @@ import type { RepoLayout } from './repos' import type { SessionConfigPatch } from './session-config' import type { ClipboardAction } from './context-menu' import type { ResolvedTheme, ThemeView } from './theme' +import type { KernelStatus, KernelInstallResult } from './kernel' /** A configured MCP server merged with its live connection status (for Settings). */ export interface McpServerView { @@ -1192,4 +1193,12 @@ export interface RoxyApi { */ onDelta(callback: (payload: RemoteDelta) => void): () => void } + kernel: { + status(): Promise + install(): Promise + start(): Promise + setAgentAccess(enable: boolean): Promise + uninstall(disableSigning: boolean): Promise + toggleTestSigning(enable: boolean): Promise + } } diff --git a/src/shared/ipc.ts b/src/shared/ipc.ts index a2af080..235b12e 100644 --- a/src/shared/ipc.ts +++ b/src/shared/ipc.ts @@ -272,7 +272,14 @@ export const CHANNELS = { /** main -> renderer: Remote Workspace sharing status changed */ remoteState: 'remote:state', /** main -> renderer: a streamed event from a phone-driven turn (live desktop mirror) */ - remoteDelta: 'remote:delta' + remoteDelta: 'remote:delta', + + kernelStatus: 'kernel:status', + kernelInstall: 'kernel:install', + kernelStart: 'kernel:start', + kernelSetAgentAccess: 'kernel:setAgentAccess', + kernelUninstall: 'kernel:uninstall', + kernelToggleTestSigning: 'kernel:toggleTestSigning' } as const export type ChannelName = (typeof CHANNELS)[keyof typeof CHANNELS] diff --git a/src/shared/kernel.ts b/src/shared/kernel.ts new file mode 100644 index 0000000..eb60dad --- /dev/null +++ b/src/shared/kernel.ts @@ -0,0 +1,16 @@ +export interface KernelStatus { + isWindows: boolean + installed: boolean + hasArtifacts: boolean + driverPath: string | null + testSigning: boolean + bridgePath: string | null + mcpRegistered: boolean + driverState: 'not-installed' | 'stopped' | 'running' | 'unknown' +} + +export interface KernelInstallResult { + ok: boolean + error?: string + steps: string[] +} diff --git a/test/kernel.ts b/test/kernel.ts new file mode 100644 index 0000000..6437ed7 --- /dev/null +++ b/test/kernel.ts @@ -0,0 +1,66 @@ +import assert from 'node:assert/strict' +import { readFileSync } from 'node:fs' +import path from 'node:path' +import { CHANNELS } from '../src/shared/ipc' +import type { KernelStatus } from '../src/shared/kernel' + +const nonWindowsStatus: KernelStatus = { + isWindows: false, + installed: false, + hasArtifacts: false, + driverPath: null, + bridgePath: null, + testSigning: false, + mcpRegistered: false, + driverState: 'not-installed' +} + +assert.equal(CHANNELS.kernelStatus, 'kernel:status') +assert.equal(CHANNELS.kernelInstall, 'kernel:install') +assert.equal(CHANNELS.kernelStart, 'kernel:start') +assert.equal(CHANNELS.kernelSetAgentAccess, 'kernel:setAgentAccess') +assert.equal(CHANNELS.kernelUninstall, 'kernel:uninstall') +assert.equal(CHANNELS.kernelToggleTestSigning, 'kernel:toggleTestSigning') +assert.equal(nonWindowsStatus.isWindows, false) + +const kernelService = readFileSync(path.join(process.cwd(), 'src/main/services/kernel.ts'), 'utf8') +assert.match(kernelService, /RELEASE_TAG = 'v\d+\.\d+\.\d+'/) +assert.match(kernelService, /RELEASE_SHA256 = '[0-9a-f]{64}'/) +assert.match(kernelService, /RELEASE_COMMIT = '[0-9a-f]{40}'/) +assert.match(kernelService, /SIGNER_THUMBPRINT = '[0-9a-f]{40}'/) +assert.match(kernelService, /BRIDGE_SHA256 = '[0-9a-f]{64}'/) +assert.match(kernelService, /Bridge executable hash mismatch/) +assert.match(kernelService, /Get-AuthenticodeSignature/) +assert.match(kernelService, /IO\.FileShare\]::None/) +assert.match(kernelService, /isSupportedPlatform/) +assert.match(kernelService, /RoxyKernelToolsAIBridge/) +assert.match(kernelService, /Another Kernel Tools operation is already in progress/) +assert.doesNotMatch(kernelService, /git\.exe', \['clone'/) +assert.doesNotMatch(kernelService, /driver', 'build\.ps1'/) + +const defaultLocale = JSON.parse( + readFileSync(path.join(process.cwd(), 'src/renderer/src/locales/default.json'), 'utf8') +) as { settings?: { kernel?: Record } } +const kernelStrings = defaultLocale.settings?.kernel +assert.ok(kernelStrings, 'settings.kernel locale section exists') +for (const key of [ + 'heading', + 'title', + 'windowsOnly', + 'description', + 'install', + 'uninstall', + 'refresh', + 'disableTestSigning', + 'enableAgentAccess', + 'disableAgentAccess', + 'confirmEnableAgentAccessDescription' +]) { + assert.ok(kernelStrings[key], `settings.kernel.${key} resolves`) +} + +const kernelIpc = readFileSync(path.join(process.cwd(), 'src/main/ipc/kernel.ts'), 'utf8') +assert.match(kernelIpc, /event\.senderFrame !== mainWindow\.webContents\.mainFrame/) +assert.match(kernelIpc, /typeof enable !== 'boolean'/) + +console.log('kernel shared contract OK')