Add agent command execution (terminal panel)
- DB: new agent_commands table (id, agent_id, command, status, output, exit_code, timestamps)
- Server: 4 new routes
POST /api/agents/:id/commands queue a command (dashboard → agent)
GET /api/agents/:id/commands history (last 50) for dashboard
GET /api/agents/commands/pending agent dequeues + marks running (Bearer auth)
POST /api/agents/commands/:id/result agent posts output/exit_code (Bearer auth)
Output capped at 128 KB; Bearer token enforced on agent-facing routes
- Agents (Linux/macOS/Windows): each polls /commands/pending each cycle,
executes via bash/PowerShell with 60s timeout, posts result back
- UI: ">_" button on each AgentCard opens CommandTerminal panel
Input line, Run button, history list (status icon + command + time)
Click any entry with output to expand/collapse its output
Auto-refreshes every 4 s while open
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
1 parent
13b3e3612b
commit
ce4bea5e29
15 files changed
+543
-15
No files matched your search
@@ -255,6 +255,51 @@ report_metrics() {
|
||||
fi
|
||||
}
|
||||
|
||||
poll_commands() {
|
||||
local resp http_code
|
||||
resp=$(curl -sf -m 10 \
|
||||
-H "Authorization: Bearer ${AGENT_TOKEN}" \
|
||||
-w '\n__HTTP_CODE__%{http_code}' \
|
||||
"${SKYWATCH_URL}/api/agents/commands/pending" 2>/dev/null) || return 0
|
||||
|
||||
http_code=$(echo "$resp" | grep -o '__HTTP_CODE__[0-9]*' | tail -1 | grep -o '[0-9]*')
|
||||
resp=$(echo "$resp" | sed '/__HTTP_CODE__/d')
|
||||
|
||||
[ "$http_code" = "204" ] && return 0
|
||||
[ "$http_code" != "200" ] && return 0
|
||||
|
||||
local cmd_id cmd_str
|
||||
cmd_id=$(echo "$resp" | grep -o '"id":[0-9]*' | head -1 | grep -o '[0-9]*')
|
||||
cmd_str=$(echo "$resp" | python3 -c "import sys,json; d=json.load(sys.stdin); print(d.get('command',''))" 2>/dev/null \
|
||||
|| echo "$resp" | sed -n 's/.*"command":"\([^"]*\)".*/\1/p')
|
||||
|
||||
[ -z "$cmd_id" ] || [ -z "$cmd_str" ] && return 0
|
||||
|
||||
log "Running command #${cmd_id}: ${cmd_str}"
|
||||
|
||||
local tmp_out exit_code
|
||||
tmp_out=$(mktemp)
|
||||
gtimeout 60 bash -c "$cmd_str" > "$tmp_out" 2>&1 || timeout 60 bash -c "$cmd_str" > "$tmp_out" 2>&1
|
||||
exit_code=$?
|
||||
|
||||
local tmp_json
|
||||
tmp_json=$(mktemp)
|
||||
python3 - "$tmp_out" "$exit_code" > "$tmp_json" 2>/dev/null << 'PYEOF'
|
||||
import sys, json
|
||||
with open(sys.argv[1]) as f:
|
||||
output = f.read(131072)
|
||||
print(json.dumps({"output": output, "exit_code": int(sys.argv[2])}))
|
||||
PYEOF
|
||||
|
||||
curl -sf -m 15 -X POST \
|
||||
-H "Authorization: Bearer ${AGENT_TOKEN}" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d "@${tmp_json}" \
|
||||
"${SKYWATCH_URL}/api/agents/commands/${cmd_id}/result" >/dev/null 2>&1 || true
|
||||
|
||||
rm -f "$tmp_out" "$tmp_json"
|
||||
}
|
||||
|
||||
main() {
|
||||
load_config
|
||||
|
||||
@@ -278,6 +323,7 @@ main() {
|
||||
|
||||
while true; do
|
||||
report_metrics
|
||||
poll_commands
|
||||
local sleep_time=$(( interval - 1 ))
|
||||
[ "$sleep_time" -lt 1 ] && sleep_time=1
|
||||
sleep "$sleep_time"
|
||||
|
||||
@@ -228,6 +228,61 @@ function Send-Metrics {
|
||||
}
|
||||
}
|
||||
|
||||
# ── Command execution ─────────────────────────────────────────────────────────
|
||||
|
||||
function Poll-Commands {
|
||||
try {
|
||||
$resp = Invoke-WebRequest `
|
||||
-Uri "$script:SKYWATCH_URL/api/agents/commands/pending" `
|
||||
-Headers @{ 'Authorization' = "Bearer $script:AGENT_TOKEN" } `
|
||||
-UseBasicParsing -TimeoutSec 10 -ErrorAction Stop
|
||||
|
||||
if ($resp.StatusCode -eq 204) { return }
|
||||
if ($resp.StatusCode -ne 200) { return }
|
||||
|
||||
$data = $resp.Content | ConvertFrom-Json
|
||||
$cmdId = $data.id
|
||||
$cmdStr = $data.command
|
||||
if (-not $cmdId -or -not $cmdStr) { return }
|
||||
|
||||
Write-Log "Running command #${cmdId}: ${cmdStr}"
|
||||
|
||||
$output = ""
|
||||
$exitCode = -1
|
||||
try {
|
||||
$proc = Start-Process -FilePath "powershell.exe" `
|
||||
-ArgumentList "-NonInteractive", "-Command", $cmdStr `
|
||||
-Wait -PassThru -NoNewWindow `
|
||||
-RedirectStandardOutput "$env:TEMP\sw_cmd_out.txt" `
|
||||
-RedirectStandardError "$env:TEMP\sw_cmd_err.txt" `
|
||||
-ErrorAction Stop
|
||||
$exitCode = $proc.ExitCode
|
||||
$stdout = if (Test-Path "$env:TEMP\sw_cmd_out.txt") { Get-Content "$env:TEMP\sw_cmd_out.txt" -Raw } else { "" }
|
||||
$stderr = if (Test-Path "$env:TEMP\sw_cmd_err.txt") { Get-Content "$env:TEMP\sw_cmd_err.txt" -Raw } else { "" }
|
||||
$output = ($stdout + $stderr).Substring(0, [Math]::Min(($stdout + $stderr).Length, 131072))
|
||||
} catch {
|
||||
$output = "ERROR: $_"
|
||||
$exitCode = 1
|
||||
} finally {
|
||||
Remove-Item "$env:TEMP\sw_cmd_out.txt", "$env:TEMP\sw_cmd_err.txt" -ErrorAction SilentlyContinue
|
||||
}
|
||||
|
||||
$payload = @{ output = $output; exit_code = $exitCode } | ConvertTo-Json -Compress
|
||||
Invoke-RestMethod `
|
||||
-Uri "$script:SKYWATCH_URL/api/agents/commands/$cmdId/result" `
|
||||
-Method POST `
|
||||
-Body $payload `
|
||||
-Headers @{ 'Content-Type' = 'application/json'; 'Authorization' = "Bearer $script:AGENT_TOKEN" } `
|
||||
-TimeoutSec 15 -ErrorAction SilentlyContinue | Out-Null
|
||||
|
||||
} catch [System.Net.WebException] {
|
||||
# 204 No Content arrives as an exception in some PS versions — that's fine
|
||||
if ($_.Exception.Response -and $_.Exception.Response.StatusCode -eq 204) { return }
|
||||
} catch {
|
||||
# Network errors — silently ignore
|
||||
}
|
||||
}
|
||||
|
||||
# ── Main ──────────────────────────────────────────────────────────────────────
|
||||
|
||||
# Ensure log directory exists
|
||||
@@ -253,5 +308,6 @@ Write-Log "SkyWatch Agent v$VERSION started (Windows) — server=$script:SKYWATC
|
||||
|
||||
while ($true) {
|
||||
Send-Metrics
|
||||
Poll-Commands
|
||||
Start-Sleep -Seconds ($interval - 1) # -1 accounts for the 1s CPU sample sleep
|
||||
}
|
||||
@@ -263,6 +263,59 @@ report_metrics() {
|
||||
fi
|
||||
}
|
||||
|
||||
# Poll for and execute one pending command from the server
|
||||
poll_commands() {
|
||||
local resp http_code
|
||||
resp=$(curl -sf -m 10 \
|
||||
-H "Authorization: Bearer ${AGENT_TOKEN}" \
|
||||
-w '\n__HTTP_CODE__%{http_code}' \
|
||||
"${SKYWATCH_URL}/api/agents/commands/pending" 2>/dev/null) || return 0
|
||||
|
||||
http_code=$(echo "$resp" | grep -o '__HTTP_CODE__[0-9]*' | tail -1 | grep -o '[0-9]*')
|
||||
resp=$(echo "$resp" | sed '/__HTTP_CODE__/d')
|
||||
|
||||
# 204 = no pending commands
|
||||
[ "$http_code" = "204" ] && return 0
|
||||
[ "$http_code" != "200" ] && return 0
|
||||
|
||||
local cmd_id cmd_str
|
||||
cmd_id=$(echo "$resp" | grep -o '"id":[0-9]*' | head -1 | grep -o '[0-9]*')
|
||||
# Extract command value (handles escaped quotes)
|
||||
cmd_str=$(echo "$resp" | python3 -c "import sys,json; d=json.load(sys.stdin); print(d.get('command',''))" 2>/dev/null \
|
||||
|| echo "$resp" | sed -n 's/.*"command":"\([^"]*\)".*/\1/p')
|
||||
|
||||
[ -z "$cmd_id" ] || [ -z "$cmd_str" ] && return 0
|
||||
|
||||
log "Running command #${cmd_id}: ${cmd_str}"
|
||||
|
||||
# Execute with 60 s timeout, capture output, cap at 128 KB
|
||||
local tmp_out exit_code
|
||||
tmp_out=$(mktemp)
|
||||
timeout 60 bash -c "$cmd_str" > "$tmp_out" 2>&1
|
||||
exit_code=$?
|
||||
|
||||
# Truncate to 128 KB
|
||||
truncate -s -"$(( $(wc -c < "$tmp_out") > 131072 ? $(wc -c < "$tmp_out") - 131072 : 0 ))" "$tmp_out" 2>/dev/null || true
|
||||
|
||||
# Build JSON payload via python (handles all escaping correctly)
|
||||
local tmp_json
|
||||
tmp_json=$(mktemp)
|
||||
python3 - "$tmp_out" "$exit_code" > "$tmp_json" 2>/dev/null << 'PYEOF'
|
||||
import sys, json
|
||||
with open(sys.argv[1]) as f:
|
||||
output = f.read(131072)
|
||||
print(json.dumps({"output": output, "exit_code": int(sys.argv[2])}))
|
||||
PYEOF
|
||||
|
||||
curl -sf -m 15 -X POST \
|
||||
-H "Authorization: Bearer ${AGENT_TOKEN}" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d "@${tmp_json}" \
|
||||
"${SKYWATCH_URL}/api/agents/commands/${cmd_id}/result" >/dev/null 2>&1 || true
|
||||
|
||||
rm -f "$tmp_out" "$tmp_json"
|
||||
}
|
||||
|
||||
main() {
|
||||
load_config
|
||||
|
||||
@@ -291,6 +344,7 @@ main() {
|
||||
|
||||
while true; do
|
||||
report_metrics
|
||||
poll_commands
|
||||
# get_cpu_percent already consumed ~1 s; subtract it from the sleep
|
||||
local sleep_time=$(( interval - 1 ))
|
||||
[ "$sleep_time" -lt 1 ] && sleep_time=1
|
||||
|
||||
Reference in new issue
Block a user