Files
jarvis/api/endpoints/agent.php
T
myron dc55e6c45b Initial commit: JARVIS AI dashboard v2.3
- 4-tier chat: HA control → Ollama → Groq → Claude
- Push-based agent system with heartbeat/metrics
- Network monitoring, alerts, Proxmox, Home Assistant
- Windows + Linux agent installers
- Stats cache cron, facts collector, KB engine
2026-05-25 13:22:57 +00:00

245 lines
11 KiB
PHP

<?php
/**
* JARVIS Agent API
* Handles registration, heartbeats, metrics, HA state pushes, and command polling
* from remote jarvis-agent clients on external networks.
*
* Auth: POST /api/agent/register → uses AGENT_REGISTRATION_KEY
* All other actions → uses X-Agent-Key header (per-agent key)
*/
header('Content-Type: application/json');
$agentAction = $action ?? ($parts[1] ?? '');
// ── Helpers ───────────────────────────────────────────────────────────────────
function agent_error(int $code, string $msg): void {
http_response_code($code);
echo json_encode(['error' => $msg]);
exit;
}
function agent_ok(array $payload = []): void {
echo json_encode(array_merge(['ok' => true], $payload));
exit;
}
function generate_api_key(): string {
return bin2hex(random_bytes(24));
}
function get_agent_by_key(string $key): ?array {
$rows = JarvisDB::query(
'SELECT * FROM registered_agents WHERE api_key = ? LIMIT 1',
[$key]
);
return $rows[0] ?? null;
}
function update_agent_seen(string $agentId, string $status = 'online'): void {
JarvisDB::query(
'UPDATE registered_agents SET last_seen = NOW(), status = ? WHERE agent_id = ?',
[$status, $agentId]
);
}
// ── Auth (all actions except register) ───────────────────────────────────────
$agentKey = $_SERVER['HTTP_X_AGENT_KEY'] ?? '';
$browserActions = ['list', 'status', 'myip'];
if ($agentAction !== 'register') {
if (in_array($agentAction, $browserActions)) {
$agent = null; // browser-accessible via session auth already validated by api.php
} else {
if (empty($agentKey)) agent_error(401, 'X-Agent-Key header required');
$agent = get_agent_by_key($agentKey);
if (!$agent) agent_error(401, 'Invalid agent key');
}
}
// ── Route ─────────────────────────────────────────────────────────────────────
switch ($agentAction) {
// ── REGISTER ─────────────────────────────────────────────────────────────
case 'register':
if ($method !== 'POST') agent_error(405, 'POST only');
$regKey = $_SERVER['HTTP_X_REGISTRATION_KEY'] ?? ($data['registration_key'] ?? '');
if ($regKey !== AGENT_REGISTRATION_KEY) agent_error(403, 'Invalid registration key');
$hostname = trim($data['hostname'] ?? '');
$agentType = $data['agent_type'] ?? 'linux';
$ipAddress = $data['ip_address'] ?? ($_SERVER['REMOTE_ADDR'] ?? '');
$capabilities = $data['capabilities'] ?? [];
$agentId = $data['agent_id'] ?? ($hostname . '_' . substr(md5($hostname . $ipAddress), 0, 8));
if (!$hostname) agent_error(400, 'hostname required');
if (!in_array($agentType, ['linux', 'homeassistant', 'proxmox', 'windows'])) agent_error(400, 'Invalid agent_type');
// Upsert agent
$existing = JarvisDB::query('SELECT api_key FROM registered_agents WHERE agent_id = ?', [$agentId]);
if ($existing) {
$apiKey = $existing[0]['api_key'];
JarvisDB::query(
'UPDATE registered_agents SET hostname=?, agent_type=?, ip_address=?, capabilities=?, last_seen=NOW(), status="online" WHERE agent_id=?',
[$hostname, $agentType, $ipAddress, json_encode($capabilities), $agentId]
);
} else {
$apiKey = generate_api_key();
JarvisDB::query(
'INSERT INTO registered_agents (agent_id, hostname, agent_type, ip_address, api_key, capabilities, last_seen, status) VALUES (?,?,?,?,?,?,NOW(),"online")',
[$agentId, $hostname, $agentType, $ipAddress, $apiKey, json_encode($capabilities)]
);
}
agent_ok(['agent_id' => $agentId, 'api_key' => $apiKey]);
// ── HEARTBEAT ────────────────────────────────────────────────────────────
case 'heartbeat':
update_agent_seen($agent['agent_id']);
// Return any pending commands for this agent
$commands = JarvisDB::query(
'SELECT id, command_type, command_data FROM agent_commands WHERE agent_id = ? AND status = "pending" ORDER BY created_at ASC LIMIT 10',
[$agent['agent_id']]
);
// Mark as delivered
if ($commands) {
$ids = implode(',', array_column($commands, 'id'));
JarvisDB::query("UPDATE agent_commands SET status='delivered', delivered_at=NOW() WHERE id IN ($ids)");
foreach ($commands as &$cmd) {
$cmd['command_data'] = json_decode($cmd['command_data'] ?? '{}', true);
}
}
agent_ok(['commands' => $commands ?: []]);
// ── METRICS ──────────────────────────────────────────────────────────────
case 'metrics':
if ($method !== 'POST') agent_error(405, 'POST only');
update_agent_seen($agent['agent_id']);
$metricType = $data['type'] ?? 'system';
$metricData = $data['data'] ?? [];
JarvisDB::query(
'INSERT INTO agent_metrics (agent_id, metric_type, metric_data) VALUES (?,?,?)',
[$agent['agent_id'], $metricType, json_encode($metricData)]
);
// Prune old metrics (keep 24h)
JarvisDB::query(
'DELETE FROM agent_metrics WHERE agent_id = ? AND recorded_at < DATE_SUB(NOW(), INTERVAL 24 HOUR)',
[$agent['agent_id']]
);
agent_ok();
// ── HA STATE PUSH ────────────────────────────────────────────────────────
case 'ha_state':
if ($method !== 'POST') agent_error(405, 'POST only');
update_agent_seen($agent['agent_id']);
$entities = $data['entities'] ?? [];
if (empty($entities)) agent_error(400, 'entities array required');
foreach ($entities as $e) {
$entityId = $e['entity_id'] ?? '';
$entityName = $e['name'] ?? $e['friendly_name'] ?? $entityId;
$domain = explode('.', $entityId)[0] ?? 'unknown';
$state = $e['state'] ?? '';
$attrs = $e['attributes'] ?? [];
$lastChanged = $e['last_changed'] ?? date('Y-m-d H:i:s');
if (!$entityId) continue;
JarvisDB::query(
'INSERT INTO ha_entities (agent_id, entity_id, entity_name, domain, state, attributes, last_changed, updated_at)
VALUES (?,?,?,?,?,?,?,NOW())
ON DUPLICATE KEY UPDATE entity_name=VALUES(entity_name), state=VALUES(state), attributes=VALUES(attributes), last_changed=VALUES(last_changed), updated_at=NOW()',
[$agent['agent_id'], $entityId, $entityName, $domain, $state, json_encode($attrs), $lastChanged]
);
}
// Also update kb_facts for compatibility with existing KB engine
$entityMap = [];
$all = JarvisDB::query('SELECT entity_id, entity_name, domain, state, attributes FROM ha_entities WHERE agent_id = ?', [$agent['agent_id']]);
foreach ($all as $row) {
$entityMap[$row['entity_id']] = [
'entity_id' => $row['entity_id'],
'name' => $row['entity_name'],
'domain' => $row['domain'],
'state' => $row['state'],
'attributes'=> json_decode($row['attributes'] ?? '{}', true),
];
}
JarvisDB::query(
'INSERT INTO kb_facts (fact_key, fact_value, fact_type) VALUES ("ha/entity_map", ?, "json")
ON DUPLICATE KEY UPDATE fact_value=VALUES(fact_value), updated_at=NOW()',
[json_encode($entityMap)]
);
agent_ok(['accepted' => count($entities)]);
// ── COMMAND RESULT ───────────────────────────────────────────────────────
case 'command_result':
if ($method !== 'POST') agent_error(405, 'POST only');
$cmdId = (int)($data['command_id'] ?? 0);
$result = $data['result'] ?? [];
$status = ($data['success'] ?? true) ? 'executed' : 'failed';
JarvisDB::query(
'UPDATE agent_commands SET status=?, executed_at=NOW(), result=? WHERE id=? AND agent_id=?',
[$status, json_encode($result), $cmdId, $agent['agent_id']]
);
agent_ok();
// ── LIST (admin: get all agents status) ──────────────────────────────────
case 'list':
// Mark agents offline if last_seen > 2 minutes ago
JarvisDB::query(
'UPDATE registered_agents SET status="offline" WHERE last_seen < DATE_SUB(NOW(), INTERVAL 2 MINUTE) AND status = "online"'
);
$agents = JarvisDB::query('SELECT agent_id, hostname, agent_type, ip_address, status, last_seen, capabilities FROM registered_agents ORDER BY agent_type, hostname');
foreach ($agents as &$a) {
$a['capabilities'] = json_decode($a['capabilities'] ?? '[]', true);
}
agent_ok(['agents' => $agents, 'my_ip' => $_SERVER['REMOTE_ADDR'] ?? '']);
// ── LATEST METRICS (for dashboard display) ───────────────────────────────
case 'status':
$agentIdReq = $data['agent_id'] ?? ($parts[2] ?? '');
$whereAgent = $agentIdReq ? 'AND agent_id = ?' : '';
$params = $agentIdReq ? [$agentIdReq] : [];
$latest = JarvisDB::query(
"SELECT agent_id, metric_type, metric_data, recorded_at
FROM agent_metrics
WHERE (agent_id, metric_type, recorded_at) IN (
SELECT agent_id, metric_type, MAX(recorded_at)
FROM agent_metrics $whereAgent
GROUP BY agent_id, metric_type
)
ORDER BY agent_id, metric_type",
$params
);
$grouped = [];
foreach ($latest as $row) {
$grouped[$row['agent_id']][$row['metric_type']] = json_decode($row['metric_data'], true);
$grouped[$row['agent_id']]['recorded_at'] = $row['recorded_at'];
}
agent_ok(['metrics' => $grouped]);
// ── MY IP (browser client IP detection) ──────────────────────────────────
case 'myip':
agent_ok(['ip' => $_SERVER['REMOTE_ADDR'] ?? '']);
default:
agent_error(404, 'Unknown agent action: ' . $agentAction);
}