mirror of
https://github.com/bunny-lab-io/Borealis.git
synced 2025-10-26 15:21:57 -06:00
Optimized and Cleaned-Up Agent Configuration Files
This commit is contained in:
10
AGENTS.md
10
AGENTS.md
@@ -69,9 +69,9 @@ All runtime logs live under `Logs/<ServiceName>` relative to the project root (`
|
||||
- This avoids 0x1/0x2 Task Scheduler errors on hosts where WorkingDirectory is ignored.
|
||||
- UserHelper (interactive) is still a direct task action to `pythonw.exe "Agent\\Borealis\\agent.py" --config user`.
|
||||
- Config files and inheritance:
|
||||
- Base config lives at `<ProjectRoot>\\agent_settings.json`.
|
||||
- On first run per-suffix, the agent seeds: `agent_settings_svc.json` (SYSTEM), `agent_settings_user.json` (interactive) from the base.
|
||||
- Set `borealis_server_url` in the base file so both inherit the same server endpoint.
|
||||
- Base config now lives at `<ProjectRoot>\\Agent\\Borealis\\Settings\\agent_settings.json`.
|
||||
- On first run per-suffix, the agent seeds: `Agent\\Borealis\\Settings\\agent_settings_svc.json` (SYSTEM) and `Agent\\Borealis\\Settings\\agent_settings_user.json` (interactive) from the base when present.
|
||||
- Server URL is stored in `<ProjectRoot>\\Agent\\Borealis\\Settings\\server_url.txt`. The deployment script prompts for it on install/repair; press Enter to accept the default `http://localhost:5000`.
|
||||
- Logging:
|
||||
- Early bootstrap log: `<ProjectRoot>\\Logs\\Agent\\bootstrap.log` (helps verify launch + mode).
|
||||
- Main logs: `<ProjectRoot>\\Logs\\Agent\\agent.log`, `agent.error.log`.
|
||||
@@ -85,12 +85,12 @@ All runtime logs live under `Logs/<ServiceName>` relative to the project root (`
|
||||
- Launch/repair agent (elevated PowerShell): `.\\Borealis.ps1 -Agent -AgentAction install`.
|
||||
- Manual short-run agent checks (non-blocking):
|
||||
- `Start-Process .\\Agent\\Scripts\\pythonw.exe -ArgumentList '".\\Agent\\Borealis\\agent.py" --system-service --config svc'`
|
||||
- Verify logs under `Logs\\Agent` and presence of `agent_settings_svc.json`.
|
||||
- Verify logs under `Logs\\Agent` and presence of `Agent\\Borealis\\Settings\\agent_settings_svc.json` and `Agent\\Borealis\\Settings\\server_url.txt`.
|
||||
|
||||
## Troubleshooting Checklist
|
||||
- Agent task “Ready” with 0x1: ensure the SYSTEM task uses `launch_service.ps1` and that WorkingDirectory is `Agent\\Borealis`.
|
||||
- No logs/configs created: verify venv exists under `Agent\\Scripts` and that wrapper points at the right paths.
|
||||
- Agent connects but Devices empty: check `agent.error.log` for aiohttp errors and confirm `borealis_server_url` is reachable; device details post occurs once on connect and then every ~5 minutes.
|
||||
- Agent connects but Devices empty: check `agent.error.log` for aiohttp errors and confirm the URL in `Agent\\Borealis\\Settings\\server_url.txt` is reachable; device details post occurs once on connect and then every ~5 minutes.
|
||||
- Quick jobs “Running” forever: ensure SYSTEM and UserHelper agents are both running; check `system_last.ps1` and wrapper logs for PowerShell errors.
|
||||
## State & Persistence
|
||||
`database.db` currently stores device inventory, runtime facts, and job history. Workflow and scheduling metadata are not yet persisted, and no internal scheduler exists beyond WebUI prototypes. Planned scheduling work will need schema updates and migration guidance once implemented.
|
||||
|
||||
25
Borealis.ps1
25
Borealis.ps1
@@ -481,6 +481,31 @@ function InstallOrUpdate-BorealisAgent {
|
||||
}
|
||||
}
|
||||
|
||||
Run-Step "Configure Agent Settings" {
|
||||
$settingsDir = Join-Path $scriptDir 'Agent\Borealis\Settings'
|
||||
$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'
|
||||
# Migrate any prior interim location file if present
|
||||
$oldServerUrlPath = Join-Path $oldSettingsDir 'server_url.txt'
|
||||
if (-not (Test-Path $serverUrlPath) -and (Test-Path $oldServerUrlPath)) {
|
||||
try { Move-Item -Path $oldServerUrlPath -Destination $serverUrlPath -Force } catch { try { Copy-Item $oldServerUrlPath $serverUrlPath -Force } catch {} }
|
||||
}
|
||||
$defaultUrl = 'http://localhost:5000'
|
||||
$currentUrl = $defaultUrl
|
||||
if (Test-Path $serverUrlPath) {
|
||||
try { $txt = (Get-Content -Path $serverUrlPath -ErrorAction SilentlyContinue | Select-Object -First 1) } catch { $txt = '' }
|
||||
if ($txt -and $txt.Trim()) { $currentUrl = $txt.Trim() }
|
||||
}
|
||||
Write-Host ""; Write-Host "Set Borealis Server URL" -ForegroundColor DarkYellow
|
||||
$prompt = "Server URL [$currentUrl]"
|
||||
$inputUrl = Read-Host $prompt
|
||||
if (-not $inputUrl) { $inputUrl = $currentUrl }
|
||||
$inputUrl = $inputUrl.Trim()
|
||||
if (-not $inputUrl) { $inputUrl = $defaultUrl }
|
||||
$inputUrl | Out-File -FilePath $serverUrlPath -Encoding utf8 -Force
|
||||
}
|
||||
|
||||
Write-Host "`nConfiguring Borealis Agent (tasks)..." -ForegroundColor Blue
|
||||
Write-Host "===================================================================================="
|
||||
Ensure-AgentTasks -ScriptRoot $scriptDir
|
||||
|
||||
@@ -117,7 +117,7 @@ def collect_summary(CONFIG):
|
||||
hostname = socket.gethostname()
|
||||
return {
|
||||
'hostname': hostname,
|
||||
'os': CONFIG.data.get('agent_operating_system', detect_agent_os()),
|
||||
'os': detect_agent_os(),
|
||||
'username': os.environ.get('USERNAME') or os.environ.get('USER') or '',
|
||||
'domain': os.environ.get('USERDOMAIN') or '',
|
||||
'uptime_sec': int(time.time() - psutil.boot_time()) if psutil else None,
|
||||
@@ -662,12 +662,7 @@ class Role:
|
||||
self._ext_ip_ts = 0
|
||||
self._refresh_ts = 0
|
||||
self._last_details = None
|
||||
try:
|
||||
# Set OS string once
|
||||
self.ctx.config.data['agent_operating_system'] = detect_agent_os()
|
||||
self.ctx.config._write()
|
||||
except Exception:
|
||||
pass
|
||||
# OS is collected dynamically; do not persist in config
|
||||
# Start periodic reporter
|
||||
try:
|
||||
self.task = self.ctx.loop.create_task(self._report_loop())
|
||||
@@ -708,7 +703,8 @@ class Role:
|
||||
|
||||
# Always post the latest available details (possibly cached)
|
||||
details_to_send = self._last_details or {'summary': collect_summary(self.ctx.config)}
|
||||
url = (self.ctx.config.data.get('borealis_server_url', 'http://localhost:5000') or '').rstrip('/') + '/api/agent/details'
|
||||
get_url = (self.ctx.hooks.get('get_server_url') if isinstance(self.ctx.hooks, dict) else None) or (lambda: 'http://localhost:5000')
|
||||
url = (get_url() or '').rstrip('/') + '/api/agent/details'
|
||||
payload = {
|
||||
'agent_id': self.ctx.agent_id,
|
||||
'hostname': details_to_send.get('summary', {}).get('hostname', socket.gethostname()),
|
||||
|
||||
@@ -300,7 +300,9 @@ class Role:
|
||||
|
||||
interval = cfg.get('interval', 1000) / 1000.0
|
||||
loop = asyncio.get_event_loop()
|
||||
executor = concurrent.futures.ThreadPoolExecutor(max_workers=self.ctx.config.data.get('max_task_workers', 8))
|
||||
# Maximum number of screenshot roles you can assign to an agent. (8 already feels overkill)
|
||||
executor = concurrent.futures.ThreadPoolExecutor(max_workers=8)
|
||||
|
||||
try:
|
||||
while True:
|
||||
x, y, w, h = overlay_widgets[nid].get_geometry()
|
||||
@@ -319,4 +321,3 @@ class Role:
|
||||
pass
|
||||
except Exception:
|
||||
traceback.print_exc()
|
||||
|
||||
|
||||
@@ -127,10 +127,14 @@ def _log_agent(message: str, fname: str = 'agent.log'):
|
||||
|
||||
def _resolve_config_path():
|
||||
"""
|
||||
Decide where to store agent_settings.json, per user’s requirement:
|
||||
- Prefer env var BOREALIS_AGENT_CONFIG (full file path)
|
||||
- Else use <ProjectRoot> alongside Borealis.ps1 and users.json
|
||||
- Migrate from legacy locations if found (user config dir or next to this script)
|
||||
Resolve the path for agent settings json in the centralized location:
|
||||
<ProjectRoot>/Agent/Borealis/Settings/agent_settings[_{suffix}].json
|
||||
|
||||
Precedence/order:
|
||||
- If BOREALIS_AGENT_CONFIG is set (full path), use it.
|
||||
- Else if BOREALIS_AGENT_CONFIG_DIR is set (dir), use agent_settings.json under it.
|
||||
- Else use <ProjectRoot>/Agent/Borealis/Settings and migrate any legacy files into it.
|
||||
- If suffix is provided, seed from base if present.
|
||||
"""
|
||||
# Full file path override
|
||||
override_file = os.environ.get("BOREALIS_AGENT_CONFIG")
|
||||
@@ -146,8 +150,14 @@ def _resolve_config_path():
|
||||
os.makedirs(override_dir, exist_ok=True)
|
||||
return os.path.join(override_dir, "agent_settings.json")
|
||||
|
||||
# Target config in project root
|
||||
project_root = _find_project_root()
|
||||
settings_dir = os.path.join(project_root, 'Agent', 'Borealis', 'Settings')
|
||||
try:
|
||||
os.makedirs(settings_dir, exist_ok=True)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# Determine filename with optional suffix
|
||||
cfg_basename = 'agent_settings.json'
|
||||
try:
|
||||
if CONFIG_NAME_SUFFIX:
|
||||
@@ -156,23 +166,53 @@ def _resolve_config_path():
|
||||
cfg_basename = f"agent_settings_{suffix}.json"
|
||||
except Exception:
|
||||
pass
|
||||
cfg_path = os.path.join(project_root, cfg_basename)
|
||||
|
||||
cfg_path = os.path.join(settings_dir, cfg_basename)
|
||||
if os.path.exists(cfg_path):
|
||||
return cfg_path
|
||||
|
||||
# If using a suffixed config and there is a base config in the project root, seed from it
|
||||
# If using a suffixed config and there is a base config (new or legacy), seed from it
|
||||
try:
|
||||
if CONFIG_NAME_SUFFIX:
|
||||
base_cfg = os.path.join(project_root, 'agent_settings.json')
|
||||
if os.path.exists(base_cfg):
|
||||
base_new = os.path.join(settings_dir, 'agent_settings.json')
|
||||
base_old_settings = os.path.join(project_root, 'Agent', 'Settings', 'agent_settings.json')
|
||||
base_legacy = os.path.join(project_root, 'agent_settings.json')
|
||||
seed_from = None
|
||||
if os.path.exists(base_new):
|
||||
seed_from = base_new
|
||||
elif os.path.exists(base_old_settings):
|
||||
seed_from = base_old_settings
|
||||
elif os.path.exists(base_legacy):
|
||||
seed_from = base_legacy
|
||||
if seed_from:
|
||||
try:
|
||||
shutil.copy2(base_cfg, cfg_path)
|
||||
shutil.copy2(seed_from, cfg_path)
|
||||
return cfg_path
|
||||
except Exception:
|
||||
pass
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# Migrate legacy root configs or prior Agent/Settings into Agent/Borealis/Settings
|
||||
try:
|
||||
legacy_root = os.path.join(project_root, cfg_basename)
|
||||
old_settings_dir = os.path.join(project_root, 'Agent', 'Settings')
|
||||
legacy_old_settings = os.path.join(old_settings_dir, cfg_basename)
|
||||
if os.path.exists(legacy_root):
|
||||
try:
|
||||
shutil.move(legacy_root, cfg_path)
|
||||
except Exception:
|
||||
shutil.copy2(legacy_root, cfg_path)
|
||||
return cfg_path
|
||||
if os.path.exists(legacy_old_settings):
|
||||
try:
|
||||
shutil.move(legacy_old_settings, cfg_path)
|
||||
except Exception:
|
||||
shutil.copy2(legacy_old_settings, cfg_path)
|
||||
return cfg_path
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# Migration: from legacy user dir or script dir
|
||||
legacy_user = _user_config_default_path()
|
||||
legacy_script_dir = os.path.join(os.path.dirname(__file__), "agent_settings.json")
|
||||
@@ -187,13 +227,11 @@ def _resolve_config_path():
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# Nothing to migrate; return desired path in root
|
||||
# Nothing to migrate; return desired path in new Settings dir
|
||||
return cfg_path
|
||||
|
||||
CONFIG_PATH = _resolve_config_path()
|
||||
DEFAULT_CONFIG = {
|
||||
"borealis_server_url": "http://localhost:5000",
|
||||
"max_task_workers": 8,
|
||||
"config_file_watcher_interval": 2,
|
||||
"agent_id": "",
|
||||
"regions": {}
|
||||
@@ -216,6 +254,15 @@ class ConfigManager:
|
||||
with open(self.path, 'r') as f:
|
||||
loaded = json.load(f)
|
||||
self.data = {**DEFAULT_CONFIG, **loaded}
|
||||
# Strip deprecated/relocated fields
|
||||
for k in ('borealis_server_url','max_task_workers','agent_operating_system','created'):
|
||||
if k in self.data:
|
||||
self.data.pop(k, None)
|
||||
# persist cleanup best-effort
|
||||
try:
|
||||
self._write()
|
||||
except Exception:
|
||||
pass
|
||||
except Exception as e:
|
||||
print(f"[WARN] Failed to parse config: {e}")
|
||||
self.data = DEFAULT_CONFIG.copy()
|
||||
@@ -418,8 +465,51 @@ def detect_agent_os():
|
||||
print(f"[WARN] OS detection failed: {e}")
|
||||
return "Unknown"
|
||||
|
||||
CONFIG.data['agent_operating_system'] = detect_agent_os()
|
||||
CONFIG._write()
|
||||
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'))
|
||||
|
||||
def get_server_url() -> str:
|
||||
"""Return the Borealis server URL from env or Agent/Borealis/Settings/server_url.txt.
|
||||
Fallback to http://localhost:5000 if missing/empty.
|
||||
"""
|
||||
try:
|
||||
env_url = os.environ.get('BOREALIS_SERVER_URL')
|
||||
if env_url and env_url.strip():
|
||||
return env_url.strip()
|
||||
# New location
|
||||
path = os.path.join(_settings_dir(), 'server_url.txt')
|
||||
if os.path.isfile(path):
|
||||
try:
|
||||
with open(path, 'r', encoding='utf-8') as f:
|
||||
txt = (f.read() or '').strip()
|
||||
if txt:
|
||||
return txt
|
||||
except Exception:
|
||||
pass
|
||||
# Prior interim location (Agent/Settings) migration support
|
||||
try:
|
||||
project_root = _find_project_root()
|
||||
old_path = os.path.join(project_root, 'Agent', 'Settings', 'server_url.txt')
|
||||
if os.path.isfile(old_path):
|
||||
with open(old_path, 'r', encoding='utf-8') as f:
|
||||
txt = (f.read() or '').strip()
|
||||
if txt:
|
||||
# Best-effort copy forward to new location so future reads use it
|
||||
try:
|
||||
os.makedirs(_settings_dir(), exist_ok=True)
|
||||
with open(path, 'w', encoding='utf-8') as wf:
|
||||
wf.write(txt)
|
||||
except Exception:
|
||||
pass
|
||||
return txt
|
||||
except Exception:
|
||||
pass
|
||||
except Exception:
|
||||
pass
|
||||
return 'http://localhost:5000'
|
||||
|
||||
# //////////////////////////////////////////////////////////////////////////
|
||||
# CORE SECTION: ASYNC TASK / WEBSOCKET
|
||||
@@ -656,7 +746,7 @@ async def send_heartbeat():
|
||||
payload = {
|
||||
"agent_id": AGENT_ID,
|
||||
"hostname": socket.gethostname(),
|
||||
"agent_operating_system": CONFIG.data.get("agent_operating_system", detect_agent_os()),
|
||||
"agent_operating_system": detect_agent_os(),
|
||||
"last_seen": int(time.time())
|
||||
}
|
||||
await sio.emit("agent_heartbeat", payload)
|
||||
@@ -962,7 +1052,7 @@ async def send_agent_details():
|
||||
"storage": collect_storage(),
|
||||
"network": collect_network(),
|
||||
}
|
||||
url = CONFIG.data.get("borealis_server_url", "http://localhost:5000") + "/api/agent/details"
|
||||
url = get_server_url().rstrip('/') + "/api/agent/details"
|
||||
payload = {
|
||||
"agent_id": AGENT_ID,
|
||||
"hostname": details.get("summary", {}).get("hostname", socket.gethostname()),
|
||||
@@ -985,7 +1075,7 @@ async def send_agent_details_once():
|
||||
"storage": collect_storage(),
|
||||
"network": collect_network(),
|
||||
}
|
||||
url = CONFIG.data.get("borealis_server_url", "http://localhost:5000") + "/api/agent/details"
|
||||
url = get_server_url().rstrip('/') + "/api/agent/details"
|
||||
payload = {
|
||||
"agent_id": AGENT_ID,
|
||||
"hostname": details.get("summary", {}).get("hostname", socket.gethostname()),
|
||||
@@ -1008,7 +1098,7 @@ async def connect():
|
||||
await sio.emit("agent_heartbeat", {
|
||||
"agent_id": AGENT_ID,
|
||||
"hostname": socket.gethostname(),
|
||||
"agent_operating_system": CONFIG.data.get("agent_operating_system", detect_agent_os()),
|
||||
"agent_operating_system": detect_agent_os(),
|
||||
"last_seen": int(time.time())
|
||||
})
|
||||
except Exception as e:
|
||||
@@ -1285,7 +1375,7 @@ async def connect_loop():
|
||||
retry=5
|
||||
while True:
|
||||
try:
|
||||
url=CONFIG.data.get('borealis_server_url',"http://localhost:5000")
|
||||
url=get_server_url()
|
||||
print(f"[INFO] Connecting Agent to {url}...")
|
||||
_log_agent(f'Connecting to {url}...')
|
||||
await sio.connect(url,transports=['websocket'])
|
||||
@@ -1316,7 +1406,7 @@ if __name__=='__main__':
|
||||
# Initialize roles context for role tasks
|
||||
# Initialize role manager and hot-load roles from Roles/
|
||||
try:
|
||||
hooks = {'send_service_control': send_service_control}
|
||||
hooks = {'send_service_control': send_service_control, 'get_server_url': get_server_url}
|
||||
if not SYSTEM_SERVICE_MODE:
|
||||
# Load interactive-context roles (tray/UI, current-user execution, screenshot, etc.)
|
||||
ROLE_MANAGER = RoleManager(
|
||||
|
||||
@@ -1,9 +0,0 @@
|
||||
{
|
||||
"borealis_server_url": "http://localhost:5000",
|
||||
"max_task_workers": 8,
|
||||
"config_file_watcher_interval": 2,
|
||||
"agent_id": "lab-operator-01-agent-66ba3ad3",
|
||||
"regions": {},
|
||||
"agent_operating_system": "Microsoft Windows 11 Pro 24H2 Build 26100.6584",
|
||||
"created": "2025-09-02 23:57:00"
|
||||
}
|
||||
@@ -1,9 +0,0 @@
|
||||
{
|
||||
"borealis_server_url": "http://localhost:5000",
|
||||
"max_task_workers": 8,
|
||||
"config_file_watcher_interval": 2,
|
||||
"agent_id": "lab-operator-01-agent-66ba3ad3",
|
||||
"regions": {},
|
||||
"agent_operating_system": "Windows 11 Pro 24H2 Build 26100.6584",
|
||||
"created": "2025-09-02 23:57:00"
|
||||
}
|
||||
Reference in New Issue
Block a user