mirror of
https://github.com/bunny-lab-io/Borealis.git
synced 2025-10-26 15:21:57 -06:00
Refine agent config naming and IDs
This commit is contained in:
10
AGENTS.md
10
AGENTS.md
@@ -96,12 +96,12 @@ All runtime logs live under `Logs/<ServiceName>` 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 `<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.
|
||||
- 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 `<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).
|
||||
@@ -115,8 +115,8 @@ All runtime logs live under `Logs/<ServiceName>` 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`.
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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}"
|
||||
|
||||
@@ -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())
|
||||
|
||||
@@ -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 `
|
||||
|
||||
@@ -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')
|
||||
|
||||
Reference in New Issue
Block a user