From 418e99c8c0e13dcea08866cd29c86f9464d0370c Mon Sep 17 00:00:00 2001 From: Nicole Rappe Date: Fri, 17 Oct 2025 20:44:26 -0600 Subject: [PATCH 1/7] Fix agent keystore initialization order --- Data/Agent/agent.py | 35 +++++----- Data/Agent/security.py | 8 ++- Data/Server/Modules/auth/jwt_service.py | 28 +++++++- Data/Server/Modules/crypto/certificates.py | 6 +- Data/Server/Modules/crypto/signing.py | 34 +++++++++- Data/Server/Modules/runtime.py | 78 ++++++++++++++++++++++ Data/Server/WebUI/vite.config.mts | 8 ++- Data/Server/server.py | 36 ++++++++++ 8 files changed, 205 insertions(+), 28 deletions(-) create mode 100644 Data/Server/Modules/runtime.py diff --git a/Data/Agent/agent.py b/Data/Agent/agent.py index 3deef9a..5bd915a 100644 --- a/Data/Agent/agent.py +++ b/Data/Agent/agent.py @@ -125,6 +125,24 @@ def _agent_guid_path() -> str: return os.path.abspath(os.path.join(os.path.dirname(__file__), 'agent_GUID')) +def _settings_dir(): + try: + return os.path.join(_find_project_root(), 'Agent', 'Borealis', 'Settings') + except Exception: + return os.path.abspath(os.path.join(os.path.dirname(__file__), 'Settings')) + + +_KEY_STORE_INSTANCE = None + + +def _key_store() -> AgentKeyStore: + global _KEY_STORE_INSTANCE + if _KEY_STORE_INSTANCE is None: + scope = 'SYSTEM' if SYSTEM_SERVICE_MODE else 'CURRENTUSER' + _KEY_STORE_INSTANCE = AgentKeyStore(_settings_dir(), scope=scope) + return _KEY_STORE_INSTANCE + + def _persist_agent_guid_local(guid: str): guid = _normalize_agent_guid(guid) if not guid: @@ -1029,23 +1047,6 @@ def _collect_heartbeat_metrics() -> Dict[str, Any]: def _settings_dir(): - try: - return os.path.join(_find_project_root(), 'Agent', 'Borealis', 'Settings') - except Exception: - return os.path.abspath(os.path.join(os.path.dirname(__file__), 'Settings')) - - -_KEY_STORE_INSTANCE = None - - -def _key_store() -> AgentKeyStore: - global _KEY_STORE_INSTANCE - if _KEY_STORE_INSTANCE is None: - scope = 'SYSTEM' if SYSTEM_SERVICE_MODE else 'CURRENTUSER' - _KEY_STORE_INSTANCE = AgentKeyStore(_settings_dir(), scope=scope) - return _KEY_STORE_INSTANCE - - SERVER_CERT_PATH = _key_store().server_certificate_path() diff --git a/Data/Agent/security.py b/Data/Agent/security.py index e3feb9d..890c005 100644 --- a/Data/Agent/security.py +++ b/Data/Agent/security.py @@ -39,7 +39,9 @@ def _restrict_permissions(path: str) -> None: def _protect(data: bytes, *, scope_system: bool) -> bytes: if not IS_WINDOWS or not win32crypt: return data - flags = win32crypt.CRYPTPROTECT_LOCAL_MACHINE if scope_system else 0 + flags = 0 + if scope_system: + flags = getattr(win32crypt, "CRYPTPROTECT_LOCAL_MACHINE", 0x4) protected = win32crypt.CryptProtectData(data, None, None, None, None, flags) # type: ignore[attr-defined] return protected[1] @@ -47,7 +49,9 @@ def _protect(data: bytes, *, scope_system: bool) -> bytes: def _unprotect(data: bytes, *, scope_system: bool) -> bytes: if not IS_WINDOWS or not win32crypt: return data - flags = win32crypt.CRYPTPROTECT_LOCAL_MACHINE if scope_system else 0 + flags = 0 + if scope_system: + flags = getattr(win32crypt, "CRYPTPROTECT_LOCAL_MACHINE", 0x4) unwrapped = win32crypt.CryptUnprotectData(data, None, None, None, None, flags) # type: ignore[attr-defined] return unwrapped[1] diff --git a/Data/Server/Modules/auth/jwt_service.py b/Data/Server/Modules/auth/jwt_service.py index 9d77859..ab5640b 100644 --- a/Data/Server/Modules/auth/jwt_service.py +++ b/Data/Server/Modules/auth/jwt_service.py @@ -7,15 +7,17 @@ from __future__ import annotations import hashlib import time from datetime import datetime, timezone -from pathlib import Path from typing import Any, Dict, Optional import jwt from cryptography.hazmat.primitives import serialization from cryptography.hazmat.primitives.asymmetric import ed25519 -_KEY_DIR = Path(__file__).resolve().parent.parent / "keys" +from Modules.runtime import ensure_runtime_dir, runtime_path + +_KEY_DIR = runtime_path("auth_keys") _KEY_FILE = _KEY_DIR / "borealis-jwt-ed25519.key" +_LEGACY_KEY_FILE = runtime_path("keys") / "borealis-jwt-ed25519.key" class JWTService: @@ -96,11 +98,17 @@ def load_service() -> JWTService: def _load_or_create_private_key() -> ed25519.Ed25519PrivateKey: - _KEY_DIR.mkdir(parents=True, exist_ok=True) + ensure_runtime_dir("auth_keys") + _migrate_legacy_key_if_present() + if _KEY_FILE.exists(): with _KEY_FILE.open("rb") as fh: return serialization.load_pem_private_key(fh.read(), password=None) + if _LEGACY_KEY_FILE.exists(): + with _LEGACY_KEY_FILE.open("rb") as fh: + return serialization.load_pem_private_key(fh.read(), password=None) + private_key = ed25519.Ed25519PrivateKey.generate() pem = private_key.private_bytes( encoding=serialization.Encoding.PEM, @@ -116,3 +124,17 @@ def _load_or_create_private_key() -> ed25519.Ed25519PrivateKey: pass return private_key + +def _migrate_legacy_key_if_present() -> None: + if not _LEGACY_KEY_FILE.exists() or _KEY_FILE.exists(): + return + + try: + ensure_runtime_dir("auth_keys") + try: + _LEGACY_KEY_FILE.replace(_KEY_FILE) + except Exception: + _KEY_FILE.write_bytes(_LEGACY_KEY_FILE.read_bytes()) + except Exception: + return + diff --git a/Data/Server/Modules/crypto/certificates.py b/Data/Server/Modules/crypto/certificates.py index b50c40f..ff6b1db 100644 --- a/Data/Server/Modules/crypto/certificates.py +++ b/Data/Server/Modules/crypto/certificates.py @@ -19,7 +19,9 @@ from cryptography.hazmat.primitives import hashes, serialization from cryptography.hazmat.primitives.asymmetric import ec from cryptography.x509.oid import NameOID -_CERT_DIR = Path(__file__).resolve().parent.parent / "certs" +from Modules.runtime import ensure_runtime_dir, runtime_path + +_CERT_DIR = runtime_path("certs") _CERT_FILE = _CERT_DIR / "borealis-server-cert.pem" _KEY_FILE = _CERT_DIR / "borealis-server-key.pem" _BUNDLE_FILE = _CERT_DIR / "borealis-server-bundle.pem" @@ -35,7 +37,7 @@ def ensure_certificate(common_name: str = "Borealis Server") -> Tuple[Path, Path Returns (cert_path, key_path, bundle_path). """ - _CERT_DIR.mkdir(parents=True, exist_ok=True) + ensure_runtime_dir("certs") regenerate = not (_CERT_FILE.exists() and _KEY_FILE.exists()) if not regenerate: diff --git a/Data/Server/Modules/crypto/signing.py b/Data/Server/Modules/crypto/signing.py index 18403c9..b74e537 100644 --- a/Data/Server/Modules/crypto/signing.py +++ b/Data/Server/Modules/crypto/signing.py @@ -10,11 +10,15 @@ from typing import Tuple from cryptography.hazmat.primitives import serialization from cryptography.hazmat.primitives.asymmetric import ed25519 +from Modules.runtime import ensure_runtime_dir, runtime_path + from .keys import base64_from_spki_der -_KEY_DIR = Path(__file__).resolve().parent.parent / "keys" +_KEY_DIR = runtime_path("script_signing_keys") _SIGNING_KEY_FILE = _KEY_DIR / "borealis-script-ed25519.key" _SIGNING_PUB_FILE = _KEY_DIR / "borealis-script-ed25519.pub" +_LEGACY_KEY_FILE = runtime_path("keys") / "borealis-script-ed25519.key" +_LEGACY_PUB_FILE = runtime_path("keys") / "borealis-script-ed25519.pub" class ScriptSigner: @@ -41,11 +45,17 @@ def load_signer() -> ScriptSigner: def _load_or_create() -> ed25519.Ed25519PrivateKey: - _KEY_DIR.mkdir(parents=True, exist_ok=True) + ensure_runtime_dir("script_signing_keys") + _migrate_legacy_material_if_present() + if _SIGNING_KEY_FILE.exists(): with _SIGNING_KEY_FILE.open("rb") as fh: return serialization.load_pem_private_key(fh.read(), password=None) + if _LEGACY_KEY_FILE.exists(): + with _LEGACY_KEY_FILE.open("rb") as fh: + return serialization.load_pem_private_key(fh.read(), password=None) + private_key = ed25519.Ed25519PrivateKey.generate() pem = private_key.private_bytes( encoding=serialization.Encoding.PEM, @@ -68,3 +78,23 @@ def _load_or_create() -> ed25519.Ed25519PrivateKey: return private_key + +def _migrate_legacy_material_if_present() -> None: + if not _LEGACY_KEY_FILE.exists() or _SIGNING_KEY_FILE.exists(): + return + + try: + ensure_runtime_dir("script_signing_keys") + try: + _LEGACY_KEY_FILE.replace(_SIGNING_KEY_FILE) + except Exception: + _SIGNING_KEY_FILE.write_bytes(_LEGACY_KEY_FILE.read_bytes()) + + if _LEGACY_PUB_FILE.exists() and not _SIGNING_PUB_FILE.exists(): + try: + _LEGACY_PUB_FILE.replace(_SIGNING_PUB_FILE) + except Exception: + _SIGNING_PUB_FILE.write_bytes(_LEGACY_PUB_FILE.read_bytes()) + except Exception: + return + diff --git a/Data/Server/Modules/runtime.py b/Data/Server/Modules/runtime.py new file mode 100644 index 0000000..822c994 --- /dev/null +++ b/Data/Server/Modules/runtime.py @@ -0,0 +1,78 @@ +"""Utility helpers for locating runtime storage paths. + +The Borealis repository keeps the authoritative source code under ``Data/`` +so that the bootstrap scripts can copy those assets into sibling ``Server/`` +and ``Agent/`` directories for execution. Runtime artefacts such as TLS +certificates or signing keys must therefore live outside ``Data`` to avoid +polluting the template tree. This module centralises the path selection so +other modules can rely on a consistent location regardless of whether they +are executed from the copied runtime directory or directly from ``Data`` +during development. +""" + +from __future__ import annotations + +import os +from functools import lru_cache +from pathlib import Path +from typing import Optional + + +def _env_path(name: str) -> Optional[Path]: + """Return a resolved ``Path`` for the given environment variable.""" + + value = os.environ.get(name) + if not value: + return None + try: + return Path(value).expanduser().resolve() + except Exception: + return None + + +@lru_cache(maxsize=None) +def project_root() -> Path: + """Best-effort detection of the repository root.""" + + env = _env_path("BOREALIS_PROJECT_ROOT") + if env: + return env + + current = Path(__file__).resolve() + for parent in current.parents: + if (parent / "Borealis.ps1").exists() or (parent / ".git").is_dir(): + return parent + + # Fallback to the ancestor that corresponds to ``/`` when the module + # lives under ``Data/Server/Modules``. + try: + return current.parents[4] + except IndexError: + return current.parent + + +@lru_cache(maxsize=None) +def server_runtime_root() -> Path: + """Location where the running server stores mutable artefacts.""" + + env = _env_path("BOREALIS_SERVER_ROOT") + if env: + return env + + root = project_root() + runtime = root / "Server" / "Borealis" + return runtime + + +def runtime_path(*parts: str) -> Path: + """Return a path relative to the server runtime root.""" + + return server_runtime_root().joinpath(*parts) + + +def ensure_runtime_dir(*parts: str) -> Path: + """Create (if required) and return a runtime directory.""" + + path = runtime_path(*parts) + path.mkdir(parents=True, exist_ok=True) + return path diff --git a/Data/Server/WebUI/vite.config.mts b/Data/Server/WebUI/vite.config.mts index c43aa0a..09be004 100644 --- a/Data/Server/WebUI/vite.config.mts +++ b/Data/Server/WebUI/vite.config.mts @@ -4,16 +4,20 @@ import react from '@vitejs/plugin-react'; import path from 'path'; import fs from 'fs'; +const runtimeCertDir = process.env.BOREALIS_CERT_DIR; + const certCandidates = [ process.env.BOREALIS_TLS_CERT, + runtimeCertDir && path.resolve(runtimeCertDir, 'borealis-server-cert.pem'), path.resolve(__dirname, '../certs/borealis-server-cert.pem'), - path.resolve(__dirname, '../../Data/Server/certs/borealis-server-cert.pem'), + path.resolve(__dirname, '../../../Server/Borealis/certs/borealis-server-cert.pem'), ] as const; const keyCandidates = [ process.env.BOREALIS_TLS_KEY, + runtimeCertDir && path.resolve(runtimeCertDir, 'borealis-server-key.pem'), path.resolve(__dirname, '../certs/borealis-server-key.pem'), - path.resolve(__dirname, '../../Data/Server/certs/borealis-server-key.pem'), + path.resolve(__dirname, '../../../Server/Borealis/certs/borealis-server-key.pem'), ] as const; const pickFirst = (candidates: readonly (string | undefined)[]) => { diff --git a/Data/Server/server.py b/Data/Server/server.py index f657bef..346562d 100644 --- a/Data/Server/server.py +++ b/Data/Server/server.py @@ -20,6 +20,8 @@ Section Guide: import os import sys +from pathlib import Path +import ssl # Ensure the modular server package is importable when the runtime is launched # from a packaged directory (e.g., Server/Borealis). We look for the canonical @@ -40,6 +42,36 @@ eventlet.monkey_patch(thread=False) from eventlet import tpool +try: + from eventlet.wsgi import HttpProtocol # type: ignore +except Exception: + HttpProtocol = None # type: ignore[assignment] +else: + _original_handle_one_request = HttpProtocol.handle_one_request + + def _quiet_tls_http_mismatch(self): # type: ignore[override] + try: + return _original_handle_one_request(self) + except ssl.SSLError as exc: # type: ignore[arg-type] + reason = getattr(exc, "reason", "") + reason_text = str(reason).lower() if reason else "" + message = " ".join(str(arg) for arg in exc.args if arg).lower() + if "http_request" in message or reason_text == "http request": + try: + self.close_connection = True # type: ignore[attr-defined] + except Exception: + pass + try: + conn = getattr(self, "socket", None) or getattr(self, "connection", None) + if conn: + conn.close() + except Exception: + pass + return None + raise + + HttpProtocol.handle_one_request = _quiet_tls_http_mismatch # type: ignore[assignment] + import requests import re import base64 @@ -167,6 +199,10 @@ TLS_CERT_PATH, TLS_KEY_PATH, TLS_BUNDLE_PATH = certificates.certificate_paths() os.environ.setdefault("BOREALIS_TLS_CERT", TLS_CERT_PATH) os.environ.setdefault("BOREALIS_TLS_KEY", TLS_KEY_PATH) os.environ.setdefault("BOREALIS_TLS_BUNDLE", TLS_BUNDLE_PATH) +try: + os.environ.setdefault("BOREALIS_CERT_DIR", str(Path(TLS_CERT_PATH).resolve().parent)) +except Exception: + pass JWT_SERVICE = jwt_service_module.load_service() SCRIPT_SIGNER = signing.load_signer() From f6c8f9b557861cf6c035d24ccb4b35054d1be3c6 Mon Sep 17 00:00:00 2001 From: Nicole Rappe Date: Fri, 17 Oct 2025 20:50:40 -0600 Subject: [PATCH 2/7] Fix duplicate settings helper causing agent import error --- Data/Agent/agent.py | 1 - 1 file changed, 1 deletion(-) diff --git a/Data/Agent/agent.py b/Data/Agent/agent.py index 5bd915a..3320b8b 100644 --- a/Data/Agent/agent.py +++ b/Data/Agent/agent.py @@ -1046,7 +1046,6 @@ def _collect_heartbeat_metrics() -> Dict[str, Any]: return metrics -def _settings_dir(): SERVER_CERT_PATH = _key_store().server_certificate_path() From bfc559e0bee0b47549363cd4cb29d7a1adcc798b Mon Sep 17 00:00:00 2001 From: Nicole Rappe Date: Fri, 17 Oct 2025 20:54:40 -0600 Subject: [PATCH 3/7] fix: include new agent runtime files in installer copy --- Borealis.ps1 | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/Borealis.ps1 b/Borealis.ps1 index 068955e..47d8502 100644 --- a/Borealis.ps1 +++ b/Borealis.ps1 @@ -784,13 +784,19 @@ function InstallOrUpdate-BorealisAgent { # Copy Agent Files to Virtual Python Environment $coreAgentFiles = @( - (Join-Path $agentSourceRoot 'agent.py'), (Join-Path $agentSourceRoot 'Python_API_Endpoints'), + (Join-Path $agentSourceRoot 'Roles'), + (Join-Path $agentSourceRoot 'Scripts'), (Join-Path $agentSourceRoot 'agent_deployment.py'), + (Join-Path $agentSourceRoot 'agent.py'), + (Join-Path $agentSourceRoot 'ansible-ee-version.txt'), (Join-Path $agentSourceRoot 'Borealis.ico'), + (Join-Path $agentSourceRoot 'fcntl_stub.py'), (Join-Path $agentSourceRoot 'launch_service.ps1'), (Join-Path $agentSourceRoot 'role_manager.py'), - (Join-Path $agentSourceRoot 'Roles') + (Join-Path $agentSourceRoot 'security.py'), + (Join-Path $agentSourceRoot 'sitecustomize.py'), + (Join-Path $agentSourceRoot 'termios_stub.py') ) Copy-Item $coreAgentFiles -Destination $agentDestinationFolder -Recurse -Force From a8ebff04a343031f8f092964f62775aba7087702 Mon Sep 17 00:00:00 2001 From: Nicole Rappe Date: Fri, 17 Oct 2025 20:58:26 -0600 Subject: [PATCH 4/7] fix: harden dpapi protect fallback --- Data/Agent/security.py | 28 ++++++++++++++++++++++++---- 1 file changed, 24 insertions(+), 4 deletions(-) diff --git a/Data/Agent/security.py b/Data/Agent/security.py index 890c005..475d1ee 100644 --- a/Data/Agent/security.py +++ b/Data/Agent/security.py @@ -42,8 +42,18 @@ def _protect(data: bytes, *, scope_system: bool) -> bytes: flags = 0 if scope_system: flags = getattr(win32crypt, "CRYPTPROTECT_LOCAL_MACHINE", 0x4) - protected = win32crypt.CryptProtectData(data, None, None, None, None, flags) # type: ignore[attr-defined] - return protected[1] + try: + protected = win32crypt.CryptProtectData(data, None, None, None, None, flags) # type: ignore[attr-defined] + except Exception: + return data + blob = protected[1] + if isinstance(blob, memoryview): + return blob.tobytes() + if isinstance(blob, bytearray): + return bytes(blob) + if isinstance(blob, bytes): + return blob + return data def _unprotect(data: bytes, *, scope_system: bool) -> bytes: @@ -52,8 +62,18 @@ def _unprotect(data: bytes, *, scope_system: bool) -> bytes: flags = 0 if scope_system: flags = getattr(win32crypt, "CRYPTPROTECT_LOCAL_MACHINE", 0x4) - unwrapped = win32crypt.CryptUnprotectData(data, None, None, None, None, flags) # type: ignore[attr-defined] - return unwrapped[1] + try: + unwrapped = win32crypt.CryptUnprotectData(data, None, None, None, None, flags) # type: ignore[attr-defined] + except Exception: + return data + blob = unwrapped[1] + if isinstance(blob, memoryview): + return blob.tobytes() + if isinstance(blob, bytearray): + return bytes(blob) + if isinstance(blob, bytes): + return blob + return data def _fingerprint_der(public_der: bytes) -> str: From b0d9b068d24d0aeadc7f0963ce4a2c79bb06e0ea Mon Sep 17 00:00:00 2001 From: Nicole Rappe Date: Fri, 17 Oct 2025 21:11:01 -0600 Subject: [PATCH 5/7] feat: allow installer code during agent deploy --- Borealis.ps1 | 72 +++++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 71 insertions(+), 1 deletion(-) diff --git a/Borealis.ps1 b/Borealis.ps1 index 47d8502..e272e04 100644 --- a/Borealis.ps1 +++ b/Borealis.ps1 @@ -8,7 +8,8 @@ param( [string]$AgentAction = '', [switch]$Vite, [switch]$Flask, - [switch]$Quick + [switch]$Quick, + [string]$InstallerCode = '' ) # Preselect menu choices from CLI args (optional) @@ -845,6 +846,7 @@ function InstallOrUpdate-BorealisAgent { $oldSettingsDir = Join-Path $scriptDir 'Agent\Settings' if (-not (Test-Path $settingsDir)) { New-Item -Path $settingsDir -ItemType Directory -Force | Out-Null } $serverUrlPath = Join-Path $settingsDir 'server_url.txt' + $configPath = Join-Path $settingsDir 'agent_settings.json' # Migrate any prior interim location file if present $oldServerUrlPath = Join-Path $oldSettingsDir 'server_url.txt' if (-not (Test-Path $serverUrlPath) -and (Test-Path $oldServerUrlPath)) { @@ -868,6 +870,74 @@ function InstallOrUpdate-BorealisAgent { # Write UTF-8 without BOM to avoid BOM being read into the URL $utf8NoBom = New-Object System.Text.UTF8Encoding($false) [System.IO.File]::WriteAllText($serverUrlPath, $inputUrl, $utf8NoBom) + + $configDefaults = [ordered]@{ + config_file_watcher_interval = 2 + agent_id = '' + regions = @{} + installer_code = '' + } + $config = [ordered]@{} + foreach ($entry in $configDefaults.GetEnumerator()) { + $config[$entry.Key] = $entry.Value + } + if (Test-Path $configPath) { + try { + $existingRaw = Get-Content -Path $configPath -Raw -ErrorAction Stop + if ($existingRaw -and $existingRaw.Trim()) { + $existingJson = $existingRaw | ConvertFrom-Json -ErrorAction Stop + foreach ($prop in $existingJson.PSObject.Properties) { + $config[$prop.Name] = $prop.Value + } + } + } catch { + Write-AgentLog -FileName 'Install.log' -Message ("[CONFIG] Failed to parse agent_settings.json: {0}" -f $_.Exception.Message) + } + } + + if ('regions' -notin $config.Keys -or $null -eq $config['regions']) { + $config['regions'] = @{} + } + + $existingInstallerCode = '' + if ('installer_code' -in $config.Keys -and $null -ne $config['installer_code']) { + $existingInstallerCode = [string]$config['installer_code'] + } + + $providedInstallerCode = '' + if ($InstallerCode -and $InstallerCode.Trim()) { + $providedInstallerCode = $InstallerCode.Trim() + } elseif ($env:BOREALIS_INSTALLER_CODE -and $env:BOREALIS_INSTALLER_CODE.Trim()) { + $providedInstallerCode = $env:BOREALIS_INSTALLER_CODE.Trim() + } + + if (-not $providedInstallerCode) { + $defaultDisplay = if ($existingInstallerCode) { $existingInstallerCode } else { '' } + Write-Host ""; Write-Host "Optional: set an installer code for agent enrollment." -ForegroundColor DarkYellow + $inputCode = Read-Host ("Installer Code [{0}]" -f $defaultDisplay) + if ($inputCode -and $inputCode.Trim()) { + $providedInstallerCode = $inputCode.Trim() + } elseif ($defaultDisplay) { + $providedInstallerCode = $defaultDisplay + } else { + $providedInstallerCode = '' + } + } + + $config['installer_code'] = $providedInstallerCode + + try { + $configJson = $config | ConvertTo-Json -Depth 10 + [System.IO.File]::WriteAllText($configPath, $configJson, $utf8NoBom) + if ($providedInstallerCode) { + Write-Host "Installer code saved to agent_settings.json." -ForegroundColor Green + } else { + Write-Host "Installer code cleared in agent_settings.json." -ForegroundColor Yellow + } + } catch { + Write-AgentLog -FileName 'Install.log' -Message ("[CONFIG] Failed to persist agent_settings.json: {0}" -f $_.Exception.Message) + Write-Host "Failed to update agent_settings.json. Check Logs/Agent/install.log for details." -ForegroundColor Red + } } Write-Host "`nConfiguring Borealis Agent (tasks)..." -ForegroundColor Blue From 751c58d2b9b85b2ebcd3ab77a0ad975f5bd0787d Mon Sep 17 00:00:00 2001 From: Nicole Rappe Date: Fri, 17 Oct 2025 21:35:50 -0600 Subject: [PATCH 6/7] fix: respect websocket ssl context --- Data/Agent/agent.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Data/Agent/agent.py b/Data/Agent/agent.py index 3320b8b..30b6c93 100644 --- a/Data/Agent/agent.py +++ b/Data/Agent/agent.py @@ -2039,11 +2039,12 @@ async def connect_loop(): url = client.websocket_base_url() print(f"[INFO] Connecting Agent to {url}...") _log_agent(f'Connecting to {url}...') + ws_kwargs = client.websocket_kwargs() await sio.connect( url, transports=['websocket'], headers=client.auth_headers(), - ssl_verify=client.session.verify, + **ws_kwargs, ) break except Exception as e: From 0b391609aed023b8c84e933e1e59d455142a9bdd Mon Sep 17 00:00:00 2001 From: Nicole Rappe Date: Fri, 17 Oct 2025 21:48:18 -0600 Subject: [PATCH 7/7] fix: align socket.io tls verification --- Data/Agent/agent.py | 34 +++++++++++++++++----------------- 1 file changed, 17 insertions(+), 17 deletions(-) diff --git a/Data/Agent/agent.py b/Data/Agent/agent.py index 30b6c93..3a99f7b 100644 --- a/Data/Agent/agent.py +++ b/Data/Agent/agent.py @@ -533,21 +533,22 @@ class AgentHttpClient: return {"Authorization": f"Bearer {self.access_token}"} return {} - def websocket_kwargs(self) -> Dict[str, Any]: - kwargs: Dict[str, Any] = {} - verify = getattr(self.session, "verify", True) - if isinstance(verify, str) and os.path.isfile(verify): - try: - ctx = ssl.create_default_context(cafile=verify) - kwargs["ssl"] = ctx - except Exception: - pass - elif verify is False: - try: - kwargs["ssl"] = ssl._create_unverified_context() - except Exception: - pass - return kwargs + def configure_socketio(self, client: "socketio.AsyncClient") -> None: + """Align the Socket.IO engine's TLS verification with the REST client.""" + try: + verify = getattr(self.session, "verify", True) + engine = getattr(client, "eio", None) + if engine is None: + return + # python-engineio accepts bool, path, or ssl.SSLContext for ssl_verify + if isinstance(verify, str) and os.path.isfile(verify): + engine.ssl_verify = verify + elif verify is False: + engine.ssl_verify = False + else: + engine.ssl_verify = True + except Exception: + pass # ------------------------------------------------------------------ # Enrollment & token management @@ -2036,15 +2037,14 @@ async def connect_loop(): while True: try: client.ensure_authenticated() + client.configure_socketio(sio) url = client.websocket_base_url() print(f"[INFO] Connecting Agent to {url}...") _log_agent(f'Connecting to {url}...') - ws_kwargs = client.websocket_kwargs() await sio.connect( url, transports=['websocket'], headers=client.auth_headers(), - **ws_kwargs, ) break except Exception as e: