xml-pipeline/dashboard/dashboard.js
dullfig 06eeea3dee
Some checks failed
CI / test (3.11) (push) Has been cancelled
CI / test (3.12) (push) Has been cancelled
CI / test (3.13) (push) Has been cancelled
CI / lint (push) Has been cancelled
CI / typecheck (push) Has been cancelled
CI / docker (push) Has been cancelled
Add AgentOS container foundation, security hardening, and management plane
Invert the agent model: the agent IS the computer. The message pump
becomes the kernel, handlers are sandboxed apps, and all access is
mediated by the platform.

Phase 1 — Container foundation:
- Multi-stage Dockerfile (python:3.12-slim, non-root user, /data volume)
- deploy/entrypoint.py with --dry-run config validation
- docker-compose.yml (cap_drop ALL, read_only, no-new-privileges)
- docker-compose.dev.yml overlay for development
- CI Docker build smoke test

Phase 2 — Security hardening:
- xml_pipeline/security/ module with default-deny container mode
- Permission gate: per-listener tool allowlist enforcement
- Network policy: egress control (only declared LLM backend domains)
- Shell tool: disabled in container mode
- File tool: restricted to /data and /config in container mode
- Fetch tool: integrates network egress policy
- Config loader: parses security and network YAML sections

Phase 3 — Management plane:
- Agent app (port 8080): minimal /health, /inject, /ws only
- Management app (port 9090): full API, audit log, dashboard
- SQLite-backed audit log for tool invocations and security events
- Static web dashboard (no framework, WebSocket-driven)
- CLI --split flag for dual-port serving

All 439 existing tests pass with zero regressions.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-03 21:37:24 -08:00

342 lines
11 KiB
JavaScript

