use serde::Deserialize;
use std::collections::HashMap;
use std::process::Command;
use std::sync::Arc;
use strike48_connector::*;
#[cfg(unix)]
use tokio::signal::unix::{SignalKind, signal};
fn tool_schemas() -> serde_json::Value {
serde_json::json!([
{
"name": "run_command",
"description": "Execute a system command on the host. Returns stdout, stderr, and exit code.",
"parameters": {
"type": "object",
"properties": {
"command": {
"type": "string",
"description": "The command to execute (e.g., 'ls', 'echo', 'cat')"
},
"args": {
"type": "array",
"items": { "type": "string" },
"description": "Command arguments (e.g., ['-la', '/tmp'])"
},
"working_dir": {
"type": "string",
"description": "Working directory for the command (optional)"
},
"timeout_secs": {
"type": "integer",
"description": "Timeout in seconds (default: 30, max: 300)"
}
},
"required": ["command"]
}
},
{
"name": "get_env",
"description": "Get the value of an environment variable",
"parameters": {
"type": "object",
"properties": {
"name": {
"type": "string",
"description": "Environment variable name"
}
},
"required": ["name"]
}
},
{
"name": "list_directory",
"description": "List contents of a directory",
"parameters": {
"type": "object",
"properties": {
"path": {
"type": "string",
"description": "Directory path to list (default: current directory)"
}
},
"required": []
}
},
{
"name": "read_file",
"description": "Read contents of a text file (max 1MB)",
"parameters": {
"type": "object",
"properties": {
"path": {
"type": "string",
"description": "Path to the file to read"
},
"max_lines": {
"type": "integer",
"description": "Maximum number of lines to read (optional)"
}
},
"required": ["path"]
}
},
{
"name": "write_file",
"description": "Write content to a file",
"parameters": {
"type": "object",
"properties": {
"path": {
"type": "string",
"description": "Path to the file to write"
},
"content": {
"type": "string",
"description": "Content to write to the file"
},
"append": {
"type": "boolean",
"description": "Append to file instead of overwriting (default: false)"
}
},
"required": ["path", "content"]
}
},
{
"name": "get_system_info",
"description": "Get system information (hostname, OS, architecture, etc.)",
"parameters": {
"type": "object",
"properties": {},
"required": []
}
}
])
}
#[derive(Debug, Deserialize)]
struct RunCommandRequest {
command: String,
#[serde(default)]
args: Vec<String>,
working_dir: Option<String>,
#[allow(dead_code)]
timeout_secs: Option<u64>,
}
struct SystemCommandConnector;
impl SystemCommandConnector {
fn run_command(&self, req: RunCommandRequest) -> serde_json::Value {
let mut cmd = Command::new(&req.command);
cmd.args(&req.args);
if let Some(ref dir) = req.working_dir {
cmd.current_dir(dir);
}
match cmd.output() {
Ok(output) => {
let stdout = String::from_utf8_lossy(&output.stdout).to_string();
let stderr = String::from_utf8_lossy(&output.stderr).to_string();
let exit_code = output.status.code().unwrap_or(-1);
serde_json::json!({
"success": output.status.success(),
"exit_code": exit_code,
"stdout": stdout,
"stderr": stderr,
"command": req.command,
"args": req.args
})
}
Err(e) => {
serde_json::json!({
"success": false,
"error": e.to_string(),
"command": req.command,
"args": req.args
})
}
}
}
fn get_env(&self, name: &str) -> serde_json::Value {
match std::env::var(name) {
Ok(value) => serde_json::json!({
"success": true,
"name": name,
"value": value
}),
Err(_) => serde_json::json!({
"success": false,
"name": name,
"error": "Environment variable not found"
}),
}
}
fn list_directory(&self, path: Option<&str>) -> serde_json::Value {
let dir_path = path.unwrap_or(".");
match std::fs::read_dir(dir_path) {
Ok(entries) => {
let items: Vec<serde_json::Value> = entries
.filter_map(|e| e.ok())
.map(|entry| {
let metadata = entry.metadata().ok();
serde_json::json!({
"name": entry.file_name().to_string_lossy(),
"is_dir": metadata.as_ref().map(|m| m.is_dir()).unwrap_or(false),
"is_file": metadata.as_ref().map(|m| m.is_file()).unwrap_or(false),
"size": metadata.as_ref().map(|m| m.len()).unwrap_or(0)
})
})
.collect();
serde_json::json!({
"success": true,
"path": dir_path,
"entries": items,
"count": items.len()
})
}
Err(e) => serde_json::json!({
"success": false,
"path": dir_path,
"error": e.to_string()
}),
}
}
fn read_file(&self, path: &str, max_lines: Option<usize>) -> serde_json::Value {
const MAX_SIZE: u64 = 1024 * 1024;
match std::fs::metadata(path) {
Ok(meta) if meta.len() > MAX_SIZE => {
return serde_json::json!({
"success": false,
"path": path,
"error": format!("File too large: {} bytes (max: {})", meta.len(), MAX_SIZE)
});
}
Err(e) => {
return serde_json::json!({
"success": false,
"path": path,
"error": e.to_string()
});
}
_ => {}
}
match std::fs::read_to_string(path) {
Ok(content) => {
let final_content = if let Some(max) = max_lines {
content.lines().take(max).collect::<Vec<_>>().join("\n")
} else {
content
};
serde_json::json!({
"success": true,
"path": path,
"content": final_content,
"lines": final_content.lines().count()
})
}
Err(e) => serde_json::json!({
"success": false,
"path": path,
"error": e.to_string()
}),
}
}
fn write_file(&self, path: &str, content: &str, append: bool) -> serde_json::Value {
use std::io::Write;
let result = if append {
std::fs::OpenOptions::new()
.create(true)
.append(true)
.open(path)
.and_then(|mut f| f.write_all(content.as_bytes()))
} else {
std::fs::write(path, content)
};
match result {
Ok(_) => serde_json::json!({
"success": true,
"path": path,
"bytes_written": content.len(),
"append": append
}),
Err(e) => serde_json::json!({
"success": false,
"path": path,
"error": e.to_string()
}),
}
}
fn get_system_info(&self) -> serde_json::Value {
serde_json::json!({
"success": true,
"hostname": hostname::get().ok().map(|h| h.to_string_lossy().to_string()),
"os": std::env::consts::OS,
"arch": std::env::consts::ARCH,
"family": std::env::consts::FAMILY,
"current_dir": std::env::current_dir().ok().map(|p| p.display().to_string()),
"temp_dir": std::env::temp_dir().display().to_string(),
"num_cpus": std::thread::available_parallelism().ok().map(|n| n.get())
})
}
}
impl BaseConnector for SystemCommandConnector {
fn connector_type(&self) -> &str {
"system-command-tool"
}
fn version(&self) -> &str {
"1.0.0"
}
fn behavior(&self) -> ConnectorBehavior {
ConnectorBehavior::Tool
}
fn metadata(&self) -> std::collections::HashMap<String, String> {
let mut meta = HashMap::new();
meta.insert("name".to_string(), "System Command Tool".to_string());
meta.insert(
"description".to_string(),
"Execute system commands on the host".to_string(),
);
meta.insert("vendor".to_string(), "Strike48".to_string());
meta.insert("icon".to_string(), "hero-command-line".to_string());
meta.insert(
"warning".to_string(),
"Executes arbitrary commands - use in sandboxed environments only".to_string(),
);
meta.insert(
"tool_schemas".to_string(),
serde_json::to_string(&tool_schemas()).unwrap_or_default(),
);
meta.insert("tool_count".to_string(), "6".to_string());
meta
}
fn execute(
&self,
request: serde_json::Value,
_capability_id: Option<&str>,
) -> std::pin::Pin<Box<dyn std::future::Future<Output = Result<serde_json::Value>> + Send + '_>>
{
Box::pin(async move {
let tool = request
.get("tool")
.and_then(|v| v.as_str())
.unwrap_or_default();
let params = request
.get("parameters")
.or_else(|| request.get("args"))
.cloned()
.unwrap_or(serde_json::json!({}));
let result = match tool {
"run_command" => {
let req: RunCommandRequest = serde_json::from_value(params).map_err(|e| {
ConnectorError::InvalidConfig(format!("Invalid parameters: {e}"))
})?;
self.run_command(req)
}
"get_env" => {
let name = params.get("name").and_then(|v| v.as_str()).ok_or_else(|| {
ConnectorError::InvalidConfig("Missing 'name' parameter".to_string())
})?;
self.get_env(name)
}
"list_directory" => {
let path = params.get("path").and_then(|v| v.as_str());
self.list_directory(path)
}
"read_file" => {
let path = params.get("path").and_then(|v| v.as_str()).ok_or_else(|| {
ConnectorError::InvalidConfig("Missing 'path' parameter".to_string())
})?;
let max_lines = params
.get("max_lines")
.and_then(|v| v.as_u64())
.map(|n| n as usize);
self.read_file(path, max_lines)
}
"write_file" => {
let path = params.get("path").and_then(|v| v.as_str()).ok_or_else(|| {
ConnectorError::InvalidConfig("Missing 'path' parameter".to_string())
})?;
let content =
params
.get("content")
.and_then(|v| v.as_str())
.ok_or_else(|| {
ConnectorError::InvalidConfig(
"Missing 'content' parameter".to_string(),
)
})?;
let append = params
.get("append")
.and_then(|v| v.as_bool())
.unwrap_or(false);
self.write_file(path, content, append)
}
"get_system_info" => self.get_system_info(),
"" => {
return Err(ConnectorError::InvalidConfig(
"Missing 'tool' field in request".to_string(),
));
}
_ => {
return Err(ConnectorError::InvalidConfig(format!(
"Unknown tool: {tool}"
)));
}
};
Ok(serde_json::json!({
"tool": tool,
"result": result,
"timestamp": chrono::Utc::now().to_rfc3339()
}))
})
}
}
#[tokio::main]
async fn main() -> Result<()> {
init_logger();
println!("=== System Command Tool ===");
println!("⚠️ WARNING: This connector executes arbitrary system commands.");
println!("⚠️ Only use in sandboxed/development environments!");
println!();
let instance_id = std::env::var("INSTANCE_ID").unwrap_or_else(|_| {
format!(
"system-cmd-{}",
chrono::Utc::now().timestamp_millis() % 10000
)
});
let config = ConnectorConfig {
connector_type: "system-command-tool".to_string(),
instance_id: instance_id.clone(),
version: "1.0.0".to_string(),
max_concurrent_requests: 100,
display_name: std::env::var("CONNECTOR_NAME").ok(),
..ConnectorConfig::from_env()
};
println!("Configuration:");
println!(" Host: {}", config.host);
println!(" Tenant ID: {}", config.tenant_id);
println!(" Instance ID: {}", config.instance_id);
if let Some(name) = &config.display_name {
println!(" Display Name: {name}");
}
println!();
println!("Available Tools:");
println!(" - run_command: Execute a shell command");
println!(" - get_env: Get environment variable value");
println!(" - list_directory: List directory contents");
println!(" - read_file: Read file contents (max 1MB)");
println!(" - write_file: Write content to a file");
println!(" - get_system_info: Get system information");
println!();
let connector = Arc::new(SystemCommandConnector);
let runner = ConnectorRunner::new(config, connector);
let shutdown_handle = runner.shutdown_handle();
#[cfg(unix)]
{
let handle = shutdown_handle.clone();
tokio::spawn(async move {
let mut sigterm =
signal(SignalKind::terminate()).expect("Failed to register SIGTERM handler");
let mut sigint =
signal(SignalKind::interrupt()).expect("Failed to register SIGINT handler");
tokio::select! {
_ = sigterm.recv() => {
println!("\nReceived SIGTERM, shutting down...");
}
_ = sigint.recv() => {
println!("\nReceived SIGINT, shutting down...");
}
}
handle.shutdown();
});
}
#[cfg(not(unix))]
{
let handle = shutdown_handle.clone();
tokio::spawn(async move {
tokio::signal::ctrl_c()
.await
.expect("Failed to register Ctrl+C handler");
println!("\nReceived Ctrl+C, shutting down...");
handle.shutdown();
});
}
println!("Starting connector...");
if let Err(e) = runner.run().await {
eprintln!("Connector error: {e}");
std::process::exit(1);
}
Ok(())
}