$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)) { $token = $_SESSION['jarvis_token'] ?? ''; $localIP = $_SERVER['REMOTE_ADDR'] ?? ''; if (empty($token) && !in_array($localIP, ['127.0.0.1', '::1'])) { agent_error(401, 'Unauthorized'); } $agent = null; } 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 (!hash_equals(AGENT_REGISTRATION_KEY, $regKey)) 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) { $cmdIds = array_column($commands, 'id'); $placeholders = implode(',', array_fill(0, count($cmdIds), '?')); JarvisDB::query("UPDATE agent_commands SET status='delivered', delivered_at=NOW() WHERE id IN ($placeholders)", $cmdIds); 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); } $realIp = $_SERVER['HTTP_CF_CONNECTING_IP'] ?? $_SERVER['HTTP_X_FORWARDED_FOR'] ?? $_SERVER['REMOTE_ADDR'] ?? ''; $realIp = trim(explode(',', $realIp)[0]); agent_ok(['agents' => $agents, 'my_ip' => $realIp]); // ── 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); }