Files
Borealis-Github-Replica/Data/Agent/script_agent.py

211 lines
8.1 KiB
Python

import os
import sys
import time
import socket
import asyncio
import json
import subprocess
import tempfile
import socketio
import platform
import time
import uuid
import tempfile
def get_project_root():
return os.path.abspath(os.path.join(os.path.dirname(__file__), "..", ".."))
def get_server_url():
# Try to reuse the agent config if present
cfg_path = os.path.join(get_project_root(), "agent_settings.json")
try:
if os.path.isfile(cfg_path):
with open(cfg_path, "r", encoding="utf-8") as f:
data = json.load(f)
url = data.get("borealis_server_url")
if isinstance(url, str) and url.strip():
return url.strip()
except Exception:
pass
return "http://localhost:5000"
def run_powershell_script_content(content: str):
# Store ephemeral script under <ProjectRoot>/Temp
temp_dir = os.path.join(get_project_root(), "Temp")
os.makedirs(temp_dir, exist_ok=True)
fd, path = tempfile.mkstemp(prefix="sj_", suffix=".ps1", dir=temp_dir, text=True)
with os.fdopen(fd, 'w', encoding='utf-8', newline='\n') as fh:
fh.write(content or "")
ps = os.path.expandvars(r"%SystemRoot%\\System32\\WindowsPowerShell\\v1.0\\powershell.exe")
if not os.path.isfile(ps):
ps = "powershell.exe"
try:
proc = subprocess.run(
[ps, "-ExecutionPolicy", "Bypass", "-NoProfile", "-File", path],
capture_output=True,
text=True,
timeout=60*60,
)
return proc.returncode, proc.stdout or "", proc.stderr or ""
except Exception as e:
return -1, "", str(e)
async def main():
sio = socketio.AsyncClient(reconnection=True)
hostname = socket.gethostname()
@sio.event
async def connect():
print("[ScriptAgent] Connected to server")
# Identify as script agent (no heartbeat to avoid UI duplication)
try:
await sio.emit("connect_agent", {"agent_id": f"{hostname}-script"})
except Exception:
pass
@sio.on("quick_job_run")
async def on_quick_job_run(payload):
# Treat as generic script_run internally
try:
target = (payload.get('target_hostname') or '').strip().lower()
if target and target != hostname.lower():
return
run_mode = (payload.get('run_mode') or 'current_user').lower()
# Only the SYSTEM service handles system-mode jobs; ignore others
if run_mode != 'system':
return
job_id = payload.get('job_id')
script_type = (payload.get('script_type') or '').lower()
content = payload.get('script_content') or ''
if script_type != 'powershell':
await sio.emit('quick_job_result', {
'job_id': job_id,
'status': 'Failed',
'stdout': '',
'stderr': f"Unsupported type: {script_type}"
})
return
# Preferred: run via ephemeral scheduled task under SYSTEM for isolation
rc, out, err = run_powershell_via_system_task(content)
if rc == -999:
# Fallback to direct execution if task creation not available
rc, out, err = run_powershell_script_content(content)
status = 'Success' if rc == 0 else 'Failed'
await sio.emit('quick_job_result', {
'job_id': job_id,
'status': status,
'stdout': out,
'stderr': err,
})
except Exception as e:
try:
await sio.emit('quick_job_result', {
'job_id': payload.get('job_id') if isinstance(payload, dict) else None,
'status': 'Failed',
'stdout': '',
'stderr': str(e),
})
except Exception:
pass
@sio.event
async def disconnect():
print("[ScriptAgent] Disconnected")
async def heartbeat_loop():
# Minimal heartbeat so device appears online even without a user helper
while True:
try:
await sio.emit("agent_heartbeat", {
"agent_id": f"{hostname}-script",
"hostname": hostname,
"agent_operating_system": f"{platform.system()} {platform.release()} (Service)",
"last_seen": int(time.time())
})
except Exception:
pass
await asyncio.sleep(30)
url = get_server_url()
while True:
try:
await sio.connect(url, transports=['websocket'])
# Heartbeat while connected
hb = asyncio.create_task(heartbeat_loop())
try:
await sio.wait()
finally:
try:
hb.cancel()
except Exception:
pass
except Exception as e:
print(f"[ScriptAgent] reconnect in 5s: {e}")
await asyncio.sleep(5)
def run_powershell_via_system_task(content: str):
"""Create an ephemeral scheduled task under SYSTEM to run the script.
Returns (rc, stdout, stderr). If the environment lacks PowerShell ScheduledTasks module, returns (-999, '', 'unavailable').
"""
ps_exe = os.path.expandvars(r"%SystemRoot%\System32\WindowsPowerShell\v1.0\powershell.exe")
if not os.path.isfile(ps_exe):
ps_exe = 'powershell.exe'
try:
os.makedirs(os.path.join(get_project_root(), 'Temp'), exist_ok=True)
# Write the target script
script_fd, script_path = tempfile.mkstemp(prefix='sys_task_', suffix='.ps1', dir=os.path.join(get_project_root(), 'Temp'), text=True)
with os.fdopen(script_fd, 'w', encoding='utf-8', newline='\n') as f:
f.write(content or '')
# Output capture path
out_path = os.path.join(get_project_root(), 'Temp', f'out_{uuid.uuid4().hex}.txt')
task_name = f"Borealis Agent - Task - {uuid.uuid4().hex} @ SYSTEM"
# Build PS to create/run task with DeleteExpiredTaskAfter
task_ps = f"""
$ErrorActionPreference='Continue'
$task = "{task_name}"
$ps = "{ps_exe}"
$scr = "{script_path}"
$out = "{out_path}"
try {{ Unregister-ScheduledTask -TaskName $task -Confirm:$false -ErrorAction SilentlyContinue }} catch {{}}
$action = New-ScheduledTaskAction -Execute $ps -Argument ('-NoProfile -ExecutionPolicy Bypass -File "' + $scr + '" *> "' + $out + '"')
$settings = New-ScheduledTaskSettingsSet -DeleteExpiredTaskAfter (New-TimeSpan -Minutes 5) -AllowStartIfOnBatteries -DontStopIfGoingOnBatteries
$principal= New-ScheduledTaskPrincipal -UserId 'SYSTEM' -LogonType ServiceAccount -RunLevel Highest
Register-ScheduledTask -TaskName $task -Action $action -Settings $settings -Principal $principal -Force | Out-Null
Start-ScheduledTask -TaskName $task | Out-Null
Start-Sleep -Seconds 2
Get-ScheduledTask -TaskName $task | Out-Null
"""
# Run task creation
proc = subprocess.run([ps_exe, '-NoProfile', '-ExecutionPolicy', 'Bypass', '-Command', task_ps], capture_output=True, text=True)
if proc.returncode != 0:
return -999, '', (proc.stderr or proc.stdout or 'scheduled task creation failed')
# Wait up to 60s for output to be written
deadline = time.time() + 60
out_data = ''
while time.time() < deadline:
try:
if os.path.isfile(out_path) and os.path.getsize(out_path) > 0:
with open(out_path, 'r', encoding='utf-8', errors='replace') as f:
out_data = f.read()
break
except Exception:
pass
time.sleep(1)
# Cleanup task (best-effort)
cleanup_ps = f"try {{ Unregister-ScheduledTask -TaskName '{task_name}' -Confirm:$false }} catch {{}}"
subprocess.run([ps_exe, '-NoProfile', '-ExecutionPolicy', 'Bypass', '-Command', cleanup_ps], capture_output=True, text=True)
return 0, out_data or '', ''
except Exception as e:
return -999, '', str(e)
if __name__ == '__main__':
asyncio.run(main())