Merge pull request #117 from bunny-lab-io:codex/rename-agent-settings-and-adjust-id-format

It seems that workflows now function properly and capture screen data.  If you switch between CURRENTUSER and SYSTEM for the screenshot node (this should not be possible since SYSTEM has no screen), it glitches the screen capture and requires the user restarts the currentuser agent.  This is a scenario that should not happen, so it will be enforced later based on the role to ensure that it cannot be used if the incorrect context is chosen in future updates.
This commit is contained in:
2025-10-16 23:49:45 -06:00
committed by GitHub
6 changed files with 188 additions and 75 deletions

View File

@@ -96,12 +96,12 @@ All runtime logs live under `Logs/<ServiceName>` relative to the project root (`
## New: Agent Launch Model, Tasks, and Logging ## New: Agent Launch Model, Tasks, and Logging
- SYSTEM mode is launched via a wrapper to guarantee WorkingDirectory and capture stdout/stderr: - 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. - `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. - 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: - Config files and inheritance:
- Base config now lives at `<ProjectRoot>\\Agent\\Borealis\\Settings\\agent_settings.json`. - 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`. - 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: - Logging:
- Early bootstrap log: `<ProjectRoot>\\Logs\\Agent\\bootstrap.log` (helps verify launch + mode). - 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`. - Dev UI separately (if needed): `cd Server\\web-interface && npm run dev`.
- Launch/repair agent (elevated PowerShell): `.\\Borealis.ps1 -Agent -AgentAction install`. - Launch/repair agent (elevated PowerShell): `.\\Borealis.ps1 -Agent -AgentAction install`.
- Manual short-run agent checks (non-blocking): - Manual short-run agent checks (non-blocking):
- `Start-Process .\\Agent\\Scripts\\pythonw.exe -ArgumentList '".\\Agent\\Borealis\\agent.py" --system-service --config svc'` - `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_svc.json` and `Agent\\Borealis\\Settings\\server_url.txt`. - Verify logs under `Logs\\Agent` and presence of `Agent\\Borealis\\Settings\\agent_settings_SYSTEM.json` and `Agent\\Borealis\\Settings\\server_url.txt`.
## Troubleshooting Checklist ## Troubleshooting Checklist
- Agent task “Ready” with 0x1: ensure the SYSTEM task uses `launch_service.ps1` and that WorkingDirectory is `Agent\\Borealis`. - Agent task “Ready” with 0x1: ensure the SYSTEM task uses `launch_service.ps1` and that WorkingDirectory is `Agent\\Borealis`.

View File

@@ -715,7 +715,7 @@ function Ensure-AgentTasks {
# Optional user-session helper for interactive roles (tray, overlays) # Optional user-session helper for interactive roles (tray, overlays)
$helperName = 'Borealis Agent (UserHelper)' $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) $usrAction = New-ScheduledTaskAction -Execute $pyw -Argument $usrArg -WorkingDirectory (Split-Path $agentPy -Parent)
$usrTrig = New-ScheduledTaskTrigger -AtLogOn $usrTrig = New-ScheduledTaskTrigger -AtLogOn
$usrSet = New-ScheduledTaskSettingsSet -Hidden -RestartCount 3 -RestartInterval (New-TimeSpan -Minutes 1) -ExecutionTimeLimit ([TimeSpan]::Zero) $usrSet = New-ScheduledTaskSettingsSet -Hidden -RestartCount 3 -RestartInterval (New-TimeSpan -Minutes 1) -ExecutionTimeLimit ([TimeSpan]::Zero)

View File

@@ -215,7 +215,7 @@ cd "$ROOT_DIR"
LOG_DIR="$(cd -- "$ROOT_DIR/../../Logs/Agent" && pwd 2>/dev/null || echo "$ROOT_DIR/../../Logs/Agent")" LOG_DIR="$(cd -- "$ROOT_DIR/../../Logs/Agent" && pwd 2>/dev/null || echo "$ROOT_DIR/../../Logs/Agent")"
mkdir -p "$LOG_DIR" mkdir -p "$LOG_DIR"
PY_BIN="${ROOT_DIR}/../bin/python3" 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 SH
chmod +x "${agentDest}/launch_service.sh" chmod +x "${agentDest}/launch_service.sh"
@@ -229,7 +229,7 @@ ensure_agent_tasks() {
# Register @reboot cron entries for system (root) and current user # Register @reboot cron entries for system (root) and current user
local agentDest="${SCRIPT_DIR}/Agent/Borealis" local agentDest="${SCRIPT_DIR}/Agent/Borealis"
local sys_cmd="bash '${agentDest}/launch_service.sh'" 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 # Root/system entry
if need_sudo; then if need_sudo; then
@@ -261,7 +261,7 @@ remove_agent() {
log_agent "=== Removal start ===" Removal.log log_agent "=== Removal start ===" Removal.log
kill_agent_processes || true kill_agent_processes || true
local sys_cmd="bash '${SCRIPT_DIR}/Agent/Borealis/launch_service.sh'" 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 remove_cron_entries "$sys_cmd" "$user_cmd" || true
rm -rf "${SCRIPT_DIR}/Agent" || true rm -rf "${SCRIPT_DIR}/Agent" || true
log_agent "=== Removal end ===" Removal.log log_agent "=== Removal end ===" Removal.log
@@ -271,7 +271,7 @@ launch_user_helper_now() {
local py="${SCRIPT_DIR}/Agent/bin/python3" local py="${SCRIPT_DIR}/Agent/bin/python3"
local helper="${SCRIPT_DIR}/Agent/Borealis/agent.py" local helper="${SCRIPT_DIR}/Agent/Borealis/agent.py"
if [[ -x "$py" && -f "$helper" ]]; then 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}" echo -e "${GREEN}Launched user-session helper.${RESET}"
else else
echo -e "${YELLOW}Agent venv or helper missing; run install first.${RESET}" echo -e "${YELLOW}Agent venv or helper missing; run install first.${RESET}"

View File

@@ -85,6 +85,58 @@ def _argv_get(flag: str, default: str = None):
return default return default
CONFIG_NAME_SUFFIX = _argv_get('--config', None) 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: if not SYSTEM_SERVICE_MODE:
# Reduce noisy Qt output and attempt to avoid Windows OleInitialize warnings # Reduce noisy Qt output and attempt to avoid Windows OleInitialize warnings
os.environ.setdefault("QT_LOGGING_RULES", "qt.qpa.*=false;*.debug=false") os.environ.setdefault("QT_LOGGING_RULES", "qt.qpa.*=false;*.debug=false")
@@ -224,21 +276,41 @@ def _resolve_config_path():
# Determine filename with optional suffix # Determine filename with optional suffix
cfg_basename = 'agent_settings.json' cfg_basename = 'agent_settings.json'
try: suffix = CONFIG_SUFFIX_CANONICAL
if CONFIG_NAME_SUFFIX: if suffix:
suffix = ''.join(ch for ch in CONFIG_NAME_SUFFIX if ch.isalnum() or ch in ('_', '-')).strip() cfg_basename = f"agent_settings_{suffix}.json"
if suffix:
cfg_basename = f"agent_settings_{suffix}.json"
except Exception:
pass
cfg_path = os.path.join(settings_dir, cfg_basename) cfg_path = os.path.join(settings_dir, cfg_basename)
if os.path.exists(cfg_path): if os.path.exists(cfg_path):
return 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 # If using a suffixed config and there is a base config (new or legacy), seed from it
try: try:
if CONFIG_NAME_SUFFIX: if suffix:
base_new = os.path.join(settings_dir, 'agent_settings.json') base_new = os.path.join(settings_dir, 'agent_settings.json')
base_old_settings = os.path.join(project_root, 'Agent', 'Settings', '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') 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 # Migrate legacy root configs or prior Agent/Settings into Agent/Borealis/Settings
try: 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') old_settings_dir = os.path.join(project_root, 'Agent', 'Settings')
legacy_old_settings = os.path.join(old_settings_dir, cfg_basename) for legacy_name in legacy_names:
if os.path.exists(legacy_root): legacy_root = os.path.join(project_root, legacy_name)
try: if os.path.exists(legacy_root):
shutil.move(legacy_root, cfg_path) try:
except Exception: shutil.move(legacy_root, cfg_path)
shutil.copy2(legacy_root, cfg_path) except Exception:
return cfg_path shutil.copy2(legacy_root, cfg_path)
if os.path.exists(legacy_old_settings): return cfg_path
try: legacy_old_settings = os.path.join(old_settings_dir, legacy_name)
shutil.move(legacy_old_settings, cfg_path) if os.path.exists(legacy_old_settings):
except Exception: try:
shutil.copy2(legacy_old_settings, cfg_path) shutil.move(legacy_old_settings, cfg_path)
return cfg_path except Exception:
shutil.copy2(legacy_old_settings, cfg_path)
return cfg_path
except Exception: except Exception:
pass pass
@@ -356,18 +435,80 @@ class ConfigManager:
CONFIG = ConfigManager(CONFIG_PATH) CONFIG = ConfigManager(CONFIG_PATH)
CONFIG.load() CONFIG.load()
def init_agent_id(): def _get_context_label() -> str:
if not CONFIG.data.get('agent_id'): return 'SYSTEM' if SYSTEM_SERVICE_MODE else 'CURRENTUSER'
host = socket.gethostname().lower()
rand = uuid.uuid4().hex[:8]
if SYSTEM_SERVICE_MODE: def _normalize_agent_guid(guid: str) -> str:
aid = f"{host}-agent-svc-{rand}" try:
elif (CONFIG_NAME_SUFFIX or '').lower() == 'user': if not guid:
aid = f"{host}-agent-{rand}-script" return ''
else: value = str(guid).strip().replace('\ufeff', '')
aid = f"{host}-agent-{rand}" if not value:
CONFIG.data['agent_id'] = aid 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() 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'] return CONFIG.data['agent_id']
AGENT_ID = init_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')) 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: def get_server_url() -> str:
"""Return the Borealis server URL from env or Agent/Borealis/Settings/server_url.txt. """Return the Borealis server URL from env or Agent/Borealis/Settings/server_url.txt.
- Strips UTF-8 BOM and whitespace - Strips UTF-8 BOM and whitespace
@@ -1263,6 +1373,7 @@ async def connect():
guid_value = (data.get('agent_guid') or '').strip() guid_value = (data.get('agent_guid') or '').strip()
if guid_value: if guid_value:
_persist_agent_guid_local(guid_value) _persist_agent_guid_local(guid_value)
_update_agent_id_for_guid(guid_value)
except Exception: except Exception:
pass pass
asyncio.create_task(_svc_checkin_once()) asyncio.create_task(_svc_checkin_once())

View File

@@ -24,7 +24,7 @@ try {
if (-not (Test-Path $agentPy)) { throw "Agent script not found: $agentPy" } if (-not (Test-Path $agentPy)) { throw "Agent script not found: $agentPy" }
$exe = if ($Console) { $py } else { if (Test-Path $pyw) { $pyw } else { $py } } $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 # 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 ` $p = Start-Process -FilePath $exe -ArgumentList $args -WindowStyle Hidden -PassThru -WorkingDirectory $scriptDir `

View File

@@ -175,6 +175,8 @@ function Get-AgentServiceId {
if (-not $AgentRoot) { $AgentRoot = $scriptDir } if (-not $AgentRoot) { $AgentRoot = $scriptDir }
$settingsDir = Join-Path $AgentRoot 'Settings' $settingsDir = Join-Path $AgentRoot 'Settings'
$candidates = @( $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_svc.json')
(Join-Path $settingsDir 'agent_settings_user.json') (Join-Path $settingsDir 'agent_settings_user.json')
(Join-Path $settingsDir 'agent_settings.json') (Join-Path $settingsDir 'agent_settings.json')