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

View File

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

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

View File

@@ -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()
suffix = CONFIG_SUFFIX_CANONICAL
if suffix:
cfg_basename = f"agent_settings_{suffix}.json"
except Exception:
pass
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,15 +332,22 @@ 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)
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)
@@ -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())

View File

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

View File

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