/**
* AgentOS Dashboard — WebSocket-driven real-time updates.
*
* No framework, no build step. Pure vanilla JS.
* Connects to the management port WebSocket at /ws.
*/
// State
let ws = null;
let reconnectTimeout = null;
let messageCount = 0;
const API_BASE = ''; // Same origin as management server
// =========================================================================
// WebSocket Connection
// =========================================================================
function connect() {
const protocol = location.protocol === 'https:' ? 'wss:' : 'ws:';
const wsUrl = `${protocol}//${location.host}/ws`;
ws = new WebSocket(wsUrl);
ws.onopen = () => {
setConnectionStatus(true);
// Request full state on connect
fetchInitialState();
};
ws.onmessage = (event) => {
try {
const data = JSON.parse(event.data);
handleEvent(data);
} catch (e) {
console.error('Failed to parse WebSocket message:', e);
}
};
ws.onclose = () => {
setConnectionStatus(false);
// Reconnect after delay
reconnectTimeout = setTimeout(connect, 3000);
};
ws.onerror = () => {
ws.close();
};
}
function setConnectionStatus(connected) {
const el = document.getElementById('connection-status');
if (connected) {
el.textContent = 'Connected';
el.className = 'status-indicator connected';
} else {
el.textContent = 'Disconnected';
el.className = 'status-indicator disconnected';
}
}
// =========================================================================
// Event Handling
// =========================================================================
function handleEvent(data) {
const event = data.event;
switch (event) {
case 'connected':
if (data.state) {
updateFullState(data.state);
}
break;
case 'agent_state':
updateAgentState(data);
break;
case 'message':
addMessage(data);
messageCount++;
updateMessageCount();
break;
case 'thread_created':
case 'thread_completed':
fetchThreads();
break;
default:
break;
}
}
// =========================================================================
// API Fetching
// =========================================================================
async function fetchJSON(path) {
try {
const resp = await fetch(`${API_BASE}${path}`);
if (!resp.ok) throw new Error(`HTTP ${resp.status}`);
return await resp.json();
} catch (e) {
console.error(`Fetch ${path} failed:`, e);
return null;
}
}
async function fetchInitialState() {
const [organism, agents, threads, usage] = await Promise.all([
fetchJSON('/api/v1/organism'),
fetchJSON('/api/v1/agents'),
fetchJSON('/api/v1/threads'),
fetchJSON('/api/v1/usage'),
]);
if (organism) {
document.getElementById('organism-name').textContent = organism.name;
updateUptime(organism.uptimeSeconds || 0);
}
if (agents) {
renderAgents(agents.agents || []);
const count = (agents.agents || []).length;
const active = (agents.agents || []).filter(a => a.state === 'processing').length;
document.getElementById('agent-count').textContent = count;
document.getElementById('agent-detail').textContent = `${active} active`;
}
if (threads) {
renderThreads(threads.threads || []);
document.getElementById('thread-count').textContent = (threads.threads || []).length;
}
if (usage) {
updateUsageCards(usage);
}
// Fetch audit
refreshAudit();
}
async function fetchThreads() {
const threads = await fetchJSON('/api/v1/threads');
if (threads) {
renderThreads(threads.threads || []);
document.getElementById('thread-count').textContent = (threads.threads || []).length;
}
}
// =========================================================================
// Rendering
// =========================================================================
function renderAgents(agents) {
const tbody = document.getElementById('agents-table');
if (!agents.length) {
tbody.innerHTML = '<tr><td colspan="5" class="empty">No agents registered</td></tr>';
return;
}
tbody.innerHTML = agents.map(a => `
<tr>
<td><code>${escapeHtml(a.name)}</code></td>
<td>${a.isAgent ? 'Agent' : 'Tool'}</td>
<td><span class="state-badge state-${(a.state || 'idle').toLowerCase()}">${a.state || 'idle'}</span></td>
<td>${(a.peers || []).map(p => `<code>${escapeHtml(p)}</code>`).join(', ') || '--'}</td>
<td>${a.messageCount || 0}</td>
</tr>
`).join('');
}
function renderThreads(threads) {
const tbody = document.getElementById('threads-table');
if (!threads.length) {
tbody.innerHTML = '<tr><td colspan="5" class="empty">No active threads</td></tr>';
return;
}
tbody.innerHTML = threads.map(t => `
<tr>
<td><code>${escapeHtml((t.threadId || t.id || '').substring(0, 8))}...</code></td>
<td><span class="state-badge state-${(t.status || 'active').toLowerCase()}">${t.status || 'active'}</span></td>
<td>${(t.participants || []).map(p => `<code>${escapeHtml(p)}</code>`).join(', ') || '--'}</td>
<td>${t.messageCount || 0}</td>
<td>${t.createdAt ? formatTime(t.createdAt) : '--'}</td>
</tr>
`).join('');
}
function addMessage(data) {
const log = document.getElementById('message-log');
const empty = log.querySelector('.empty');
if (empty) empty.remove();
const entry = document.createElement('div');
entry.className = 'log-entry';
entry.innerHTML = `
<span class="log-time">${formatTime(Date.now() / 1000)}</span>
<span class="log-from">${escapeHtml(data.from || data.fromId || '?')}</span>
<span class="log-content">${escapeHtml(data.payloadType || data.payload_type || JSON.stringify(data).substring(0, 200))}</span>
`;
log.insertBefore(entry, log.firstChild);
// Limit log entries
while (log.children.length > 500) {
log.removeChild(log.lastChild);
}
}
function updateAgentState(data) {
// Re-fetch agents on state change
fetchJSON('/api/v1/agents').then(agents => {
if (agents) {
renderAgents(agents.agents || []);
const count = (agents.agents || []).length;
const active = (agents.agents || []).filter(a => a.state === 'processing').length;
document.getElementById('agent-count').textContent = count;
document.getElementById('agent-detail').textContent = `${active} active`;
}
});
}
function updateUsageCards(usage) {
if (usage.totals) {
const tokens = usage.totals.totalTokens || 0;
const cost = usage.totals.totalCost || usage.totals.estimatedCost || 0;
document.getElementById('token-count').textContent = formatNumber(tokens);
document.getElementById('token-cost').textContent = `$${cost.toFixed(4)}`;
}
}
function updateMessageCount() {
document.getElementById('message-count').textContent = formatNumber(messageCount);
}
function updateUptime(seconds) {
const h = Math.floor(seconds / 3600);
const m = Math.floor((seconds % 3600) / 60);
const s = Math.floor(seconds % 60);
document.getElementById('uptime').textContent =
`${h.toString().padStart(2, '0')}:${m.toString().padStart(2, '0')}:${s.toString().padStart(2, '0')}`;
}
// =========================================================================
// Audit Log
// =========================================================================
async function refreshAudit() {
const severity = document.getElementById('audit-severity').value;
const type = document.getElementById('audit-type').value;
let url = '/api/v1/audit/events?limit=200';
if (severity) url += `&severity=${severity}`;
if (type) url += `&event_type=${type}`;
const data = await fetchJSON(url);
if (!data) return;
const log = document.getElementById('audit-log');
if (!data.events || !data.events.length) {
log.innerHTML = '<div class="empty">No audit events matching filters</div>';
return;
}
log.innerHTML = data.events.map(e => `
<div class="log-entry">
<span class="log-time">${formatTime(e.timestamp)}</span>
<span class="log-from severity-${e.severity}">[${e.severity.toUpperCase()}]</span>
<span class="log-from">${escapeHtml(e.listener_name)}</span>
<span class="log-content">${escapeHtml(e.event_type)}: ${escapeHtml(JSON.stringify(e.details).substring(0, 300))}</span>
</div>
`).join('');
}
// Make refreshAudit available globally for the onclick handler
window.refreshAudit = refreshAudit;
// =========================================================================
// Tab Navigation
// =========================================================================
document.querySelectorAll('.tab').forEach(tab => {
tab.addEventListener('click', () => {
document.querySelectorAll('.tab').forEach(t => t.classList.remove('active'));
document.querySelectorAll('.tab-content').forEach(c => c.classList.remove('active'));
tab.classList.add('active');
const target = document.getElementById(`tab-${tab.dataset.tab}`);
if (target) target.classList.add('active');
// Refresh data for active tab
if (tab.dataset.tab === 'audit') refreshAudit();
if (tab.dataset.tab === 'threads') fetchThreads();
});
});
// =========================================================================
// Utilities
// =========================================================================
function escapeHtml(str) {
if (!str) return '';
const div = document.createElement('div');
div.textContent = String(str);
return div.innerHTML;
}
function formatTime(timestamp) {
const d = new Date(timestamp * 1000);
return d.toLocaleTimeString();
}
function formatNumber(n) {
if (n >= 1_000_000) return (n / 1_000_000).toFixed(1) + 'M';
if (n >= 1_000) return (n / 1_000).toFixed(1) + 'K';
return String(n);
}
// =========================================================================
// Periodic Refresh
// =========================================================================
setInterval(() => {
// Refresh uptime
fetchJSON('/health').then(data => {
if (data && data.uptime_seconds !== undefined) {
updateUptime(data.uptime_seconds);
}
});
}, 10000);
setInterval(() => {
// Refresh usage
fetchJSON('/api/v1/usage').then(data => {
if (data) updateUsageCards(data);
});
}, 30000);
// =========================================================================
// Boot
// =========================================================================
connect();