From 31088cb8dee311d540144d998e9b98cf55bba209 Mon Sep 17 00:00:00 2001 From: Mike Swanson Date: Fri, 15 May 2026 15:23:05 -0700 Subject: [PATCH] sync: auto-sync from DESKTOP-0O8A1RL at 2026-05-15 15:23:02 Author: Mike Swanson Machine: DESKTOP-0O8A1RL Timestamp: 2026-05-15 15:23:02 --- projects/msp-tools/guru-rmm | 2 +- session-logs/2026-05-15-session.md | 128 +++++++++++++ session-logs/disable_defender.ps1 | 24 +++ session-logs/remove_defender.ps1 | 12 ++ session-logs/tmp_pluto_addkeys.py | 72 ++++++++ session-logs/tmp_pluto_applog.ps1 | 32 ++++ session-logs/tmp_pluto_check.py | 75 ++++++++ session-logs/tmp_pluto_check2.py | 68 +++++++ session-logs/tmp_pluto_evtlog.ps1 | 7 + session-logs/tmp_pluto_evtlog2.ps1 | 18 ++ session-logs/tmp_rmm_admkeys.py | 74 ++++++++ session-logs/tmp_rmm_debug.py | 41 +++++ session-logs/tmp_rmm_poll.py | 23 +++ session-logs/tmp_rmm_retry.py | 43 +++++ session-logs/tmp_rmm_retry2.py | 76 ++++++++ session-logs/tmp_rmm_ssh_fix.py | 45 +++++ session-logs/tmp_rmm_sshkey.py | 55 ++++++ session-logs/tmp_rmm_test.py | 47 +++++ session-logs/tmp_wxs.xml | 163 +++++++++++++++++ session-logs/tmp_wxs_new.xml | 254 ++++++++++++++++++++++++++ session-logs/webhook-handler-fixed.py | 109 +++++++++++ 21 files changed, 1367 insertions(+), 1 deletion(-) create mode 100644 session-logs/disable_defender.ps1 create mode 100644 session-logs/remove_defender.ps1 create mode 100644 session-logs/tmp_pluto_addkeys.py create mode 100644 session-logs/tmp_pluto_applog.ps1 create mode 100644 session-logs/tmp_pluto_check.py create mode 100644 session-logs/tmp_pluto_check2.py create mode 100644 session-logs/tmp_pluto_evtlog.ps1 create mode 100644 session-logs/tmp_pluto_evtlog2.ps1 create mode 100644 session-logs/tmp_rmm_admkeys.py create mode 100644 session-logs/tmp_rmm_debug.py create mode 100644 session-logs/tmp_rmm_poll.py create mode 100644 session-logs/tmp_rmm_retry.py create mode 100644 session-logs/tmp_rmm_retry2.py create mode 100644 session-logs/tmp_rmm_ssh_fix.py create mode 100644 session-logs/tmp_rmm_sshkey.py create mode 100644 session-logs/tmp_rmm_test.py create mode 100644 session-logs/tmp_wxs.xml create mode 100644 session-logs/tmp_wxs_new.xml create mode 100644 session-logs/webhook-handler-fixed.py diff --git a/projects/msp-tools/guru-rmm b/projects/msp-tools/guru-rmm index 52b5695..509f901 160000 --- a/projects/msp-tools/guru-rmm +++ b/projects/msp-tools/guru-rmm @@ -1 +1 @@ -Subproject commit 52b56951b3335a7b1fad9766c6138449df04276a +Subproject commit 509f9018b7cd3011bf2b42f2c36f71793b7d1300 diff --git a/session-logs/2026-05-15-session.md b/session-logs/2026-05-15-session.md index cda42cd..5ff8bd6 100644 --- a/session-logs/2026-05-15-session.md +++ b/session-logs/2026-05-15-session.md @@ -521,3 +521,131 @@ curl http://localhost:3001/api/changelog/server/latest # 200 - **Key file**: `agent/src/transport/websocket.rs` — `last_incoming` at line ~279, `sleep_until` at line ~361 - **Key file**: `server/src/api/changelog.rs` - **Key file**: `scripts/generate-changelog.sh` + +--- + +## Update: 15:20 PT — Pluto SSH recovery, Defender removal, build pipeline repair, perf test + +## User +- **User:** Mike Swanson (mike) +- **Machine:** DESKTOP-0O8A1RL +- **Role:** admin +- **Session span:** ~18:00 UTC – 22:20 UTC 2026-05-15 (continued from prior context window) + +## Session Summary + +The session opened with Pluto (172.16.3.36, Windows Server 2019, the Windows build server) offline and unreachable via SSH. Pluto had been unreachable since at least the prior session. SSH key access had been lost — the cause was investigated via Windows event logs pulled through the RMM. The OpenSSH operational log revealed that the last successful connections used key fingerprint `SHA256:FirWvKG7jOqtG2nzX+D0a79/YLFjGAwuWcjP3yz5hCs`, which is root's key on the build server (`/root/.ssh/id_ed25519`), not the guru user's key. This was the root cause of subsequent SSH failures: prior repair attempts added guru's key (`Q+ivqd/...`) instead of root's key. SSH access was restored by adding root's key to `C:\ProgramData\ssh\administrators_authorized_keys` via RMM cmd script. A secondary issue caused the initial repair attempts to fail even with the correct key content: PowerShell's `>` operator writes UTF-16 LE, which Windows OpenSSH silently rejects. The file must be written with explicit ASCII encoding via `[System.IO.File]::WriteAllText(..., [System.Text.Encoding]::ASCII)`. Once both the correct key and correct encoding were in place, SSH worked. + +With Pluto accessible, Windows Defender was removed to improve build performance. `Set-MpPreference` and registry policy approaches were blocked by Tamper Protection. DISM failed due to wrong flag syntax for Server 2019. `Uninstall-WindowsFeature` fails over SSH due to a Windows console I/O buffer issue. The only working approach was running `Uninstall-WindowsFeature -Name Windows-Defender -Restart` interactively via ScreenConnect. Pluto rebooted, Defender was fully removed. + +With Defender gone, the build pipeline was repaired end-to-end. Three separate issues prevented automatic builds from firing. First: Gitea 1.25.2 blocks webhook delivery to private/internal IP addresses by default — no `[webhook]` section existed in `app.ini`, so all push events were silently dropped. Fix: added `ALLOWED_HOST_LIST = *` to `app.ini` and restarted the Gitea container. Second: the webhook handler (`/opt/gururmm/webhook-handler.py`) used `subprocess.Popen` without ever calling `proc.wait()`, causing every completed build to leave a zombie sudo process. `os.kill(pid, 0)` returns success for zombies, so `is_build_running()` permanently returned True after the first build, silently dropping all subsequent webhooks. Fix: moved build execution to a daemon thread that calls `proc.wait()` and removes the lock file on completion. Third: `administrators_authorized_keys` had guru's key instead of root's key; the build script runs as root via sudo, so only root's key matters. Fix: added root's key via RMM alongside guru's key. + +With all three fixes in place, a clean build completed in 42 seconds total (1s Linux, 25s Pluto, rest deploy/sign). The previous baseline with Defender enabled was 367 seconds — an 8.7x speedup. Defender had consumed approximately 325 seconds per build on Pluto alone (scanning cargo output, the sccache directory, and the compiled binaries during linking and signing). A Gitea webhook to the Pluto password (`Paper123!@#`) was also set during the session when Mike reset the Administrator account after the Defender removal complications. + +## Key Decisions + +- **ASCII encoding for authorized_keys**: PowerShell's `>` and `Out-File` default to UTF-16 LE. Windows OpenSSH requires ASCII or UTF-8 without BOM for authorized_keys files. Silently fails with no error message — looks like a permissions issue. Use `[System.IO.File]::WriteAllText` with `[System.Text.Encoding]::ASCII` exclusively. +- **Root's key, not guru's key**: The build script runs as root via `sudo bash /opt/gururmm/build-agents.sh`. SSH connections to Pluto use `/root/.ssh/id_ed25519`, not `/home/guru/.ssh/id_ed25519`. Both keys should be in `administrators_authorized_keys` — root's for builds, guru's for manual access. +- **Defender removal via ScreenConnect only**: All automated approaches (registry, DISM, scheduled task, `Uninstall-WindowsFeature` over SSH) fail on Server 2019 with Tamper Protection enabled. Interactive console is required. Not worth automating further. +- **Thread-based build dispatch in webhook handler**: Alternative was fixing `is_build_running()` to detect zombies via `/proc//status`. Thread approach is cleaner: `proc.wait()` in the thread reaps the child and removes the lock atomically. Lock file is only present while the build is actively running. +- **No manual build runs**: Rule established (and saved to memory) — `build-agents.sh` must only be triggered via the Gitea webhook pipeline. Manual runs execute as `guru` instead of root, breaking log writes, artifact cleanup, and service restart. + +## Problems Encountered + +- **SSH key wrong user**: Added guru's key to Pluto instead of root's key. Build pipeline uses root. SSH from build server (as guru via manual testing) worked; build pipeline (as root) failed. Fixed by adding root's key via RMM. +- **UTF-16 encoding silently broke SSH auth**: CMD `echo` and PowerShell `>` both produce encodings that Windows OpenSSH rejects. No error in sshd logs — just falls through to password auth. Resolution: `[System.IO.File]::WriteAllText` with explicit ASCII encoding. +- **Gitea silently blocked webhook delivery**: `ALLOWED_HOST_LIST` unset in `app.ini` caused Gitea 1.25.2 to drop all push webhook deliveries to 172.16.3.30 with no log entry, no retry, and a 200 response from the test delivery endpoint. Discovered by checking nginx access logs (zero POST entries from Gitea despite successful pushes). +- **Zombie lock permanently blocking builds**: Every build after the first was silently skipped. `is_build_running()` returned True indefinitely because zombie PIDs respond to `os.kill(pid, 0)`. Discovered by checking lock file PID against `ps` — process showed ``. Fixed by reaping child in a thread. +- **Gitea app.ini edit left duplicate `[webhook]` sections**: Echo without `-e` wrote literal `\n` characters. Fixed by pulling the file out of the container with `docker cp`, cleaning with `grep -v`, and pushing back. +- **`Uninstall-WindowsFeature` over SSH returns "Win32 internal error 0x5"**: Not an access denial — the console output buffer isn't available in a non-interactive SSH session. This specific cmdlet requires a real console. Cannot be automated over SSH. + +## Configuration Changes + +| Location | File/Resource | Change | +|---|---|---| +| Gitea container | `/data/gitea/conf/app.ini` | Added `[webhook]\nALLOWED_HOST_LIST = *` | +| Build server | `/opt/gururmm/webhook-handler.py` | Replaced Popen-without-wait with daemon thread; zombie-aware `is_build_running()` | +| Pluto | `C:\ProgramData\ssh\administrators_authorized_keys` | Added root's key + guru's key; ASCII-encoded, icacls restricted | +| Pluto | Windows Defender | Fully removed via `Uninstall-WindowsFeature` | +| Memory | `project_pluto_build_server.md` | Added Administrator password, SSH encoding requirement, root key vs guru key distinction | +| Memory | `MEMORY.md` | Added GuruRMM build rule entry | +| Memory | `feedback_gururmm_builds.md` | New: no manual builds, always use webhook pipeline | + +## Credentials & Secrets + +- **Pluto Administrator password**: `Paper123!@#` (set 2026-05-15 by Mike via ScreenConnect after Defender removal complications) +- **Jupiter root**: `172.16.3.20` / `root` / `Th1nk3r^99##` — from vault `infrastructure/jupiter-unraid-primary.sops.yaml` +- **Jupiter iDRAC**: `172.16.1.73` / `root` / `Window123!@#-idrac` +- **Gitea API token**: `9b1da4b79a38ef782268341d25a4b6880572063f` (azcomputerguru account) — from vault `services/gitea.sops.yaml` +- **RMM API**: `claude-api@azcomputerguru.com` / `ClaudeAPI2026!@#` — `http://localhost:3001/api` + +## Infrastructure & Servers + +- **Pluto**: `172.16.3.36`, Windows Server 2019, VM on Jupiter. SSH: `Administrator@172.16.3.36`. Build pipeline SSHes as root (uses `/root/.ssh/id_ed25519`). Manual access uses guru's key. +- **Jupiter**: `172.16.3.20`, Unraid primary. SSH: `root@172.16.3.20`. 125 GB RAM total, 92 GB used (80 GB VMs, ~8 GB Docker). 33 GB available. +- **Jupiter VMs**: Windows Server 2016 (32 GB), GuruRMM (16 GB), OwnCloud (16 GB), Claude-Builder (8 GB), Unifi (8 GB) +- **Jupiter notable Docker containers**: seafile-elasticsearch (1.86 GB / 2 GB limit — at capacity), app (1.39 GB), seafile (1.13 GB), gitea (852 MB) +- **Gitea**: Docker container on Jupiter, port 3000 (internal). External: `https://git.azcomputerguru.com` (via Cloudflare). Always use `http://172.16.3.20:3000` for API calls. +- **Build webhook**: `POST http://172.16.3.30/webhook/build` → nginx → `http://127.0.0.1:9000` → `gururmm-webhook.service` → `/opt/gururmm/webhook-handler.py` + +## Commands & Outputs + +```bash +# SSH to build server +ssh guru@172.16.3.30 + +# SSH hop to Pluto (from build server) +ssh -o StrictHostKeyChecking=no Administrator@172.16.3.36 hostname + +# Jupiter RAM check +ssh root@172.16.3.20 "free -h" +# Mem: 125Gi total, 92Gi used, 808Mi free, 34Gi buff/cache, 33Gi available + +# Gitea webhook test delivery +curl -s -X POST 'http://172.16.3.20:3000/api/v1/repos/azcomputerguru/gururmm/hooks/1/tests' \ + -H 'Authorization: token 9b1da4b79a38ef782268341d25a4b6880572063f' + +# Trigger build via empty commit (correct method) +ssh guru@172.16.3.30 "cd /home/guru/gururmm && git commit --allow-empty -m 'chore: trigger build' && git push" + +# Restart Gitea after app.ini change +ssh root@172.16.3.20 "docker restart gitea" + +# Check webhook handler zombie issue +cat /var/run/gururmm-build.lock # showed PID +ps -p # showed +rm /var/run/gururmm-build.lock # cleared stale lock +``` + +Build performance results: +``` +Baseline (Defender on, warm sccache): 367s total +Post-Defender (warm sccache): 42s total + Linux agent: 1s (fully cached) + Pluto: 25s (cargo + WiX + 4 binaries) + Deploy/sign: 16s +Speedup: 8.7x +``` + +## Pending / Incomplete Tasks + +- **Pluto password not in vault**: `infrastructure/pluto-build-server.sops.yaml` doesn't exist yet. Password `Paper123!@#` is in memory only. Mike to add to vault. +- **BB-SERVER enrollment loop**: duplicate key `idx_agents_site_device` — pre-existing, unresolved. +- **Windows 0.6.21 not yet distributed**: Pluto builds produce 0.6.21 Windows artifacts on each run. After today's fixes, they should now deploy correctly on future pushes. Verify next build publishes Windows artifacts. +- **IMC1 Unicode escape sequence** in hardware inventory: unresolved. +- **Policy wiring plan** (`ticklish-questing-stallman.md`): Deferred. +- **Portal changelog page**: API exists, no dashboard UI. +- **seafile-elasticsearch at container memory limit** (1.86 GB / 2 GB): Monitor — may need limit raised. +- **macOS agent builds**: Not yet implemented. +- **pre-commit hook not executable** on build server: `hint: The '/home/guru/gururmm/scripts/hooks/pre-commit' hook was ignored because it's not set as executable` — emitted on every commit. Low priority but noisy. + +## Reference Information + +- **Build pipeline commits (gururmm)**: `7773f49`, `44fef95`, `6eed227`, `106fce9`, `3e9ef32`, `509f901` (all empty trigger commits from this session) +- **Pluto agent ID (RMM)**: `5316f56f-a1b3-4ac5-97ac-71ddf6a74d2e` +- **Root SSH key fingerprint** (build server, used by pipeline): `SHA256:FirWvKG7jOqtG2nzX+D0a79/YLFjGAwuWcjP3yz5hCs` — `/root/.ssh/id_ed25519.pub` +- **Guru SSH key fingerprint** (build server, manual access): `SHA256:Q+ivqd/K3eKMqvLdwlkvNWKxvp3NyLt17PcxDwtykFs` — `/home/guru/.ssh/id_ed25519.pub` +- **Webhook handler**: `/opt/gururmm/webhook-handler.py` — `gururmm-webhook.service`, port 9000 +- **Build script**: `/opt/gururmm/build-agents.sh` (production, runs as root via webhook) +- **Gitea webhook ID**: 1, repo `azcomputerguru/gururmm`, event `push`, URL `http://172.16.3.30/webhook/build` +- **Gitea app.ini**: `/data/gitea/conf/app.ini` inside `gitea` Docker container on Jupiter diff --git a/session-logs/disable_defender.ps1 b/session-logs/disable_defender.ps1 new file mode 100644 index 0000000..aec29c2 --- /dev/null +++ b/session-logs/disable_defender.ps1 @@ -0,0 +1,24 @@ +$regPath = "HKLM:\SOFTWARE\Policies\Microsoft\Windows Defender" +if (-not (Test-Path $regPath)) { New-Item -Path $regPath -Force | Out-Null } +Set-ItemProperty -Path $regPath -Name "DisableAntiSpyware" -Value 1 -Type DWord +Write-Host "Policy key set" + +Set-MpPreference -DisableRealtimeMonitoring $true -ErrorAction SilentlyContinue +Set-MpPreference -DisableBehaviorMonitoring $true -ErrorAction SilentlyContinue +Set-MpPreference -DisableIOAVProtection $true -ErrorAction SilentlyContinue +Set-MpPreference -DisableScriptScanning $true -ErrorAction SilentlyContinue +Set-MpPreference -DisableArchiveScanning $true -ErrorAction SilentlyContinue +Write-Host "MpPreference overrides applied" + +try { + Stop-Service -Name WinDefend -Force -ErrorAction Stop + Set-Service -Name WinDefend -StartupType Disabled + Write-Host "WinDefend: stopped and disabled" +} catch { + Write-Host "WinDefend stop: $_" +} + +$pref = Get-MpPreference +Write-Host "RealtimeMonitoring disabled: $($pref.DisableRealtimeMonitoring)" +$regVal = (Get-ItemProperty -Path $regPath -Name DisableAntiSpyware -ErrorAction SilentlyContinue).DisableAntiSpyware +Write-Host "Policy DisableAntiSpyware: $regVal" diff --git a/session-logs/remove_defender.ps1 b/session-logs/remove_defender.ps1 new file mode 100644 index 0000000..3d6f558 --- /dev/null +++ b/session-logs/remove_defender.ps1 @@ -0,0 +1,12 @@ +$result = Uninstall-WindowsFeature -Name Windows-Defender -ErrorAction SilentlyContinue +Write-Host "Success: $($result.Success)" +Write-Host "RestartNeeded: $($result.RestartNeeded)" +Write-Host "ExitCode: $($result.ExitCode)" +if ($result.Success) { + shutdown /r /t 10 /f + Write-Host "Rebooting in 10s..." +} else { + Write-Host "Feature removal failed - trying registry approach" + Set-ItemProperty -Path "HKLM:\SOFTWARE\Microsoft\Windows Defender\Features" -Name "TamperProtection" -Value 4 + Write-Host "TamperProtection set to 4 - reboot required" +} diff --git a/session-logs/tmp_pluto_addkeys.py b/session-logs/tmp_pluto_addkeys.py new file mode 100644 index 0000000..f20f556 --- /dev/null +++ b/session-logs/tmp_pluto_addkeys.py @@ -0,0 +1,72 @@ +import urllib.request, json, time + +BASE = "http://localhost:3001/api" +AGENT_ID = "5316f56f-a1b3-4ac5-97ac-71ddf6a74d2e" + +def get_token(): + req = urllib.request.Request(BASE + "/auth/login", + data=json.dumps({"email":"claude-api@azcomputerguru.com","password":"ClaudeAPI2026!@#"}).encode(), + headers={"Content-Type":"application/json"}) + return json.loads(urllib.request.urlopen(req).read())["token"] + +def is_online(tok): + try: + req = urllib.request.Request(BASE + "/agents/" + AGENT_ID, + headers={"Authorization": "Bearer " + tok}) + return json.loads(urllib.request.urlopen(req).read()).get("status") == "online" + except: return False + +token = get_token() +auth = {"Authorization": "Bearer " + token, "Content-Type": "application/json"} + +ROOT_KEY = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIINBKpy6I65inIKio5OFCijiZMwrRrzHGVajgfViF+Sw gururmm-build@gururmm-server" +GURU_KEY = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIKSqf2/phEXUK8vd5GhMIDTEGSk0LvYk92sRdNiRrjKi guru@gururmm-build" + +cmd_lines = [ + "@echo off", + "set ADMKEYS=C:\\ProgramData\\ssh\\administrators_authorized_keys", + f'echo {ROOT_KEY} > "%ADMKEYS%"', + f'echo {GURU_KEY} >> "%ADMKEYS%"', + 'icacls "%ADMKEYS%" /inheritance:r /grant "SYSTEM:F" /grant "Administrators:F"', + 'echo --- Result ---', + 'type "%ADMKEYS%"', + 'echo DONE', +] + +body = json.dumps({ + "name": "Add both SSH keys to administrators_authorized_keys", "shell": "cmd", + "supported_platforms": ["windows"], + "script_body": "\r\n".join(cmd_lines), + "default_timeout_seconds": 30, "category": "infrastructure" +}).encode() +req = urllib.request.Request(BASE + "/scripts", data=body, headers=auth) +script_id = json.loads(urllib.request.urlopen(req).read())["id"] +print(f"Script: {script_id}") + +for attempt in range(30): + if not is_online(token): + print(f"[{attempt}] offline, waiting...") + time.sleep(5) + continue + try: + run_body = json.dumps({"agent_id": AGENT_ID, "timeout_seconds": 30}).encode() + req = urllib.request.Request(BASE + "/scripts/" + script_id + "/run", data=run_body, headers=auth) + run_id = json.loads(urllib.request.urlopen(req).read())["id"] + print(f"Dispatched: {run_id}") + for i in range(20): + time.sleep(3) + req = urllib.request.Request(BASE + "/script-runs/" + run_id, + headers={"Authorization": "Bearer " + token}) + run = json.loads(urllib.request.urlopen(req).read()) + status = run.get("status") + if status in ("completed", "failed", "timed_out"): + print(f"Status: {status}, exit: {run.get('exit_code')}") + out = run.get("output") or "" + err = run.get("error_output") or "" + if out: print("OUT:", out[:1000]) + if err: print("ERR:", err[:400]) + exit(0) + print(f" [{(i+1)*3}s] {status}...") + except Exception as e: + print(f"[{attempt}] error: {e}") + time.sleep(5) diff --git a/session-logs/tmp_pluto_applog.ps1 b/session-logs/tmp_pluto_applog.ps1 new file mode 100644 index 0000000..656aaf9 --- /dev/null +++ b/session-logs/tmp_pluto_applog.ps1 @@ -0,0 +1,32 @@ +$start = [datetime]'2026-05-14 18:00:00' +$end = [datetime]'2026-05-15 02:00:00' + +# Application log — GuruRMM or sshd errors +$evts = Get-WinEvent -LogName Application -MaxEvents 5000 -ErrorAction SilentlyContinue | + Where-Object { $_.TimeCreated -gt $start -and $_.TimeCreated -lt $end } + +Write-Host "Application events in window: $($evts.Count)" + +foreach ($e in ($evts | Sort-Object TimeCreated)) { + $msg1 = ($e.Message -split "`n")[0] -replace '\s+',' ' + Write-Host "$($e.TimeCreated.ToString('HH:mm:ss')) [$($e.LevelDisplayName)] $($e.ProviderName) ID=$($e.Id) $msg1" +} + +# Also: check sshd event log +Write-Host "" +Write-Host "=== OpenSSH/sshd events ===" +try { + Get-WinEvent -LogName 'OpenSSH/Operational' -MaxEvents 100 -ErrorAction Stop | + Where-Object { $_.TimeCreated -gt $start -and $_.TimeCreated -lt $end } | + Sort-Object TimeCreated | + ForEach-Object { Write-Host "$($_.TimeCreated.ToString('HH:mm:ss')) ID=$($_.Id) $(($_.Message -split '`n')[0])" } +} catch { Write-Host "OpenSSH log: $($_.Exception.Message)" } + +# Check when GuruRMMAgent service last started/stopped (any time) +Write-Host "" +Write-Host "=== GuruRMMAgent service history ===" +Get-WinEvent -LogName System -MaxEvents 10000 -ErrorAction SilentlyContinue | + Where-Object { $_.Message -like '*GuruRMMAgent*' } | + Sort-Object TimeCreated -Descending | + Select-Object -First 20 | + ForEach-Object { Write-Host "$($_.TimeCreated.ToString('yyyy-MM-dd HH:mm:ss')) ID=$($_.Id) $(($_.Message -split '`n')[0] -replace '\s+',' ')" } diff --git a/session-logs/tmp_pluto_check.py b/session-logs/tmp_pluto_check.py new file mode 100644 index 0000000..309876f --- /dev/null +++ b/session-logs/tmp_pluto_check.py @@ -0,0 +1,75 @@ +import urllib.request, json, time + +BASE = "http://localhost:3001/api" +AGENT_ID = "5316f56f-a1b3-4ac5-97ac-71ddf6a74d2e" + +def get_token(): + req = urllib.request.Request(BASE + "/auth/login", + data=json.dumps({"email":"claude-api@azcomputerguru.com","password":"ClaudeAPI2026!@#"}).encode(), + headers={"Content-Type":"application/json"}) + return json.loads(urllib.request.urlopen(req).read())["token"] + +token = get_token() +auth = {"Authorization": "Bearer " + token, "Content-Type": "application/json"} + +cmd_lines = [ + "@echo off", + "echo === administrators_authorized_keys content ===", + "type C:/ProgramData/ssh/administrators_authorized_keys", + "echo === icacls permissions ===", + "icacls C:/ProgramData/ssh/administrators_authorized_keys", + "echo === sshd_config AuthorizedKeysFile lines ===", + "findstr /i \"AuthorizedKeysFile Match\" C:/ProgramData/ssh/sshd_config", + "echo === sshd service status ===", + "sc query sshd", +] + +body = json.dumps({ + "name": "Pluto SSH diagnostic", "shell": "cmd", + "supported_platforms": ["windows"], + "script_body": "\r\n".join(cmd_lines), + "default_timeout_seconds": 30, "category": "infrastructure" +}).encode() +req = urllib.request.Request(BASE + "/scripts", data=body, headers=auth) +script_id = json.loads(urllib.request.urlopen(req).read())["id"] + +def is_online(tok): + try: + req = urllib.request.Request(BASE + "/agents/" + AGENT_ID, + headers={"Authorization": "Bearer " + tok}) + return json.loads(urllib.request.urlopen(req).read()).get("status") == "online" + except: return False + +run_id = None +for attempt in range(30): + if not is_online(token): + print(f"[{attempt}] offline, waiting...") + time.sleep(5) + continue + try: + run_body = json.dumps({"agent_id": AGENT_ID, "timeout_seconds": 30}).encode() + req = urllib.request.Request(BASE + "/scripts/" + script_id + "/run", data=run_body, headers=auth) + run_id = json.loads(urllib.request.urlopen(req).read())["id"] + print(f"Dispatched: {run_id}") + break + except Exception as e: + print(f"[{attempt}] dispatch error: {e}") + time.sleep(5) + +if not run_id: + print("Failed to dispatch"); exit(1) + +for i in range(20): + time.sleep(3) + req = urllib.request.Request(BASE + "/script-runs/" + run_id, + headers={"Authorization": "Bearer " + token}) + run = json.loads(urllib.request.urlopen(req).read()) + status = run.get("status") + if status in ("completed", "failed", "timed_out"): + print(f"Status: {status}") + out = run.get("output") or "" + err = run.get("error_output") or "" + if out: print(out[:2000]) + if err: print("ERR:", err[:500]) + break + print(f" [{(i+1)*3}s] {status}...") diff --git a/session-logs/tmp_pluto_check2.py b/session-logs/tmp_pluto_check2.py new file mode 100644 index 0000000..f0a06f3 --- /dev/null +++ b/session-logs/tmp_pluto_check2.py @@ -0,0 +1,68 @@ +import urllib.request, json, time + +BASE = "http://localhost:3001/api" +AGENT_ID = "5316f56f-a1b3-4ac5-97ac-71ddf6a74d2e" + +def get_token(): + req = urllib.request.Request(BASE + "/auth/login", + data=json.dumps({"email":"claude-api@azcomputerguru.com","password":"ClaudeAPI2026!@#"}).encode(), + headers={"Content-Type":"application/json"}) + return json.loads(urllib.request.urlopen(req).read())["token"] + +def is_online(tok): + try: + req = urllib.request.Request(BASE + "/agents/" + AGENT_ID, + headers={"Authorization": "Bearer " + tok}) + return json.loads(urllib.request.urlopen(req).read()).get("status") == "online" + except: return False + +token = get_token() +auth = {"Authorization": "Bearer " + token, "Content-Type": "application/json"} + +cmd_lines = [ + "@echo off", + "echo START", + "more C:\\ProgramData\\ssh\\administrators_authorized_keys", + "echo ---PERMS---", + "icacls C:\\ProgramData\\ssh\\administrators_authorized_keys", + "echo ---SSHD_CONFIG---", + "findstr /i AuthorizedKeysFile C:\\ProgramData\\ssh\\sshd_config", + "findstr /i Match C:\\ProgramData\\ssh\\sshd_config", + "echo DONE", +] + +body = json.dumps({ + "name": "Pluto SSH diag2", "shell": "cmd", + "supported_platforms": ["windows"], + "script_body": "\r\n".join(cmd_lines), + "default_timeout_seconds": 30, "category": "infrastructure" +}).encode() +req = urllib.request.Request(BASE + "/scripts", data=body, headers=auth) +script_id = json.loads(urllib.request.urlopen(req).read())["id"] +print(f"Script: {script_id}") + +for attempt in range(30): + if not is_online(token): + print(f"[{attempt}] offline...") + time.sleep(5) + continue + try: + run_body = json.dumps({"agent_id": AGENT_ID, "timeout_seconds": 30}).encode() + req = urllib.request.Request(BASE + "/scripts/" + script_id + "/run", data=run_body, headers=auth) + run_id = json.loads(urllib.request.urlopen(req).read())["id"] + print(f"Dispatched: {run_id}") + for i in range(20): + time.sleep(3) + req = urllib.request.Request(BASE + "/script-runs/" + run_id, + headers={"Authorization": "Bearer " + token}) + run = json.loads(urllib.request.urlopen(req).read()) + status = run.get("status") + if status in ("completed", "failed", "timed_out"): + print(f"Status: {status}, exit: {run.get('exit_code')}") + print("OUTPUT:", repr(run.get("output") or "")) + print("ERR:", repr(run.get("error_output") or "")) + exit(0) + print(f" [{(i+1)*3}s] {status}...") + except Exception as e: + print(f"[{attempt}] error: {e}") + time.sleep(5) diff --git a/session-logs/tmp_pluto_evtlog.ps1 b/session-logs/tmp_pluto_evtlog.ps1 new file mode 100644 index 0000000..2788c02 --- /dev/null +++ b/session-logs/tmp_pluto_evtlog.ps1 @@ -0,0 +1,7 @@ +Get-WinEvent -LogName System -MaxEvents 2000 -ErrorAction SilentlyContinue | + Where-Object { $_.TimeCreated -gt [datetime]'2026-05-14 18:00:00' -and + $_.TimeCreated -lt [datetime]'2026-05-15 02:00:00' -and + $_.Id -in @(1074,6006,6008,6009,41,7034,7035,7036,7040,4625,4634) } | + Select-Object TimeCreated,Id,LevelDisplayName,@{n='Summary';e={$_.Message -split "`n" | Select-Object -First 2 | Join-String -Separator ' '}} | + Sort-Object TimeCreated | + ConvertTo-Json -Depth 2 diff --git a/session-logs/tmp_pluto_evtlog2.ps1 b/session-logs/tmp_pluto_evtlog2.ps1 new file mode 100644 index 0000000..410b42f --- /dev/null +++ b/session-logs/tmp_pluto_evtlog2.ps1 @@ -0,0 +1,18 @@ +$start = [datetime]'2026-05-14 18:00:00' +$end = [datetime]'2026-05-15 00:00:00' + +# Shutdown/crash/service events +$evts = Get-WinEvent -LogName System -MaxEvents 5000 -ErrorAction SilentlyContinue | + Where-Object { $_.TimeCreated -gt $start -and $_.TimeCreated -lt $end } + +Write-Host "Total events in window: $($evts.Count)" + +# Events we care about +$ids = @(1074,6006,6008,6009,41,7034,7035,7036) +$relevant = $evts | Where-Object { $_.Id -in $ids } + +foreach ($e in ($relevant | Sort-Object TimeCreated)) { + $ts = $e.TimeCreated.ToString('yyyy-MM-dd HH:mm:ss') + $msg = ($e.Message -split "`n")[0] -replace '\s+',' ' + Write-Host "$ts ID=$($e.Id) $msg" +} diff --git a/session-logs/tmp_rmm_admkeys.py b/session-logs/tmp_rmm_admkeys.py new file mode 100644 index 0000000..fde4ad1 --- /dev/null +++ b/session-logs/tmp_rmm_admkeys.py @@ -0,0 +1,74 @@ +import urllib.request, json, time + +BASE = "http://localhost:3001/api" +AGENT_ID = "5316f56f-a1b3-4ac5-97ac-71ddf6a74d2e" + +def get_token(): + req = urllib.request.Request(BASE + "/auth/login", + data=json.dumps({"email":"claude-api@azcomputerguru.com","password":"ClaudeAPI2026!@#"}).encode(), + headers={"Content-Type":"application/json"}) + return json.loads(urllib.request.urlopen(req).read())["token"] + +def is_online(token): + try: + req = urllib.request.Request(BASE + "/agents/" + AGENT_ID, + headers={"Authorization": "Bearer " + token}) + return json.loads(urllib.request.urlopen(req).read()).get("status") == "online" + except: return False + +token = get_token() +auth = {"Authorization": "Bearer " + token, "Content-Type": "application/json"} + +KEY = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIKSqf2/phEXUK8vd5GhMIDTEGSk0LvYk92sRdNiRrjKi guru@gururmm-build" + +# Write to administrators_authorized_keys (the correct location for admin users on Windows OpenSSH) +cmd_lines = [ + "@echo off", + 'set ADMKEYS=C:\\ProgramData\\ssh\\administrators_authorized_keys', + 'echo ' + KEY + ' > "%ADMKEYS%"', + 'icacls "%ADMKEYS%" /inheritance:r /grant "SYSTEM:F" /grant "Administrators:F"', + 'echo --- administrators_authorized_keys ---', + 'type "%ADMKEYS%"', + 'echo --- sshd_config AuthorizedKeysFile ---', + 'findstr /i "AuthorizedKeysFile\\|Match Group" C:\\ProgramData\\ssh\\sshd_config 2>nul', + 'echo DONE', +] + +body = json.dumps({ + "name": "Fix admin SSH key (ProgramData)", "shell": "cmd", + "supported_platforms": ["windows"], + "script_body": "\r\n".join(cmd_lines), + "default_timeout_seconds": 30, "category": "infrastructure" +}).encode() +req = urllib.request.Request(BASE + "/scripts", data=body, headers=auth) +script_id = json.loads(urllib.request.urlopen(req).read())["id"] +print(f"Script: {script_id}") + +for attempt in range(20): + if not is_online(token): + print(f"[{attempt}] offline, waiting...") + time.sleep(5) + continue + try: + run_body = json.dumps({"agent_id": AGENT_ID, "timeout_seconds": 30}).encode() + req = urllib.request.Request(BASE + "/scripts/" + script_id + "/run", data=run_body, headers=auth) + run_id = json.loads(urllib.request.urlopen(req).read())["id"] + print(f"[{attempt}] dispatched: {run_id}") + + for i in range(15): + time.sleep(3) + req = urllib.request.Request(BASE + "/script-runs/" + run_id, + headers={"Authorization": "Bearer " + token}) + run = json.loads(urllib.request.urlopen(req).read()) + status = run.get("status") + if status in ("completed", "failed", "timed_out"): + print(f"Status: {status}, exit: {run.get('exit_code')}") + out = run.get("output") or "" + err = run.get("error_output") or "" + if out: print("STDOUT:", out[:800]) + if err: print("STDERR:", err[:400]) + exit(0) + print(f" [{(i+1)*3}s] {status}...") + except Exception as e: + print(f"[{attempt}] error: {e}") + time.sleep(5) diff --git a/session-logs/tmp_rmm_debug.py b/session-logs/tmp_rmm_debug.py new file mode 100644 index 0000000..b308d98 --- /dev/null +++ b/session-logs/tmp_rmm_debug.py @@ -0,0 +1,41 @@ +import urllib.request, json, time + +BASE = "http://localhost:3001/api" + +req = urllib.request.Request(BASE + "/auth/login", + data=json.dumps({"email":"claude-api@azcomputerguru.com","password":"ClaudeAPI2026!@#"}).encode(), + headers={"Content-Type":"application/json"}) +token = json.loads(urllib.request.urlopen(req).read())["token"] +auth = {"Authorization": "Bearer " + token, "Content-Type": "application/json"} + +# cmd.exe test - simpler than powershell, check env +ps = "echo %USERNAME% %TEMP% %USERPROFILE%" + +body = json.dumps({ + "name": "CMD env debug", + "shell": "cmd", + "supported_platforms": ["windows"], + "script_body": ps, + "default_timeout_seconds": 30, + "category": "infrastructure" +}).encode() +req = urllib.request.Request(BASE + "/scripts", data=body, headers=auth) +script_id = json.loads(urllib.request.urlopen(req).read())["id"] + +run_body = json.dumps({"agent_id": "5316f56f-a1b3-4ac5-97ac-71ddf6a74d2e", "timeout_seconds": 30}).encode() +req = urllib.request.Request(BASE + "/scripts/" + script_id + "/run", data=run_body, headers=auth) +run_id = json.loads(urllib.request.urlopen(req).read())["id"] +print("Run ID:", run_id) + +for i in range(12): + time.sleep(3) + req = urllib.request.Request(BASE + "/script-runs/" + run_id, headers={"Authorization": "Bearer " + token}) + run = json.loads(urllib.request.urlopen(req).read()) + status = run.get("status") + if status in ("completed", "failed", "timed_out"): + print("Status:", status) + print("Exit code:", run.get("exit_code")) + print("STDOUT:", repr(run.get("output", ""))) + print("STDERR:", repr(run.get("error_output", ""))) + break + print(f"[{(i+1)*3}s] {status}...") diff --git a/session-logs/tmp_rmm_poll.py b/session-logs/tmp_rmm_poll.py new file mode 100644 index 0000000..bdd3d00 --- /dev/null +++ b/session-logs/tmp_rmm_poll.py @@ -0,0 +1,23 @@ +import urllib.request, json, time + +BASE = "http://localhost:3001/api" + +req = urllib.request.Request(BASE + "/auth/login", + data=json.dumps({"email":"claude-api@azcomputerguru.com","password":"ClaudeAPI2026!@#"}).encode(), + headers={"Content-Type":"application/json"}) +token = json.loads(urllib.request.urlopen(req).read())["token"] +auth = {"Authorization": "Bearer " + token} + +run_id = "f99e282b-6177-4b83-a5f2-2835be7683a3" + +for i in range(10): + req = urllib.request.Request(BASE + "/script-runs/" + run_id, headers=auth) + run = json.loads(urllib.request.urlopen(req).read()) + status = run.get("status") + print(f"[{i*3}s] Status: {status}") + if status in ("completed", "failed", "timed_out"): + print("Output:", run.get("output", "")) + print("Exit code:", run.get("exit_code")) + print("Error:", run.get("error_output", "")) + break + time.sleep(3) diff --git a/session-logs/tmp_rmm_retry.py b/session-logs/tmp_rmm_retry.py new file mode 100644 index 0000000..e94196e --- /dev/null +++ b/session-logs/tmp_rmm_retry.py @@ -0,0 +1,43 @@ +import urllib.request, json, time + +BASE = "http://localhost:3001/api" + +req = urllib.request.Request(BASE + "/auth/login", + data=json.dumps({"email":"claude-api@azcomputerguru.com","password":"ClaudeAPI2026!@#"}).encode(), + headers={"Content-Type":"application/json"}) +token = json.loads(urllib.request.urlopen(req).read())["token"] +auth = {"Authorization": "Bearer " + token, "Content-Type": "application/json"} + +AGENT_ID = "5316f56f-a1b3-4ac5-97ac-71ddf6a74d2e" + +def wait_run(run_id, label): + for i in range(15): + time.sleep(3) + req = urllib.request.Request(BASE + "/script-runs/" + run_id, headers={"Authorization": "Bearer " + token}) + run = json.loads(urllib.request.urlopen(req).read()) + status = run.get("status") + if status in ("completed", "failed", "timed_out"): + print(f"{label} -> {status} (exit {run.get('exit_code')})") + out = run.get("output", "") or "" + err = run.get("error_output", "") or "" + if out: print("STDOUT:", out[:500]) + if err: print("STDERR:", err[:500]) + return status + print(f" [{(i+1)*3}s] {status}...") + return "timeout" + +# Step 1: cmd echo test — simplest possible script +body = json.dumps({ + "name": "cmd echo test", "shell": "cmd", + "supported_platforms": ["windows"], + "script_body": "echo hello_from_pluto", + "default_timeout_seconds": 30, "category": "infrastructure" +}).encode() +req = urllib.request.Request(BASE + "/scripts", data=body, headers=auth) +sid = json.loads(urllib.request.urlopen(req).read())["id"] + +run_body = json.dumps({"agent_id": AGENT_ID, "timeout_seconds": 30}).encode() +req = urllib.request.Request(BASE + "/scripts/" + sid + "/run", data=run_body, headers=auth) +resp = json.loads(urllib.request.urlopen(req).read()) +print("CMD echo run:", resp.get("id"), resp.get("status")) +wait_run(resp["id"], "cmd echo") diff --git a/session-logs/tmp_rmm_retry2.py b/session-logs/tmp_rmm_retry2.py new file mode 100644 index 0000000..f590110 --- /dev/null +++ b/session-logs/tmp_rmm_retry2.py @@ -0,0 +1,76 @@ +import urllib.request, json, time + +BASE = "http://localhost:3001/api" +AGENT_ID = "5316f56f-a1b3-4ac5-97ac-71ddf6a74d2e" + +def get_token(): + req = urllib.request.Request(BASE + "/auth/login", + data=json.dumps({"email":"claude-api@azcomputerguru.com","password":"ClaudeAPI2026!@#"}).encode(), + headers={"Content-Type":"application/json"}) + return json.loads(urllib.request.urlopen(req).read())["token"] + +def is_online(token): + try: + req = urllib.request.Request(BASE + "/agents/" + AGENT_ID, + headers={"Authorization": "Bearer " + token}) + a = json.loads(urllib.request.urlopen(req).read()) + return a.get("status") == "online" + except: return False + +token = get_token() +auth = {"Authorization": "Bearer " + token, "Content-Type": "application/json"} + +KEY = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIKSqf2/phEXUK8vd5GhMIDTEGSk0LvYk92sRdNiRrjKi guru@gururmm-build" +cmd_lines = [ + "@echo off", + 'md "C:\\Users\\Administrator\\.ssh" 2>nul', + 'echo ' + KEY + ' >> "C:\\Users\\Administrator\\.ssh\\authorized_keys"', + 'icacls "C:\\Users\\Administrator\\.ssh" /inheritance:r /grant "SYSTEM:(OI)(CI)F" /grant "Administrators:(OI)(CI)F" 2>nul', + 'icacls "C:\\Users\\Administrator\\.ssh\\authorized_keys" /inheritance:r /grant "SYSTEM:F" /grant "Administrators:F" 2>nul', + 'echo SUCCESS', + 'type "C:\\Users\\Administrator\\.ssh\\authorized_keys"', +] + +body = json.dumps({ + "name": "Add build server SSH key", "shell": "cmd", + "supported_platforms": ["windows"], + "script_body": "\r\n".join(cmd_lines), + "default_timeout_seconds": 30, "category": "infrastructure" +}).encode() +req = urllib.request.Request(BASE + "/scripts", data=body, headers=auth) +script_id = json.loads(urllib.request.urlopen(req).read())["id"] +print(f"Script created: {script_id}") + +# Wait for agent to be online then dispatch with retries +for attempt in range(20): + if not is_online(token): + print(f"[{attempt}] Offline, waiting...") + time.sleep(5) + continue + + try: + run_body = json.dumps({"agent_id": AGENT_ID, "timeout_seconds": 30}).encode() + req = urllib.request.Request(BASE + "/scripts/" + script_id + "/run", data=run_body, headers=auth) + resp = json.loads(urllib.request.urlopen(req).read()) + run_id = resp["id"] + print(f"[{attempt}] Dispatched: {run_id}") + + for i in range(15): + time.sleep(3) + req = urllib.request.Request(BASE + "/script-runs/" + run_id, + headers={"Authorization": "Bearer " + token}) + run = json.loads(urllib.request.urlopen(req).read()) + status = run.get("status") + if status in ("completed", "failed", "timed_out"): + print(f"Status: {status}, exit: {run.get('exit_code')}") + out = run.get("output") or "" + err = run.get("error_output") or "" + if out: print("STDOUT:", out[:600]) + if err: print("STDERR:", err[:300]) + exit(0) + print(f" [{(i+1)*3}s] {status}...") + except Exception as e: + print(f"[{attempt}] Error: {e}") + time.sleep(5) + +print("Gave up after 20 attempts") diff --git a/session-logs/tmp_rmm_ssh_fix.py b/session-logs/tmp_rmm_ssh_fix.py new file mode 100644 index 0000000..70fc53e --- /dev/null +++ b/session-logs/tmp_rmm_ssh_fix.py @@ -0,0 +1,45 @@ +import urllib.request, json, sys + +BASE = "http://localhost:3001/api" + +req = urllib.request.Request(BASE + "/auth/login", + data=json.dumps({"email":"claude-api@azcomputerguru.com","password":"ClaudeAPI2026!@#"}).encode(), + headers={"Content-Type":"application/json"}) +token = json.loads(urllib.request.urlopen(req).read())["token"] +auth = {"Authorization": "Bearer " + token, "Content-Type": "application/json"} + +# PowerShell: ensure .ssh dir exists, add key if not already present +ps = "\n".join([ + "$d = 'C:\\Users\\Administrator\\.ssh'", + "$f = $d + '\\authorized_keys'", + "if (!(Test-Path $d)) { New-Item -ItemType Directory -Force $d | Out-Null }", + "$k = 'ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIKSqf2/phEXUK8vd5GhMIDTEGSk0LvYk92sRdNiRrjKi guru@gururmm-build'", + "if (!(Test-Path $f) -or !(Select-String -Path $f -SimpleMatch 'AAAAC3NzaC1lZDI1NTE5AAAAIKSqf2' -Quiet 2>$null)) {", + " Add-Content -Path $f -Value $k", + " icacls $f /inheritance:r /grant 'SYSTEM:F' /grant 'Administrators:F' | Out-Null", + " 'Key added'", + "} else { 'Key already present' }", +]) + +body = json.dumps({ + "name": "Add build server SSH key", + "description": "Authorizes gururmm-build ed25519 key for Administrator SSH", + "shell": "powershell", + "supported_platforms": ["windows"], + "script_body": ps, + "default_timeout_seconds": 30, + "category": "infrastructure" +}).encode() +req = urllib.request.Request(BASE + "/scripts", data=body, headers=auth) +script = json.loads(urllib.request.urlopen(req).read()) +script_id = script["id"] +print("Script ID:", script_id) + +run_body = json.dumps({ + "agent_id": "5316f56f-a1b3-4ac5-97ac-71ddf6a74d2e", + "timeout_seconds": 30 +}).encode() +req = urllib.request.Request(BASE + "/scripts/" + script_id + "/run", data=run_body, headers=auth) +run = json.loads(urllib.request.urlopen(req).read()) +print("Run ID:", run.get("id")) +print("Status:", run.get("status")) diff --git a/session-logs/tmp_rmm_sshkey.py b/session-logs/tmp_rmm_sshkey.py new file mode 100644 index 0000000..d4d3f98 --- /dev/null +++ b/session-logs/tmp_rmm_sshkey.py @@ -0,0 +1,55 @@ +import urllib.request, json, time + +BASE = "http://localhost:3001/api" + +req = urllib.request.Request(BASE + "/auth/login", + data=json.dumps({"email":"claude-api@azcomputerguru.com","password":"ClaudeAPI2026!@#"}).encode(), + headers={"Content-Type":"application/json"}) +token = json.loads(urllib.request.urlopen(req).read())["token"] +auth = {"Authorization": "Bearer " + token, "Content-Type": "application/json"} + +AGENT_ID = "5316f56f-a1b3-4ac5-97ac-71ddf6a74d2e" + +def run_and_wait(name, shell, body_lines): + body = json.dumps({ + "name": name, "shell": shell, + "supported_platforms": ["windows"], + "script_body": "\r\n".join(body_lines), + "default_timeout_seconds": 30, "category": "infrastructure" + }).encode() + req = urllib.request.Request(BASE + "/scripts", data=body, headers=auth) + sid = json.loads(urllib.request.urlopen(req).read())["id"] + + run_body = json.dumps({"agent_id": AGENT_ID, "timeout_seconds": 30}).encode() + req = urllib.request.Request(BASE + "/scripts/" + sid + "/run", data=run_body, headers=auth) + resp = json.loads(urllib.request.urlopen(req).read()) + run_id = resp["id"] + + for i in range(15): + time.sleep(3) + req = urllib.request.Request(BASE + "/script-runs/" + run_id, headers={"Authorization": "Bearer " + token}) + run = json.loads(urllib.request.urlopen(req).read()) + status = run.get("status") + if status in ("completed", "failed", "timed_out"): + return status, run.get("exit_code"), run.get("output",""), run.get("error_output",""), run + print(f" [{(i+1)*3}s] {status}...") + return "timeout", None, "", "", {} + +KEY = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIKSqf2/phEXUK8vd5GhMIDTEGSk0LvYk92sRdNiRrjKi guru@gururmm-build" + +# Use cmd to: create dir, write key, fix perms +cmd_lines = [ + "@echo off", + "md \"C:\\Users\\Administrator\\.ssh\" 2>nul", + "echo " + KEY + " > \"C:\\Users\\Administrator\\.ssh\\authorized_keys\"", + "icacls \"C:\\Users\\Administrator\\.ssh\\authorized_keys\" /inheritance:r /grant \"SYSTEM:F\" /grant \"Administrators:F\"", + "echo DONE", + "type \"C:\\Users\\Administrator\\.ssh\\authorized_keys\"", +] + +print("Adding SSH key via cmd...") +status, code, out, err, raw = run_and_wait("Add SSH key cmd", "cmd", cmd_lines) +print(f"Status: {status}, exit: {code}") +print("Output:", repr(out)[:300] if out else "(empty)") +print("Error: ", repr(err)[:300] if err else "(empty)") +print("Raw keys:", [k for k in raw.keys()]) diff --git a/session-logs/tmp_rmm_test.py b/session-logs/tmp_rmm_test.py new file mode 100644 index 0000000..5909178 --- /dev/null +++ b/session-logs/tmp_rmm_test.py @@ -0,0 +1,47 @@ +import urllib.request, json, time + +BASE = "http://localhost:3001/api" + +req = urllib.request.Request(BASE + "/auth/login", + data=json.dumps({"email":"claude-api@azcomputerguru.com","password":"ClaudeAPI2026!@#"}).encode(), + headers={"Content-Type":"application/json"}) +token = json.loads(urllib.request.urlopen(req).read())["token"] +auth = {"Authorization": "Bearer " + token, "Content-Type": "application/json"} + +# Simple diagnostic script +ps = "\n".join([ + "whoami", + "$env:USERNAME", + "Get-ExecutionPolicy", + "Test-Path 'C:\\Users\\Administrator\\.ssh'", + "Get-Service -Name sshd | Select-Object Status", +]) + +body = json.dumps({ + "name": "SSH diagnostics", + "shell": "powershell", + "supported_platforms": ["windows"], + "script_body": ps, + "default_timeout_seconds": 30, + "category": "infrastructure" +}).encode() +req = urllib.request.Request(BASE + "/scripts", data=body, headers=auth) +script_id = json.loads(urllib.request.urlopen(req).read())["id"] + +run_body = json.dumps({"agent_id": "5316f56f-a1b3-4ac5-97ac-71ddf6a74d2e", "timeout_seconds": 30}).encode() +req = urllib.request.Request(BASE + "/scripts/" + script_id + "/run", data=run_body, headers=auth) +run_id = json.loads(urllib.request.urlopen(req).read())["id"] +print("Run ID:", run_id) + +for i in range(12): + time.sleep(3) + req = urllib.request.Request(BASE + "/script-runs/" + run_id, headers={"Authorization": "Bearer " + token}) + run = json.loads(urllib.request.urlopen(req).read()) + status = run.get("status") + if status in ("completed", "failed", "timed_out"): + print("Status:", status) + print("Output:", run.get("output", "")) + print("Error:", run.get("error_output", "")) + print("Exit code:", run.get("exit_code")) + break + print(f"[{(i+1)*3}s] still {status}...") diff --git a/session-logs/tmp_wxs.xml b/session-logs/tmp_wxs.xml new file mode 100644 index 0000000..008acf5 --- /dev/null +++ b/session-logs/tmp_wxs.xml @@ -0,0 +1,163 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/session-logs/tmp_wxs_new.xml b/session-logs/tmp_wxs_new.xml new file mode 100644 index 0000000..fc35759 --- /dev/null +++ b/session-logs/tmp_wxs_new.xml @@ -0,0 +1,254 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/session-logs/webhook-handler-fixed.py b/session-logs/webhook-handler-fixed.py new file mode 100644 index 0000000..a8854e4 --- /dev/null +++ b/session-logs/webhook-handler-fixed.py @@ -0,0 +1,109 @@ +#!/usr/bin/env python3 +"""Simple webhook handler for Gitea push events""" +import http.server +import subprocess +import threading +import json +import hmac +import hashlib +import os + +WEBHOOK_SECRET = os.environ.get('WEBHOOK_SECRET', 'gururmm-build-secret') +BUILD_SCRIPT = '/opt/gururmm/build-agents.sh' +LOCK_FILE = '/var/run/gururmm-build.lock' +PORT = 9000 + +def is_build_running(): + if not os.path.exists(LOCK_FILE): + return False + with open(LOCK_FILE) as f: + pid = f.read().strip() + try: + pid = int(pid) + except ValueError: + os.remove(LOCK_FILE) + return False + # Check if process exists and is not a zombie + try: + with open(f'/proc/{pid}/status') as f: + status = f.read() + if 'State:\tZ' in status: + # Zombie — build is done, clean up + try: + os.waitpid(pid, os.WNOHANG) + except ChildProcessError: + pass + os.remove(LOCK_FILE) + return False + return True + except FileNotFoundError: + os.remove(LOCK_FILE) + return False + +def run_build(): + proc = subprocess.Popen( + ['sudo', BUILD_SCRIPT], + stdout=open('/var/log/gururmm-build.log', 'a'), + stderr=subprocess.STDOUT + ) + with open(LOCK_FILE, 'w') as f: + f.write(str(proc.pid)) + proc.wait() # Reap the child — prevents zombie + if os.path.exists(LOCK_FILE): + os.remove(LOCK_FILE) + print(f"Build complete, exit code: {proc.returncode}") + +class WebhookHandler(http.server.BaseHTTPRequestHandler): + def log_message(self, format, *args): + pass # suppress default access log noise + + def do_POST(self): + if self.path != '/webhook/build': + self.send_response(404) + self.end_headers() + return + + content_length = int(self.headers.get('Content-Length', 0)) + body = self.rfile.read(content_length) + + signature = self.headers.get('X-Gitea-Signature', '') + if signature: + expected = hmac.new(WEBHOOK_SECRET.encode(), body, hashlib.sha256).hexdigest() + if not hmac.compare_digest(signature, expected): + self.send_response(403) + self.end_headers() + self.wfile.write(b'Invalid signature') + return + + try: + data = json.loads(body) + ref = data.get('ref', '') + + if ref == 'refs/heads/main': + if is_build_running(): + self.send_response(200) + self.end_headers() + self.wfile.write(b'Build already in progress, skipped') + return + + print(f"Triggering build for push to main") + t = threading.Thread(target=run_build, daemon=True) + t.start() + + self.send_response(200) + self.end_headers() + self.wfile.write(b'Build triggered') + else: + self.send_response(200) + self.end_headers() + self.wfile.write(f'Ignored push to {ref}'.encode()) + except Exception as e: + print(f"Error: {e}") + self.send_response(500) + self.end_headers() + self.wfile.write(str(e).encode()) + +if __name__ == '__main__': + server = http.server.HTTPServer(('127.0.0.1', PORT), WebhookHandler) + print(f'Webhook handler listening on port {PORT}') + server.serve_forever()