diff --git a/AGENTS.md b/AGENTS.md index 21d02ed..e406e78 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -96,12 +96,12 @@ All runtime logs live under `Logs/` relative to the project root (` ## New: Agent Launch Model, Tasks, and Logging - SYSTEM mode is launched via a wrapper to guarantee WorkingDirectory and capture stdout/stderr: - `Agent\\Borealis\\launch_service.ps1` is registered as the scheduled task action for the SYSTEM agent. - - The wrapper runs `Agent\\Scripts\\pythonw.exe Agent\\Borealis\\agent.py --system-service --config svc` with `Set-Location` to `Agent\\Borealis` and redirects output to `%ProgramData%\\Borealis\\svc.out.log` and `svc.err.log`. + - The wrapper runs `Agent\\Scripts\\pythonw.exe Agent\\Borealis\\agent.py --system-service --config SYSTEM` with `Set-Location` to `Agent\\Borealis` and redirects output to `%ProgramData%\\Borealis\\svc.out.log` and `svc.err.log`. - 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`. +- UserHelper (interactive) is still a direct task action to `pythonw.exe "Agent\\Borealis\\agent.py" --config CURRENTUSER`. - Config files and inheritance: - Base config now lives at `\\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. +- On first run per-suffix, the agent seeds: `Agent\\Borealis\\Settings\\agent_settings_SYSTEM.json` (SYSTEM) and `Agent\\Borealis\\Settings\\agent_settings_CURRENTUSER.json` (interactive) from the base when present. - Server URL is stored in `\\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: `\\Logs\\Agent\\bootstrap.log` (helps verify launch + mode). @@ -115,8 +115,8 @@ All runtime logs live under `Logs/` relative to the project root (` - Dev UI separately (if needed): `cd Server\\web-interface && npm run dev`. - 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\\Borealis\\Settings\\agent_settings_svc.json` and `Agent\\Borealis\\Settings\\server_url.txt`. + - `Start-Process .\\Agent\\Scripts\\pythonw.exe -ArgumentList '".\\Agent\\Borealis\\agent.py" --system-service --config SYSTEM'` + - Verify logs under `Logs\\Agent` and presence of `Agent\\Borealis\\Settings\\agent_settings_SYSTEM.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`. diff --git a/Borealis.ps1 b/Borealis.ps1 index 78cdbc7..7e5c57a 100644 --- a/Borealis.ps1 +++ b/Borealis.ps1 @@ -715,7 +715,7 @@ function Ensure-AgentTasks { # Optional user-session helper for interactive roles (tray, overlays) $helperName = 'Borealis Agent (UserHelper)' - $usrArg = ('"{0}" --config user' -f $agentPy) + $usrArg = ('"{0}" --config CURRENTUSER' -f $agentPy) $usrAction = New-ScheduledTaskAction -Execute $pyw -Argument $usrArg -WorkingDirectory (Split-Path $agentPy -Parent) $usrTrig = New-ScheduledTaskTrigger -AtLogOn $usrSet = New-ScheduledTaskSettingsSet -Hidden -RestartCount 3 -RestartInterval (New-TimeSpan -Minutes 1) -ExecutionTimeLimit ([TimeSpan]::Zero) diff --git a/Borealis.sh b/Borealis.sh index 6122d52..97e1f1c 100644 --- a/Borealis.sh +++ b/Borealis.sh @@ -215,7 +215,7 @@ cd "$ROOT_DIR" LOG_DIR="$(cd -- "$ROOT_DIR/../../Logs/Agent" && pwd 2>/dev/null || echo "$ROOT_DIR/../../Logs/Agent")" mkdir -p "$LOG_DIR" PY_BIN="${ROOT_DIR}/../bin/python3" -exec "$PY_BIN" "$ROOT_DIR/agent.py" --system-service --config svc >>"$LOG_DIR/svc.out.log" 2>>"$LOG_DIR/svc.err.log" +exec "$PY_BIN" "$ROOT_DIR/agent.py" --system-service --config SYSTEM >>"$LOG_DIR/svc.out.log" 2>>"$LOG_DIR/svc.err.log" SH chmod +x "${agentDest}/launch_service.sh" @@ -229,7 +229,7 @@ ensure_agent_tasks() { # Register @reboot cron entries for system (root) and current user local agentDest="${SCRIPT_DIR}/Agent/Borealis" local sys_cmd="bash '${agentDest}/launch_service.sh'" - local user_cmd="bash -lc 'cd "${agentDest}" && "${SCRIPT_DIR}/Agent/bin/python3" ./agent.py --config user'" + local user_cmd="bash -lc 'cd "${agentDest}" && "${SCRIPT_DIR}/Agent/bin/python3" ./agent.py --config CURRENTUSER'" # Root/system entry if need_sudo; then @@ -261,7 +261,7 @@ remove_agent() { log_agent "=== Removal start ===" Removal.log kill_agent_processes || true local sys_cmd="bash '${SCRIPT_DIR}/Agent/Borealis/launch_service.sh'" - local user_cmd="bash -lc 'cd "${SCRIPT_DIR}/Agent/Borealis" && "${SCRIPT_DIR}/Agent/bin/python3" ./agent.py --config user'" + local user_cmd="bash -lc 'cd "${SCRIPT_DIR}/Agent/Borealis" && "${SCRIPT_DIR}/Agent/bin/python3" ./agent.py --config CURRENTUSER'" remove_cron_entries "$sys_cmd" "$user_cmd" || true rm -rf "${SCRIPT_DIR}/Agent" || true log_agent "=== Removal end ===" Removal.log @@ -271,7 +271,7 @@ launch_user_helper_now() { local py="${SCRIPT_DIR}/Agent/bin/python3" local helper="${SCRIPT_DIR}/Agent/Borealis/agent.py" if [[ -x "$py" && -f "$helper" ]]; then - (cd "${SCRIPT_DIR}/Agent/Borealis" && nohup "$py" -W ignore::SyntaxWarning "$helper" --config user >/dev/null 2>&1 & ) + (cd "${SCRIPT_DIR}/Agent/Borealis" && nohup "$py" -W ignore::SyntaxWarning "$helper" --config CURRENTUSER >/dev/null 2>&1 & ) echo -e "${GREEN}Launched user-session helper.${RESET}" else echo -e "${YELLOW}Agent venv or helper missing; run install first.${RESET}" diff --git a/Data/Agent/agent.py b/Data/Agent/agent.py index 53afe21..36401c2 100644 --- a/Data/Agent/agent.py +++ b/Data/Agent/agent.py @@ -85,6 +85,58 @@ def _argv_get(flag: str, default: str = None): return default CONFIG_NAME_SUFFIX = _argv_get('--config', None) + +def _canonical_config_suffix(raw_suffix: str) -> str: + try: + if not raw_suffix: + return '' + value = str(raw_suffix).strip() + if not value: + return '' + normalized = value.lower() + if normalized in ('svc', 'system', 'system_service', 'service'): + return 'SYSTEM' + if normalized in ('user', 'currentuser', 'interactive'): + return 'CURRENTUSER' + sanitized = ''.join(ch for ch in value if ch.isalnum() or ch in ('_', '-')).strip('_-') + return sanitized + except Exception: + return '' + + +CONFIG_SUFFIX_CANONICAL = _canonical_config_suffix(CONFIG_NAME_SUFFIX) + + +def _agent_guid_path() -> str: + try: + root = _find_project_root() + return os.path.join(root, 'Agent', 'Borealis', 'agent_GUID') + except Exception: + return os.path.abspath(os.path.join(os.path.dirname(__file__), 'agent_GUID')) + + +def _persist_agent_guid_local(guid: str): + guid = _normalize_agent_guid(guid) + if not guid: + return + path = _agent_guid_path() + try: + directory = os.path.dirname(path) + if directory: + os.makedirs(directory, exist_ok=True) + existing = '' + if os.path.isfile(path): + try: + with open(path, 'r', encoding='utf-8') as fh: + existing = fh.read().strip() + except Exception: + existing = '' + if existing != guid: + with open(path, 'w', encoding='utf-8') as fh: + fh.write(guid) + except Exception as exc: + _log_agent(f'Failed to persist agent GUID locally: {exc}', fname='agent.error.log') + if not SYSTEM_SERVICE_MODE: # Reduce noisy Qt output and attempt to avoid Windows OleInitialize warnings os.environ.setdefault("QT_LOGGING_RULES", "qt.qpa.*=false;*.debug=false") @@ -224,21 +276,41 @@ def _resolve_config_path(): # Determine filename with optional suffix cfg_basename = 'agent_settings.json' - try: - if CONFIG_NAME_SUFFIX: - suffix = ''.join(ch for ch in CONFIG_NAME_SUFFIX if ch.isalnum() or ch in ('_', '-')).strip() - if suffix: - cfg_basename = f"agent_settings_{suffix}.json" - except Exception: - pass + suffix = CONFIG_SUFFIX_CANONICAL + if suffix: + cfg_basename = f"agent_settings_{suffix}.json" cfg_path = os.path.join(settings_dir, cfg_basename) if os.path.exists(cfg_path): return cfg_path + # Migrate legacy suffixed config names to the new canonical form + legacy_map = { + 'SYSTEM': ['agent_settings_svc.json'], + 'CURRENTUSER': ['agent_settings_user.json'], + } + try: + legacy_candidates = [] + if suffix: + for legacy_name in legacy_map.get(suffix.upper(), []): + legacy_candidates.extend([ + os.path.join(settings_dir, legacy_name), + os.path.join(project_root, legacy_name), + os.path.join(project_root, 'Agent', 'Settings', legacy_name), + ]) + for legacy in legacy_candidates: + if os.path.exists(legacy): + try: + shutil.move(legacy, cfg_path) + except Exception: + shutil.copy2(legacy, cfg_path) + return cfg_path + except Exception: + pass + # If using a suffixed config and there is a base config (new or legacy), seed from it try: - if CONFIG_NAME_SUFFIX: + if suffix: 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') @@ -260,21 +332,28 @@ def _resolve_config_path(): # Migrate legacy root configs or prior Agent/Settings into Agent/Borealis/Settings try: - legacy_root = os.path.join(project_root, cfg_basename) + legacy_names = [cfg_basename] + try: + if suffix: + legacy_names.extend(legacy_map.get(suffix.upper(), [])) + except Exception: + pass 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 + for legacy_name in legacy_names: + legacy_root = os.path.join(project_root, legacy_name) + if os.path.exists(legacy_root): + try: + shutil.move(legacy_root, cfg_path) + except Exception: + shutil.copy2(legacy_root, cfg_path) + return cfg_path + legacy_old_settings = os.path.join(old_settings_dir, legacy_name) + 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 @@ -356,18 +435,80 @@ class ConfigManager: CONFIG = ConfigManager(CONFIG_PATH) CONFIG.load() -def init_agent_id(): - if not CONFIG.data.get('agent_id'): - host = socket.gethostname().lower() - rand = uuid.uuid4().hex[:8] - if SYSTEM_SERVICE_MODE: - aid = f"{host}-agent-svc-{rand}" - elif (CONFIG_NAME_SUFFIX or '').lower() == 'user': - aid = f"{host}-agent-{rand}-script" - else: - aid = f"{host}-agent-{rand}" - CONFIG.data['agent_id'] = aid +def _get_context_label() -> str: + return 'SYSTEM' if SYSTEM_SERVICE_MODE else 'CURRENTUSER' + + +def _normalize_agent_guid(guid: str) -> str: + try: + if not guid: + return '' + value = str(guid).strip().replace('\ufeff', '') + if not value: + return '' + value = value.strip('{}') + try: + return str(uuid.UUID(value)).upper() + except Exception: + cleaned = ''.join(ch for ch in value if ch in string.hexdigits or ch == '-') + cleaned = cleaned.strip('-') + if cleaned: + try: + return str(uuid.UUID(cleaned)).upper() + except Exception: + pass + return value.upper() + except Exception: + return '' + + +def _read_agent_guid_from_disk() -> str: + try: + path = _agent_guid_path() + if os.path.isfile(path): + with open(path, 'r', encoding='utf-8') as fh: + value = fh.read() + return _normalize_agent_guid(value) + except Exception: + pass + return '' + + +def _ensure_agent_guid() -> str: + guid = _read_agent_guid_from_disk() + if guid: + return guid + new_guid = str(uuid.uuid4()).upper() + _persist_agent_guid_local(new_guid) + return new_guid + + +def _compose_agent_id(hostname: str, guid: str, context: str) -> str: + host = (hostname or '').strip() + if not host: + host = 'UNKNOWN-HOST' + host = host.replace(' ', '-').upper() + normalized_guid = _normalize_agent_guid(guid) or guid or 'UNKNOWN-GUID' + context_label = (context or '').strip().upper() or _get_context_label() + return f"{host}_{normalized_guid}_{context_label}" + + +def _update_agent_id_for_guid(guid: str): + normalized_guid = _normalize_agent_guid(guid) + if not normalized_guid: + return + desired = _compose_agent_id(socket.gethostname(), normalized_guid, _get_context_label()) + existing = (CONFIG.data.get('agent_id') or '').strip() + if existing != desired: + CONFIG.data['agent_id'] = desired CONFIG._write() + global AGENT_ID + AGENT_ID = CONFIG.data['agent_id'] + + +def init_agent_id(): + guid = _ensure_agent_guid() + _update_agent_id_for_guid(guid) return CONFIG.data['agent_id'] AGENT_ID = init_agent_id() @@ -537,37 +678,6 @@ def _settings_dir(): return os.path.abspath(os.path.join(os.path.dirname(__file__), 'Settings')) -def _agent_guid_path() -> str: - try: - root = _find_project_root() - return os.path.join(root, 'Agent', 'Borealis', 'agent_GUID') - except Exception: - return os.path.abspath(os.path.join(os.path.dirname(__file__), 'agent_GUID')) - - -def _persist_agent_guid_local(guid: str): - guid = (guid or '').strip() - if not guid: - return - path = _agent_guid_path() - try: - directory = os.path.dirname(path) - if directory: - os.makedirs(directory, exist_ok=True) - existing = '' - if os.path.isfile(path): - try: - with open(path, 'r', encoding='utf-8') as fh: - existing = fh.read().strip() - except Exception: - existing = '' - if existing != guid: - with open(path, 'w', encoding='utf-8') as fh: - fh.write(guid) - except Exception as exc: - _log_agent(f'Failed to persist agent GUID locally: {exc}', fname='agent.error.log') - - def get_server_url() -> str: """Return the Borealis server URL from env or Agent/Borealis/Settings/server_url.txt. - Strips UTF-8 BOM and whitespace @@ -1263,6 +1373,7 @@ async def connect(): guid_value = (data.get('agent_guid') or '').strip() if guid_value: _persist_agent_guid_local(guid_value) + _update_agent_id_for_guid(guid_value) except Exception: pass asyncio.create_task(_svc_checkin_once()) diff --git a/Data/Agent/launch_service.ps1 b/Data/Agent/launch_service.ps1 index bb17113..4c7a48a 100644 --- a/Data/Agent/launch_service.ps1 +++ b/Data/Agent/launch_service.ps1 @@ -24,7 +24,7 @@ try { if (-not (Test-Path $agentPy)) { throw "Agent script not found: $agentPy" } $exe = if ($Console) { $py } else { if (Test-Path $pyw) { $pyw } else { $py } } - $args = @("`"$agentPy`"","--system-service","--config","svc") + $args = @("`"$agentPy`"","--system-service","--config","SYSTEM") # Launch and keep the task in Running state by waiting on the child $p = Start-Process -FilePath $exe -ArgumentList $args -WindowStyle Hidden -PassThru -WorkingDirectory $scriptDir ` diff --git a/Update.ps1 b/Update.ps1 index fdd631c..d63355f 100644 --- a/Update.ps1 +++ b/Update.ps1 @@ -175,6 +175,8 @@ function Get-AgentServiceId { if (-not $AgentRoot) { $AgentRoot = $scriptDir } $settingsDir = Join-Path $AgentRoot 'Settings' $candidates = @( + (Join-Path $settingsDir 'agent_settings_SYSTEM.json') + (Join-Path $settingsDir 'agent_settings_CURRENTUSER.json') (Join-Path $settingsDir 'agent_settings_svc.json') (Join-Path $settingsDir 'agent_settings_user.json') (Join-Path $settingsDir 'agent_settings.json')