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

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