Optimized and Cleaned-Up Agent Configuration Files

This commit is contained in:
2025-09-27 22:37:55 -06:00
parent 91aafc305d
commit 71a0d153cf
7 changed files with 148 additions and 54 deletions

View File

@@ -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.

View File

@@ -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

View File

@@ -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()),

View File

@@ -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()

View File

@@ -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 users 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(

View File

@@ -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"
}

View File

@@ -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"
}