sync: Multi-project updates - SolverBot, GuruRMM, Dataforth

SolverBot:
- Inject active project path into agent system prompts so agents
  know which directory to scope file operations to

GuruRMM:
- Bump agent version to 0.6.0
- Add serde aliases for PowerShell/ClaudeTask command types
- Add typed CommandType enum on server for proper serialization
- Support claude_task command type in send_command API

Dataforth:
- Fix SCP space-escaping in Sync-FromNAS.ps1

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-02-18 16:16:18 -07:00
parent 6d3582d5dc
commit 8b6f0bcc96
37 changed files with 1544 additions and 15 deletions

View File

@@ -68,9 +68,9 @@ function Copy-FromNAS {
New-Item -ItemType Directory -Path $localDir -Force | Out-Null
}
# FIX: Use -i for key auth, remove embedded quotes around remote path
# DOS 8.3 filenames have no spaces - embedded quotes caused "ambiguous target"
$result = & $SCP -O -i $SSH_KEY -o StrictHostKeyChecking=accept-new -o UserKnownHostsFile="C:\Shares\test\scripts\.ssh\known_hosts" "${NAS_USER}@${NAS_IP}:${RemotePath}" "$LocalPath" 2>&1
# Escape spaces in remote path for SCP (unescaped spaces cause "ambiguous target")
$escapedRemote = $RemotePath -replace ' ', '\ '
$result = & $SCP -O -i $SSH_KEY -o StrictHostKeyChecking=accept-new -o UserKnownHostsFile="C:\Shares\test\scripts\.ssh\known_hosts" "${NAS_USER}@${NAS_IP}:${escapedRemote}" "$LocalPath" 2>&1
if ($LASTEXITCODE -ne 0) {
$errorMsg = $result | Out-String
Write-Log " SCP PULL ERROR (exit $LASTEXITCODE): $errorMsg"
@@ -93,9 +93,9 @@ function Copy-ToNAS {
$remoteDir = ($RemotePath -replace '[^/]+$', '').TrimEnd('/')
Invoke-NASCommand "mkdir -p '$remoteDir'" | Out-Null
# FIX: Use -i for key auth, remove embedded quotes around remote path
# DOS 8.3 filenames have no spaces - embedded quotes caused "ambiguous target"
$result = & $SCP -O -i $SSH_KEY -o StrictHostKeyChecking=accept-new -o UserKnownHostsFile="C:\Shares\test\scripts\.ssh\known_hosts" "$LocalPath" "${NAS_USER}@${NAS_IP}:${RemotePath}" 2>&1
# Escape spaces in remote path for SCP (unescaped spaces cause "ambiguous target")
$escapedRemote = $RemotePath -replace ' ', '\ '
$result = & $SCP -O -i $SSH_KEY -o StrictHostKeyChecking=accept-new -o UserKnownHostsFile="C:\Shares\test\scripts\.ssh\known_hosts" "$LocalPath" "${NAS_USER}@${NAS_IP}:${escapedRemote}" 2>&1
if ($LASTEXITCODE -ne 0) {
$errorMsg = $result | Out-String
Write-Log " SCP PUSH ERROR (exit $LASTEXITCODE): $errorMsg"

View File

@@ -0,0 +1,606 @@
# GuruRMM - Complete Reference Documentation
**Project:** GuruRMM - Remote Monitoring and Management Platform
**Version:** Server 0.2.0 / Agent 0.3.5 (deployed as 0.5.1)
**Last Updated:** 2026-02-17
---
## Table of Contents
1. [Project Overview](#project-overview)
2. [Architecture](#architecture)
3. [API Endpoints](#api-endpoints)
4. [WebSocket Protocol](#websocket-protocol)
5. [Command Execution](#command-execution)
6. [Claude Code Integration](#claude-code-integration)
7. [Agent Configuration](#agent-configuration)
8. [Deployed Agents](#deployed-agents)
9. [Database](#database)
10. [Authentication](#authentication)
11. [Auto-Update System](#auto-update-system)
12. [Known Issues](#known-issues)
13. [Development](#development)
14. [File Structure](#file-structure)
---
## Project Overview
GuruRMM is a Remote Monitoring and Management (RMM) platform built entirely in Rust. It provides real-time agent monitoring, remote command execution, system metrics collection, and service watchdog capabilities for managed IT environments.
### Technology Stack
| Component | Technology | Version |
|------------|-----------------------------------------|---------|
| Server | Rust (Axum 0.7, SQLx 0.8, PostgreSQL) | 0.2.0 |
| Agent | Rust (cross-platform, native service) | 0.3.5 (deployed as 0.5.1) |
| Dashboard | React + TypeScript + Vite | -- |
| Real-time | WebSocket (tokio-tungstenite) | -- |
| Database | PostgreSQL | -- |
---
## Architecture
### Server
- **Internal Address:** 172.16.3.30:3001
- **Production URL:** https://rmm-api.azcomputerguru.com
- **WebSocket Endpoint:** wss://rmm-api.azcomputerguru.com/ws
- **Database:** PostgreSQL (same server)
- **Service:** systemd unit `gururmm-server`
- **Source:** `D:\ClaudeTools\projects\msp-tools\guru-rmm\server\`
### Agent
- **Windows Service Name:** GuruRMM (uses native-service feature)
- **Legacy Mode:** NSSM wrapper for Windows 7 / Server 2008 R2
- **Config Path:** `C:\ProgramData\GuruRMM\agent.toml`
- **Binary Path:** `C:\Program Files\GuruRMM\gururmm-agent.exe`
- **Source:** `D:\ClaudeTools\projects\msp-tools\guru-rmm\agent\`
### Communication Model
```
+-------------------+ WebSocket (persistent, bidirectional) +-------------------+
| GuruRMM Agent | <-----------------------------------------------> | GuruRMM Server |
| (Windows/Linux) | | (Axum + Tokio) |
+-------------------+ +-------------------+
|
| REST API (JWT)
v
+-------------------+
| Dashboard |
| (React + TS) |
+-------------------+
```
- **Primary:** WebSocket -- persistent bidirectional connection between agent and server
- **Legacy Fallback:** REST heartbeat polling -- [WARNING] NOT FULLY IMPLEMENTED
- **Auth:** API key sent in initial WebSocket authentication message
- **Site-Based Auth:** WORD-WORD-NUMBER format site codes combined with device_id
---
## API Endpoints
### Authentication
| Method | Path | Description | Auth Required |
|--------|--------------------|----------------------------|---------------|
| POST | /api/auth/login | User login (email/password -> JWT) | No |
| POST | /api/auth/register | User registration | No (disabled) |
| GET | /api/auth/me | Get current user info | Yes |
### Clients
| Method | Path | Description | Auth Required |
|--------|-------------------------|-------------------------|---------------|
| GET | /api/clients | List all clients | Yes |
| POST | /api/clients | Create client | Yes |
| GET | /api/clients/:id | Get client by ID | Yes |
| PUT | /api/clients/:id | Update client | Yes |
| DELETE | /api/clients/:id | Delete client | Yes |
| GET | /api/clients/:id/sites | List client's sites | Yes |
### Sites
| Method | Path | Description | Auth Required |
|--------|--------------------------------|--------------------------|---------------|
| GET | /api/sites | List all sites | Yes |
| POST | /api/sites | Create site | Yes |
| GET | /api/sites/:id | Get site by ID | Yes |
| PUT | /api/sites/:id | Update site | Yes |
| DELETE | /api/sites/:id | Delete site | Yes |
| POST | /api/sites/:id/regenerate-key | Regenerate site API key | Yes |
### Agents
| Method | Path | Description | Auth Required |
|--------|--------------------------|--------------------------------------|---------------|
| GET | /api/agents | List all agents | Yes |
| POST | /api/agents | Register agent (authenticated) | Yes |
| GET | /api/agents/stats | Agent statistics | Yes |
| GET | /api/agents/unassigned | List unassigned agents | Yes |
| GET | /api/agents/:id | Get agent details | Yes |
| DELETE | /api/agents/:id | Delete agent | Yes |
| POST | /api/agents/:id/move | Move agent to different site | Yes |
| GET | /api/agents/:id/state | Get agent state (network, metrics) | Yes |
### Commands
| Method | Path | Description | Auth Required |
|--------|----------------------------|----------------------------|---------------|
| POST | /api/agents/:id/command | Send command to agent | Yes |
| GET | /api/commands | List recent commands | Yes |
| GET | /api/commands/:id | Get command status/result | Yes |
### Metrics
| Method | Path | Description | Auth Required |
|--------|----------------------------|---------------------------|---------------|
| GET | /api/agents/:id/metrics | Get agent metrics history | Yes |
| GET | /api/metrics/summary | Metrics summary | Yes |
### Legacy Agent Endpoints
These endpoints do **not** require JWT authentication. They are used by agents in legacy polling mode.
| Method | Path | Description | Auth Required |
|--------|-------------------------------|------------------------------|---------------|
| POST | /api/agent/register-legacy | Register with site code | No |
| POST | /api/agent/heartbeat | Agent heartbeat | No |
| POST | /api/agent/command-result | Submit command result | No |
[WARNING] Legacy heartbeat returns empty `pending_commands` -- not implemented (agents.rs line 334).
[WARNING] Legacy command-result endpoint does not store results (agents.rs lines 354-360).
### WebSocket
| Method | Path | Description | Auth Required |
|--------|------|------------------------|---------------------|
| GET | /ws | WebSocket upgrade | API key in auth msg |
---
## WebSocket Protocol
### Connection Flow
1. Client initiates WebSocket upgrade to `wss://rmm-api.azcomputerguru.com/ws`
2. Agent sends authentication message with API key and device info
3. Server validates API key (SHA256 hash match or site code lookup)
4. On success, server registers the WebSocket connection for the agent
5. Bidirectional message exchange begins
### Message Types
**Agent -> Server:**
- `Auth` -- Initial authentication payload (api_key, hostname, os_info, version)
- `Heartbeat` -- Periodic keepalive
- `MetricsReport` -- System metrics (CPU, memory, disk, network)
- `NetworkState` -- Network configuration snapshot (hash-based change detection)
- `CommandResult` -- Result of executed command (exit_code, stdout, stderr, duration)
- `WatchdogEvent` -- Service monitoring event
**Server -> Agent:**
- `AuthResponse` -- Success/failure of authentication
- `Command` -- Command to execute (CommandPayload)
- `Update` -- Auto-update instruction (download_url, checksum)
- `Ping` -- Keepalive ping
---
## Command Execution
### Command Types
| Type | Description | Shell Used |
|--------------|----------------------------------------------|---------------------------------------------|
| shell | System shell command | cmd.exe (Windows), /bin/sh (Unix) |
| powershell | PowerShell command | powershell -NoProfile -NonInteractive -Command |
| python | Python inline code | python -c |
| script | Custom interpreter | Configurable |
| claude_task | Claude Code task execution (special handler) | Claude Code CLI |
### Command Flow
```
1. Dashboard sends POST /api/agents/:id/command
Body: { command_type, command, timeout_seconds, elevated }
2. Server creates command record in database (status = pending)
3. If agent is connected via WebSocket:
-> Server sends command via WebSocket
-> Status updated to "running"
4. If agent is offline:
-> Command stays as "pending" (queued)
5. Agent receives command and executes it
6. Agent sends CommandResult back via WebSocket
-> { id, exit_code, stdout, stderr, duration_ms }
7. Server updates database with result
```
### Command States
| State | Description |
|-----------|------------------------------------------------|
| pending | Created, agent offline or not yet sent |
| running | Sent to agent via WebSocket, awaiting result |
| completed | Agent reported exit_code = 0 |
| failed | Agent reported exit_code != 0 |
### [BUG] Server-Agent Command Type Mismatch
This is a **critical** known bug that prevents all remote command execution.
**Root Cause:**
The server's `CommandPayload` serializes `command_type` as a plain JSON string:
```json
{"command_type": "powershell", "command": "Get-Process", ...}
```
The agent's `CommandPayload` expects `command_type` as a Rust enum (`CommandType::PowerShell`), which serde deserializes from an object or tagged format, not a bare string.
**Result:** Serde deserialization fails silently on the agent side. Commands are never executed. All commands remain in "running" state permanently because no `CommandResult` is ever sent back.
**Fix Required:** Either:
- Change the server to serialize `command_type` in the enum format the agent expects, OR
- Change the agent to accept plain string values for `command_type`
---
## Claude Code Integration
### Architecture
The agent includes a built-in Claude Code executor for running AI-assisted tasks.
- **Singleton:** Global `ClaudeExecutor` via `once_cell::Lazy`
- **Working Directory:** Restricted to `C:\Shares\test\` only
- **Rate Limit:** 10 tasks per hour (sliding window)
- **Max Concurrent:** 2 simultaneous tasks
- **Default Timeout:** 300 seconds (max 600)
- **Input Sanitization:** Blocks `& | ; $ ( ) < > \` \n \r`
### Claude Task Command Format
The server sends:
```json
{
"type": "command",
"payload": {
"id": "uuid",
"command_type": "claude_task",
"command": "task description",
"timeout_seconds": 300,
"elevated": false
}
}
```
[WARNING] This also suffers from the command type mismatch bug. The agent expects `command_type` to be an object for ClaudeTask:
```json
{
"claude_task": {
"task": "...",
"working_directory": "...",
"context_files": [...]
}
}
```
### Exit Codes
| Code | Meaning |
|------|------------------------------------------|
| 0 | Task completed successfully |
| 1 | Task failed |
| 124 | Task timed out |
| -1 | Executor error (rate limit, validation) |
---
## Agent Configuration
### agent.toml Format
```toml
[server]
url = "wss://rmm-api.azcomputerguru.com/ws"
api_key = "SITE-CODE-1234" # or grmm_xxxxx API key
[metrics]
interval_seconds = 60 # Range: 10-3600, default: 60
collect_cpu = true
collect_memory = true
collect_disk = true
collect_network = true
[watchdog]
enabled = true
check_interval_seconds = 30
[[watchdog.services]]
name = "ServiceName"
action = "restart"
max_restarts = 3
restart_cooldown_seconds = 60
```
### Hardcoded Intervals
These values are currently not configurable via `agent.toml`:
| Interval | Value | Notes |
|----------------------------|-------------|--------------------------------|
| Heartbeat | 30 seconds | |
| Network state check | 30 seconds | Uses hash-based change detection |
| Connection idle timeout | 90 seconds | |
| Auth timeout | 10 seconds | |
| Reconnect delay | 10 seconds | |
| Command execution timeout | 300 seconds | Configurable per command |
---
## Deployed Agents
| Hostname | Agent ID (prefix) | Version | OS | Status |
|-------------|--------------------|---------|-----------------------------|---------|
| ACG-M-L5090 | 97f63c3b-... | 0.5.1 | Windows 11 (26200) | online |
| AD2 | d28a1c90-... | 0.5.1 | Windows Server 2016 (14393) | online |
| gururmm | 8cd0440f-... | 0.5.1 | Ubuntu 22.04 | offline |
| SL-SERVER | 2585f6d5-... | 0.5.1 | unknown | offline |
| SL-SERVER | dff818e6-... | 0.5.1 | unknown | online |
---
## Database
### Connection Details
| Parameter | Value |
|-----------|------------------------------------|
| Host | 172.16.3.30 |
| Port | 5432 |
| Database | gururmm |
| User | gururmm |
| Password | 43617ebf7eb242e814ca9988cc4df5ad |
### Key Tables
| Table | Description |
|---------------------|------------------------------------------------|
| users | User accounts (JWT auth, Argon2id hashing) |
| clients | Client organizations |
| sites | Physical locations with API keys |
| agents | RMM agent instances |
| agent_state | Latest agent state (network, metrics snapshot) |
| agent_updates | Agent update tracking |
| alerts | System alerts |
| commands | Remote command execution log |
| metrics | Performance metrics time series |
| policies | Configuration policies |
| registration_tokens | Agent registration tokens |
| watchdog_events | Service monitoring events |
---
## Authentication
### API Authentication (JWT)
1. Send `POST /api/auth/login` with `{ email, password }`
2. Server validates credentials (Argon2id password hash)
3. Returns JWT token (24-hour expiry)
4. Include token in subsequent requests: `Authorization: Bearer <token>`
**Admin Credentials:**
| Field | Value |
|----------|------------------------------------|
| Email | claude-api@azcomputerguru.com |
| Password | ClaudeAPI2026!@# |
### Agent Authentication (API Key)
Two authentication modes:
1. **Direct API Key** -- Agent sends `grmm_xxxxx` format key, server matches against `api_key_hash` (SHA256) in agents table
2. **Site-Based** -- Agent sends site code (WORD-WORD-NUMBER format, e.g., `DARK-GROVE-7839`) combined with `device_id`, server looks up site and registers/matches agent
### SSO (Optional)
- **Provider:** Microsoft Entra ID
- **Client ID:** 18a15f5d-7ab8-46f4-8566-d7b5436b84b6
---
## Auto-Update System
### Update Flow
```
1. Agent connects via WebSocket and sends its version in the auth payload
2. Server checks if a newer version is available for the agent's OS/architecture
3. If update needed:
-> Server sends Update message with download_url and SHA256 checksum
4. Agent downloads the new binary from the download URL
5. Agent verifies the SHA256 checksum
6. Agent replaces its own binary and restarts
7. On reconnection, agent reports previous_version in auth payload
8. Server marks the update as completed
```
### Download Location
- **Server Path:** `/var/www/gururmm/downloads/`
- **Public URL:** `https://rmm-api.azcomputerguru.com/downloads/`
---
## Known Issues
### CRITICAL
| ID | Type | Description |
|----|------------|--------------------------------------------------------------------------------------------|
| 1 | [BUG] | Command type mismatch between server (String) and agent (Enum) -- commands never execute |
| 2 | [TODO] | Legacy heartbeat returns empty pending_commands (agents.rs line 334) |
| 3 | [TODO] | Legacy command-result endpoint does not store results (agents.rs lines 354-360) |
| 4 | [SECURITY] | CORS configured with AllowOrigin::Any -- should be restricted to known origins |
### MAJOR
| ID | Description |
|----|--------------------------------------------------------------------------------|
| 1 | No command timeout enforcement on server side |
| 2 | No retry logic for failed WebSocket sends |
| 3 | Database inconsistency: agent shows "online" but command sends fail silently |
| 4 | Missing database indexes on frequently queried columns |
| 5 | No rate limiting on command submissions |
### MINOR
| ID | Description |
|----|--------------------------------------------------------------------------|
| 1 | Hardcoded intervals (heartbeat, network check) not configurable |
| 2 | Watchdog events logged but not stored in database |
| 3 | No log rotation configured |
| 4 | Unicode characters in agent output (should use ASCII per coding guidelines) |
---
## Development
### Building
```bash
# Server
cd server && cargo build --release
# Agent (Windows, native service mode)
cd agent && cargo build --release
# Agent (Legacy mode for Windows 7 / Server 2008 R2)
cd agent && cargo build --release --features legacy --no-default-features
```
### Testing
```bash
cargo test # Run unit tests
cargo clippy # Run linter
cargo fmt --check # Check formatting
```
### Deploying the Server
```bash
# On gururmm server (172.16.3.30)
systemctl stop gururmm-server
cp target/release/gururmm-server /opt/gururmm/
systemctl start gururmm-server
journalctl -u gururmm-server -f
```
### Deploying the Agent
```cmd
REM On target Windows machine
sc stop GuruRMM
copy gururmm-agent.exe "C:\Program Files\GuruRMM\gururmm-agent.exe"
sc start GuruRMM
```
---
## File Structure
```
D:\ClaudeTools\projects\msp-tools\guru-rmm\
|
+-- agent/
| +-- src/
| | +-- main.rs # Entry point, CLI parsing, service install
| | +-- config.rs # TOML config loading and validation
| | +-- claude.rs # Claude Code executor (rate-limited singleton)
| | +-- service.rs # Windows service handler (native-service feature)
| | +-- device_id.rs # Hardware-based device ID generation
| | +-- transport/
| | | +-- mod.rs # Message types (AgentMessage, ServerMessage, CommandType enum)
| | | +-- websocket.rs # WebSocket client, reconnection, command execution
| | +-- metrics/
| | | +-- mod.rs # System metrics collection, network state hashing
| | +-- updater/
| | +-- mod.rs # Self-update logic (download, verify, replace)
| +-- deploy/ # Deployment configs per site
| +-- Cargo.toml # v0.3.5, features: native-service, legacy
|
+-- server/
| +-- src/
| | +-- main.rs # Axum server setup, router, middleware
| | +-- api/
| | | +-- mod.rs # Route definitions and grouping
| | | +-- agents.rs # Agent management + legacy polling endpoints
| | | +-- commands.rs # Command dispatch and status tracking
| | | +-- auth.rs # JWT login, registration, user info
| | | +-- clients.rs # Client CRUD operations
| | | +-- sites.rs # Site management and API key regeneration
| | | +-- metrics.rs # Metrics query endpoints
| | +-- ws/
| | | +-- mod.rs # WebSocket handler, ServerMessage types, CommandPayload (String type)
| | +-- db/
| | | +-- agents.rs # Agent database operations
| | | +-- commands.rs # Command database operations
| | +-- auth/
| | +-- mod.rs # JWT middleware and token validation
| +-- Cargo.toml # v0.2.0
|
+-- dashboard/ # React frontend (if present)
|
+-- docs/
+-- FEATURE_ROADMAP.md # Complete feature plan (654 lines)
+-- REMEDIATION_PLAN.md # Security and code review (1277 lines)
```
---
## Quick Reference
| Item | Value |
|--------------------|---------------------------------------------|
| Server URL | https://rmm-api.azcomputerguru.com |
| WebSocket URL | wss://rmm-api.azcomputerguru.com/ws |
| Internal Address | 172.16.3.30:3001 |
| Database | PostgreSQL @ 172.16.3.30:5432/gururmm |
| Service Name | gururmm-server (systemd) |
| Agent Service | GuruRMM (Windows SCM) |
| Agent Config | C:\ProgramData\GuruRMM\agent.toml |
| Agent Binary | C:\Program Files\GuruRMM\gururmm-agent.exe |
| Downloads | https://rmm-api.azcomputerguru.com/downloads/ |
| Admin Email | claude-api@azcomputerguru.com |
| SSO Client ID | 18a15f5d-7ab8-46f4-8566-d7b5436b84b6 |
---
*Document generated 2026-02-17. Source of truth for GuruRMM project reference.*

View File

@@ -1,6 +1,6 @@
[package]
name = "gururmm-agent"
version = "0.3.5"
version = "0.6.0"
edition = "2021"
description = "GuruRMM Agent - Cross-platform RMM agent"
authors = ["GuruRMM"]

View File

@@ -199,6 +199,9 @@ pub enum CommandType {
Shell,
/// PowerShell command (Windows)
/// Alias "powershell" for backwards compatibility with servers that send
/// the command type as a plain string instead of snake_case enum format.
#[serde(alias = "powershell")]
PowerShell,
/// Python script
@@ -208,6 +211,7 @@ pub enum CommandType {
Script { interpreter: String },
/// Claude Code task execution
#[serde(alias = "claude_task")]
ClaudeTask {
/// Task description for Claude Code
task: String,

View File

@@ -0,0 +1,67 @@
import requests, json, time, sys
with open("/tmp/agent_b64.txt") as f:
b64 = f.read().strip()
print("Base64 length:", len(b64))
token_r = requests.post("http://localhost:3001/api/auth/login", json={"email": "claude-api@azcomputerguru.com", "password": "ClaudeAPI2026!@#"})
token = token_r.json()["token"]
headers = {"Authorization": "Bearer " + token, "Content-Type": "application/json"}
agent_id = "d28a1c90-47d7-448f-a287-197bc8892234"
chunk_size = 200000
chunks = [b64[i:i+chunk_size] for i in range(0, len(b64), chunk_size)]
print("Chunks:", len(chunks))
def send_cmd(cmd, timeout=60, wait=15):
r = requests.post(
"http://localhost:3001/api/agents/" + agent_id + "/command",
headers=headers,
json={"command_type": "powershell", "command": cmd, "timeout_seconds": timeout}
)
cmd_id = r.json()["command_id"]
time.sleep(wait)
r2 = requests.get("http://localhost:3001/api/commands/" + cmd_id, headers=headers)
return r2.json()
# Chunk 1: create file
cmd = "$b = [Convert]::FromBase64String('" + chunks[0] + "'); New-Item -ItemType Directory -Path C:/Temp -Force | Out-Null; [System.IO.File]::WriteAllBytes('C:/Temp/agent_chunk.bin', $b); Write-Output ('Chunk 1: ' + $b.Length.ToString() + ' bytes')"
d = send_cmd(cmd)
print("Chunk 1:", d["status"], d.get("stdout",""))
if d["status"] != "completed":
print("ERROR:", (d.get("stderr","") or "")[:300])
sys.exit(1)
# Append remaining chunks
for i, chunk in enumerate(chunks[1:], 2):
cmd = "$b = [Convert]::FromBase64String('" + chunk + "'); $f = [System.IO.File]::Open('C:/Temp/agent_chunk.bin', [System.IO.FileMode]::Append); $f.Write($b, 0, $b.Length); $f.Close(); Write-Output ('Chunk " + str(i) + ": ' + $b.Length.ToString() + ' bytes')"
d = send_cmd(cmd, wait=10)
print("Chunk", i, ":", d["status"], d.get("stdout",""))
if d["status"] != "completed":
print("ERROR:", (d.get("stderr","") or "")[:300])
sys.exit(1)
# Verify final size
d = send_cmd("(Get-Item C:/Temp/agent_chunk.bin).Length", wait=5)
print("Final size:", d.get("stdout","").strip(), "(expected 3577856)")
# Now create update script that stops service, replaces binary, starts service
update_cmd = """
$src = 'C:/Temp/agent_chunk.bin'
$dst = 'C:/Program Files/GuruRMM/gururmm-agent.exe'
$bak = 'C:/Program Files/GuruRMM/gururmm-agent.exe.bak'
Copy-Item $dst $bak -Force
Stop-Service GuruRMMAgent -Force
Start-Sleep -Seconds 2
Copy-Item $src $dst -Force
Start-Service GuruRMMAgent
Write-Output 'Agent updated and restarted'
"""
print("\nNow creating scheduled task to perform the update...")
sched_cmd = 'schtasks /create /tn AgentUpdate /tr "powershell.exe -ExecutionPolicy Bypass -Command \\"' + update_cmd.replace('\n', '; ').strip() + '\\"" /sc ONCE /st 00:00 /sd 01/01/2030 /ru SYSTEM /f'
d = send_cmd(sched_cmd, wait=5)
print("Sched task create:", d["status"], d.get("stdout",""), d.get("stderr","")[:200] if d.get("stderr") else "")
d = send_cmd("schtasks /run /tn AgentUpdate", wait=5)
print("Sched task run:", d["status"], d.get("stdout",""))
print("\nAgent will restart momentarily. Wait 30s then check connection.")

View File

@@ -0,0 +1,88 @@
"""Deploy agent binary to AD2 by writing base64 text chunks to a file, then decoding."""
import requests, json, time, sys, base64
# Read and encode the binary
with open("agent/target/release/gururmm-agent.exe", "rb") as f:
binary = f.read()
b64 = base64.b64encode(binary).decode('ascii')
print(f"Binary: {len(binary)} bytes, Base64: {len(b64)} chars")
# Auth
token_r = requests.post('http://172.16.3.30:3001/api/auth/login', json={
'email': 'claude-api@azcomputerguru.com',
'password': 'ClaudeAPI2026!@#'
})
token = token_r.json()['token']
headers = {'Authorization': 'Bearer ' + token, 'Content-Type': 'application/json'}
agent_id = 'd28a1c90-47d7-448f-a287-197bc8892234'
def send_cmd(cmd, timeout=60, wait=10):
r = requests.post(
'http://172.16.3.30:3001/api/agents/' + agent_id + '/command',
headers=headers,
json={'command_type': 'powershell', 'command': cmd, 'timeout_seconds': timeout}
)
data = r.json()
cmd_id = data['command_id']
time.sleep(wait)
# Poll until complete
for attempt in range(10):
r2 = requests.get('http://172.16.3.30:3001/api/commands/' + cmd_id, headers=headers)
d = r2.json()
if d['status'] != 'running':
return d
time.sleep(5)
return d
# Step 1: Delete old file and create fresh one
print("Step 1: Preparing temp file...")
d = send_cmd("Remove-Item C:/Temp/agent.b64 -Force -ErrorAction SilentlyContinue; "
"New-Item -ItemType Directory -Path C:/Temp -Force | Out-Null; "
"'' | Set-Content C:/Temp/agent.b64 -NoNewline; "
"Write-Output 'Ready'", wait=8)
print(f" {d['status']}: {d.get('stdout','').strip()}")
if d['status'] != 'completed':
print(f" ERROR: {(d.get('stderr','') or '')[:300]}")
sys.exit(1)
# Step 2: Write base64 text in chunks
# Windows command line limit is ~32KB, keep chunks under 20KB to be safe
chunk_size = 20000
chunks = [b64[i:i+chunk_size] for i in range(0, len(b64), chunk_size)]
print(f"Step 2: Writing {len(chunks)} chunks of ~{chunk_size} chars each...")
for i, chunk in enumerate(chunks):
# Use Add-Content to append text (no base64 decode here, just text)
# Escape single quotes in base64 (shouldn't have any, but just in case)
safe_chunk = chunk.replace("'", "''")
cmd = f"Add-Content -Path C:/Temp/agent.b64 -Value '{safe_chunk}' -NoNewline; Write-Output 'chunk{i+1}ok'"
d = send_cmd(cmd, wait=5)
status = d['status']
stdout = d.get('stdout', '').strip()
if status != 'completed' or f'chunk{i+1}ok' not in stdout:
print(f" Chunk {i+1}/{len(chunks)} FAILED: {status} - {stdout}")
print(f" stderr: {(d.get('stderr','') or '')[:300]}")
sys.exit(1)
if (i+1) % 10 == 0 or i == 0 or i == len(chunks)-1:
print(f" Chunk {i+1}/{len(chunks)}: OK")
# Step 3: Verify base64 file size
print("Step 3: Verifying base64 file...")
d = send_cmd(f"$f = Get-Item C:/Temp/agent.b64; Write-Output $f.Length", wait=5)
remote_size = d.get('stdout', '').strip()
print(f" Remote b64 size: {remote_size} (expected: {len(b64)})")
# Step 4: Decode base64 file to binary
print("Step 4: Decoding base64 to binary...")
cmd = ("$b64 = Get-Content C:/Temp/agent.b64 -Raw; "
"$bytes = [Convert]::FromBase64String($b64); "
"[System.IO.File]::WriteAllBytes('C:/Temp/gururmm-agent-new.exe', $bytes); "
"$f = Get-Item C:/Temp/gururmm-agent-new.exe; "
"Write-Output ('Decoded: ' + $f.Length.ToString() + ' bytes')")
d = send_cmd(cmd, timeout=120, wait=15)
print(f" {d['status']}: {d.get('stdout','').strip()}")
if d.get('stderr'):
print(f" stderr: {(d.get('stderr','') or '')[:300]}")
print(f"\nExpected binary size: {len(binary)} bytes")
print("Done!")

View File

@@ -0,0 +1,35 @@
import requests, json, time, sys
# Auth
token_r = requests.post('http://172.16.3.30:3001/api/auth/login', json={
'email': 'claude-api@azcomputerguru.com',
'password': 'ClaudeAPI2026!@#'
})
token = token_r.json()['token']
headers = {'Authorization': 'Bearer ' + token, 'Content-Type': 'application/json'}
agent_id = 'd28a1c90-47d7-448f-a287-197bc8892234'
# Send download command via PowerShell
cmd = (
"New-Item -ItemType Directory -Path C:/Temp -Force | Out-Null; "
"Invoke-WebRequest -Uri 'http://172.16.3.30/gururmm-agent-new.exe' "
"-OutFile 'C:/Temp/gururmm-agent-new.exe' -UseBasicParsing; "
"$f = Get-Item 'C:/Temp/gururmm-agent-new.exe'; "
"Write-Output ('Downloaded: ' + $f.Length.ToString() + ' bytes')"
)
r = requests.post(
'http://172.16.3.30:3001/api/agents/' + agent_id + '/command',
headers=headers,
json={'command_type': 'powershell', 'command': cmd, 'timeout_seconds': 120}
)
print('Send:', r.json())
cmd_id = r.json()['command_id']
# Wait and check
time.sleep(20)
r2 = requests.get('http://172.16.3.30:3001/api/commands/' + cmd_id, headers=headers)
d = r2.json()
print('Status:', d['status'])
print('stdout:', d.get('stdout', ''))
print('stderr:', (d.get('stderr', '') or '')[:500])

View File

@@ -0,0 +1,46 @@
import requests, json, time, sys
# Auth
token_r = requests.post('http://172.16.3.30:3001/api/auth/login', json={
'email': 'claude-api@azcomputerguru.com',
'password': 'ClaudeAPI2026!@#'
})
token = token_r.json()['token']
headers = {'Authorization': 'Bearer ' + token, 'Content-Type': 'application/json'}
agent_id = 'd28a1c90-47d7-448f-a287-197bc8892234'
def send_cmd(cmd, timeout=120, wait=20):
r = requests.post(
'http://172.16.3.30:3001/api/agents/' + agent_id + '/command',
headers=headers,
json={'command_type': 'powershell', 'command': cmd, 'timeout_seconds': timeout}
)
data = r.json()
print('Sent:', data.get('status', 'error'), data.get('message', ''))
cmd_id = data['command_id']
time.sleep(wait)
r2 = requests.get('http://172.16.3.30:3001/api/commands/' + cmd_id, headers=headers)
d = r2.json()
print('Status:', d['status'])
print('stdout:', d.get('stdout', ''))
stderr = d.get('stderr', '') or ''
if stderr:
print('stderr:', stderr[:500])
print('exit_code:', d.get('exit_code'))
return d
# First, check what transfer tools are available on AD2
print("=== Checking available tools ===")
d = send_cmd("Get-Command scp,ssh,curl -ErrorAction SilentlyContinue | Select-Object Name,Source | Format-Table -AutoSize", wait=10)
print("\n=== Trying SCP from AD2 to RMM server ===")
# Use scp with StrictHostKeyChecking=no for automated transfer
cmd = (
"scp -o StrictHostKeyChecking=no -o UserKnownHostsFile=NUL "
"guru@172.16.3.30:/tmp/gururmm-agent-new.exe C:/Temp/gururmm-agent-new.exe 2>&1; "
"if (Test-Path C:/Temp/gururmm-agent-new.exe) { "
" $f = Get-Item C:/Temp/gururmm-agent-new.exe; "
" Write-Output ('File size: ' + $f.Length.ToString() + ' bytes') "
"} else { Write-Output 'File not found after SCP' }"
)
d = send_cmd(cmd, wait=30)

View File

@@ -10,16 +10,16 @@ use uuid::Uuid;
use crate::auth::AuthUser;
use crate::db::{self, Command};
use crate::ws::{CommandPayload, ServerMessage};
use crate::ws::{CommandPayload, CommandType, ServerMessage};
use crate::AppState;
/// Request to send a command to an agent
#[derive(Debug, Deserialize)]
pub struct SendCommandRequest {
/// Command type (shell, powershell, python, script)
/// Command type (shell, powershell, python, script, claude_task)
pub command_type: String,
/// Command text to execute
/// Command text to execute (also used as task description for claude_task)
pub command: String,
/// Timeout in seconds (optional, default 300)
@@ -27,6 +27,12 @@ pub struct SendCommandRequest {
/// Run as elevated/admin (optional, default false)
pub elevated: Option<bool>,
/// Working directory for claude_task (optional, default C:\Shares\test)
pub working_directory: Option<String>,
/// Context files for claude_task (optional)
pub context_files: Option<Vec<String>>,
}
/// Response after sending a command
@@ -59,6 +65,18 @@ pub async fn send_command(
"Command sent by user"
);
// Validate and build command type
let command_type = if CommandType::is_claude_task(&req.command_type) {
CommandType::new_claude_task(
req.command.clone(),
req.working_directory.clone(),
req.context_files.clone(),
)
} else {
CommandType::from_api_string(&req.command_type)
.map_err(|e| (StatusCode::BAD_REQUEST, e))?
};
// Verify agent exists
let _agent = db::get_agent_by_id(&state.db, agent_id)
.await
@@ -66,9 +84,10 @@ pub async fn send_command(
.ok_or((StatusCode::NOT_FOUND, "Agent not found".to_string()))?;
// Create command record with user ID for audit trail
// Store the canonical db string format for consistency
let create = db::CreateCommand {
agent_id,
command_type: req.command_type.clone(),
command_type: command_type.as_db_string().to_string(),
command_text: req.command.clone(),
created_by: Some(user.user_id),
};
@@ -80,10 +99,11 @@ pub async fn send_command(
// Check if agent is connected
let agents = state.agents.read().await;
if agents.is_connected(&agent_id) {
// Send command via WebSocket
// Send command via WebSocket using the proper enum type
// This serializes as snake_case to match the agent's expected format
let cmd_msg = ServerMessage::Command(CommandPayload {
id: command.id,
command_type: req.command_type,
command_type,
command: req.command,
timeout_seconds: req.timeout_seconds,
elevated: req.elevated.unwrap_or(false),

View File

@@ -193,10 +193,74 @@ pub struct NetworkStatePayload {
pub state_hash: String,
}
/// Types of commands that can be sent to agents.
/// Must match the agent's CommandType enum serialization format.
/// Uses snake_case to match the agent's #[serde(rename_all = "snake_case")].
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum CommandType {
/// Shell command (cmd on Windows, sh on Unix)
Shell,
/// PowerShell command (Windows)
PowerShell,
/// Python script
Python,
/// Raw script execution
Script,
/// Claude Code task execution
ClaudeTask {
/// Task description for Claude Code
task: String,
/// Optional working directory
working_directory: Option<String>,
/// Optional context files
context_files: Option<Vec<String>>,
},
}
impl CommandType {
/// Parse a command type string from the API into the enum.
/// Accepts both snake_case ("power_shell") and common formats ("powershell").
/// Note: ClaudeTask requires additional fields - use `new_claude_task()` instead.
pub fn from_api_string(s: &str) -> Result<Self, String> {
match s.to_lowercase().as_str() {
"shell" => Ok(Self::Shell),
"powershell" | "power_shell" => Ok(Self::PowerShell),
"python" => Ok(Self::Python),
"script" => Ok(Self::Script),
"claude_task" | "claudetask" => Err(
"claude_task type requires task field - use the claude_task-specific API fields".to_string()
),
_ => Err(format!("Unknown command type: '{}'. Valid types: shell, powershell, python, script, claude_task", s)),
}
}
/// Check if a command type string represents a claude_task.
pub fn is_claude_task(s: &str) -> bool {
matches!(s.to_lowercase().as_str(), "claude_task" | "claudetask")
}
/// Create a ClaudeTask command type with the required fields.
pub fn new_claude_task(task: String, working_directory: Option<String>, context_files: Option<Vec<String>>) -> Self {
Self::ClaudeTask { task, working_directory, context_files }
}
/// Convert back to the string format stored in the database.
pub fn as_db_string(&self) -> &'static str {
match self {
Self::Shell => "shell",
Self::PowerShell => "powershell",
Self::Python => "python",
Self::Script => "script",
Self::ClaudeTask { .. } => "claude_task",
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CommandPayload {
pub id: Uuid,
pub command_type: String,
pub command_type: CommandType,
pub command: String,
pub timeout_seconds: Option<u64>,
pub elevated: bool,

View File

@@ -0,0 +1,80 @@
"""Create scheduled task on AD2 to swap agent binary and restart service."""
import requests, time
# Auth
token_r = requests.post('http://172.16.3.30:3001/api/auth/login', json={
'email': 'claude-api@azcomputerguru.com',
'password': 'ClaudeAPI2026!@#'
})
token = token_r.json()['token']
headers = {'Authorization': 'Bearer ' + token, 'Content-Type': 'application/json'}
agent_id = 'd28a1c90-47d7-448f-a287-197bc8892234'
# Create a scheduled task to swap the binary and restart
# Uses schtasks for simplicity - runs a PowerShell script that:
# 1. Stops the agent service
# 2. Backs up old binary
# 3. Copies new binary in place
# 4. Starts the service
# 5. Writes result to file
cmd = (
"$script = @'\n"
"Stop-Service GuruRMMAgent -Force\n"
"Start-Sleep -Seconds 3\n"
"Copy-Item \"C:\\Program Files\\GuruRMM\\gururmm-agent.exe\" \"C:\\Program Files\\GuruRMM\\gururmm-agent.exe.bak\" -Force\n"
"Copy-Item \"C:\\Temp\\gururmm-agent-new.exe\" \"C:\\Program Files\\GuruRMM\\gururmm-agent.exe\" -Force\n"
"Start-Sleep -Seconds 1\n"
"Start-Service GuruRMMAgent\n"
"\"Agent updated at $(Get-Date)\" | Out-File C:\\Temp\\update_result.txt\n"
"'@\n"
"$script | Out-File C:/Temp/update_agent.ps1 -Encoding UTF8\n"
"Write-Output ('Script written: ' + (Get-Item C:/Temp/update_agent.ps1).Length.ToString() + ' bytes')"
)
print("Step 1: Writing update script to AD2...")
r = requests.post(
'http://172.16.3.30:3001/api/agents/' + agent_id + '/command',
headers=headers,
json={'command_type': 'powershell', 'command': cmd, 'timeout_seconds': 30}
)
print('Send:', r.json())
cmd_id = r.json()['command_id']
time.sleep(10)
r2 = requests.get('http://172.16.3.30:3001/api/commands/' + cmd_id, headers=headers)
d = r2.json()
print('Status:', d['status'])
print('stdout:', d.get('stdout', ''))
if d.get('stderr'):
print('stderr:', (d.get('stderr', '') or '')[:300])
if d['status'] != 'completed':
print("FAILED to write script")
exit(1)
# Step 2: Create and run scheduled task
print("\nStep 2: Creating scheduled task to run update...")
cmd2 = (
"schtasks /create /tn AgentBinaryUpdate /tr "
"\"powershell.exe -ExecutionPolicy Bypass -File C:\\Temp\\update_agent.ps1\" "
"/sc ONCE /st 00:00 /sd 01/01/2030 /ru SYSTEM /rl HIGHEST /f; "
"schtasks /run /tn AgentBinaryUpdate; "
"Write-Output 'Update task started - agent will restart momentarily'"
)
r = requests.post(
'http://172.16.3.30:3001/api/agents/' + agent_id + '/command',
headers=headers,
json={'command_type': 'powershell', 'command': cmd2, 'timeout_seconds': 30}
)
print('Send:', r.json())
cmd_id = r.json()['command_id']
time.sleep(10)
r2 = requests.get('http://172.16.3.30:3001/api/commands/' + cmd_id, headers=headers)
d = r2.json()
print('Status:', d['status'])
print('stdout:', d.get('stdout', ''))
if d.get('stderr'):
print('stderr:', (d.get('stderr', '') or '')[:300])
print("\nAgent will stop, binary will be swapped, then service restarts.")
print("Wait ~30 seconds then check if agent reconnects.")

Submodule projects/solverbot updated: 342fe0fdf4...60b2617651