Ported from authority branch agent/g7-security-critical-closure-20260914 (d5f5c9b, base=beabd053).
S1 auth: role/user from server token->identity mapping, ignore client X-AIOS-* headers.
S2 bootstrap credential: PBKDF2 random-salt, fail-closed SECRET, no plaintext fallback, session HMAC+expiry.
S3 path containment + control-plane file-read hardening.
S4 admin UI XSS + real governed approval + dashboard CSRF/branch validation.
S5 runtime delivery: registry.yaml conflict, openapi securityScheme, install.sh fail propagation, do_POST shadowing.
Tests: 5 negative suites (66 tests). Evidence pack in deliverables/g7-evidence/.
362 lines
18 KiB
HTML
362 lines
18 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>AI Evolution Dashboard</title>
|
|
<style>
|
|
* { margin: 0; padding: 0; box-sizing: border-box; }
|
|
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif; background: #0d1117; color: #c9d1d9; height: 100vh; overflow: hidden; }
|
|
.app { display: flex; height: 100vh; }
|
|
.sidebar { width: 260px; background: #161b22; border-right: 1px solid #30363d; padding: 20px; display: flex; flex-direction: column; gap: 16px; }
|
|
.sidebar h1 { font-size: 18px; color: #f0f6fc; border-bottom: 1px solid #30363d; padding-bottom: 12px; margin-bottom: 8px; }
|
|
.sidebar-nav { list-style: none; display: flex; flex-direction: column; gap: 4px; }
|
|
.sidebar-nav li { padding: 8px 12px; border-radius: 6px; cursor: pointer; font-size: 14px; color: #8b949e; }
|
|
.sidebar-nav li:hover, .sidebar-nav li.active { background: #1f6feb22; color: #58a6ff; }
|
|
.sidebar-nav li i { margin-right: 8px; }
|
|
.main { flex: 1; padding: 24px; overflow-y: auto; }
|
|
.main-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 24px; }
|
|
.main-header h2 { font-size: 20px; }
|
|
.status-badge { padding: 4px 12px; border-radius: 12px; font-size: 12px; font-weight: 600; text-transform: uppercase; }
|
|
.status-badge.ok { background: #1b3a2d; color: #3fb950; border: 1px solid #3fb95044; }
|
|
.status-badge.degraded { background: #3a2e1b; color: #d29922; border: 1px solid #d2992244; }
|
|
.status-badge.fail { background: #3a1b1b; color: #f85149; border: 1px solid #f8514944; }
|
|
.card { background: #161b22; border: 1px solid #30363d; border-radius: 8px; padding: 16px; margin-bottom: 16px; }
|
|
.card-title { font-size: 14px; color: #8b949e; margin-bottom: 8px; }
|
|
.card-value { font-size: 28px; font-weight: 700; color: #f0f6fc; }
|
|
.row { display: flex; gap: 16px; }
|
|
.col { flex: 1; }
|
|
.grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(300px, 1fr)); gap: 16px; }
|
|
pre { white-space: pre-wrap; word-break: break-word; }
|
|
.command-item { padding: 12px; margin-bottom: 8px; border-radius: 6px; background: #0d1117; border: 1px solid #21262d; }
|
|
.command-item .meta { display: flex; gap: 8px; align-items: center; font-size: 12px; color: #8b949e; }
|
|
.command-item .name { font-size: 14px; color: #58a6ff; font-weight: 600; }
|
|
.command-item .state { font-size: 11px; padding: 2px 8px; border-radius: 10px; text-transform: uppercase; }
|
|
.state-active { background: #1b3a2d; color: #3fb950; }
|
|
.state-running { background: #3a2e1b; color: #d29922; }
|
|
.state-completed { background: #1b3a2d; color: #3fb950; }
|
|
.state-failed { background: #3a1b1b; color: #f85149; }
|
|
.loading { text-align: center; padding: 40px; color: #8b949e; }
|
|
.error { padding: 20px; background: #3a1b1b; border: 1px solid #f8514944; border-radius: 8px; color: #f85149; margin-bottom: 16px; }
|
|
.version-item { padding: 8px 12px; background: #0d1117; border: 1px solid #21262d; border-radius: 6px; margin-bottom: 4px; display: flex; justify-content: space-between; }
|
|
.version-item .tag { color: #58a6ff; font-weight: 600; }
|
|
.version-item .name { color: #8b949e; font-size: 13px; }
|
|
.agent-item { background: #0d1117; border: 1px solid #21262d; border-radius: 6px; margin-bottom: 8px; padding: 12px; }
|
|
.agent-item .agent-title { font-size: 14px; font-weight: 600; color: #c9d1d9; margin-bottom: 4px; }
|
|
.agent-item .agent-meta { font-size: 12px; color: #8b949e; }
|
|
.agent-run-item { padding: 8px 12px; margin-bottom: 4px; border-radius: 4px; background: #0d1117; border: 1px solid #21262d; display: flex; justify-content: space-between; align-items: center; }
|
|
.agent-run-item .run-id { color: #58a6ff; font-size: 13px; font-weight: 600; }
|
|
.agent-run-item .source { color: #8b949e; font-size: 12px; }
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<div id="app" class="app">
|
|
<div class="sidebar">
|
|
<h1>🕹️ AI Evolution Dashboard</h1>
|
|
<ul class="sidebar-nav">
|
|
<li class="active" onclick="showPanel('overview')"><i>📊</i> Overview</li>
|
|
<li onclick="showPanel('versions')"><i>🏷️</i> Versions</li>
|
|
<li onclick="showPanel('commands')"><i>⚡</i> Commands</li>
|
|
<li onclick="showPanel('agents')"><i>🤖</i> Agent Runs</li>
|
|
<li onclick="showPanel('arch-owner')"><i>🔐</i> Owner Gate</li>
|
|
<li onclick="showPanel('arch-approve')"><i>✓</i> Approve Changes</li>
|
|
</ul>
|
|
</div>
|
|
<div class="main">
|
|
<div id="panel-overview" class="panel">
|
|
<div class="main-header">
|
|
<h2>System Overview</h2>
|
|
<div id="header-status" class="status-badge">Loading...</div>
|
|
</div>
|
|
<div id="loading" class="loading">Loading...</div>
|
|
<div id="error" class="error" style="display:none;"></div>
|
|
<div id="content">
|
|
<div class="row">
|
|
<div class="col"><div class="card"><div class="card-title">Current Baseline</div><div id="base" class="card-value">-</div></div></div>
|
|
<div class="col"><div class="card"><div class="card-title">Git Branch</div><div id="branch" class="card-value">-</div></div></div>
|
|
<div class="col"><div class="card"><div class="card-title">Uptime</div><div id="uptime" class="card-value">-</div></div></div>
|
|
</div>
|
|
<div class="card" style="margin-top:16px">
|
|
<div class="card-title">Event Bus Status</div>
|
|
<pre id="bus-status">-</pre>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div id="panel-versions" class="panel" style="display:none;">
|
|
<div class="main-header"><h2>Version Timeline</h2></div>
|
|
<div id="versions-list">Loading...</div>
|
|
</div>
|
|
<div id="panel-commands" class="panel" style="display:none;">
|
|
<div class="main-header"><h2>Command Lookup</h2></div>
|
|
<div id="commands-list">Loading...</div>
|
|
</div>
|
|
<div id="panel-agents" class="panel" style="display:none;">
|
|
<div class="main-header"><h2>Recent Agent Executions</h2></div>
|
|
<div id="panel-arch-owner" class="panel" style="display:none;">
|
|
<div class="main-header"><h2>🔐 Architecture Owner Gate</h2></div>
|
|
<div id="arch-owner-content">Loading...</div>
|
|
</div>
|
|
<div id="panel-arch-approve" class="panel" style="display:none;">
|
|
<div class="main-header"><h2>✓ Approve Architecture Changes</h2></div>
|
|
<div id="arch-approve-content">Loading...</div>
|
|
</div>
|
|
<div id="agents-list">Loading...</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<script>
|
|
// Security: all dynamic values (API/git/agent/owner-gate) are rendered with
|
|
// textContent / DOM APIs. innerHTML is never used to inject untrusted data.
|
|
function clearNode(node) {
|
|
if (!node) return;
|
|
while (node.firstChild) node.removeChild(node.firstChild);
|
|
}
|
|
function setText(id, text) {
|
|
const n = document.getElementById(id);
|
|
if (n) n.textContent = text;
|
|
}
|
|
function addClass(node, cls) { if (node) node.classList.add(cls); }
|
|
function div(cls) { const d = document.createElement('div'); if (cls) addClass(d, cls); return d; }
|
|
function span(cls, text) { const s = document.createElement('span'); if (cls) addClass(s, cls); if (text != null) s.textContent = text; return s; }
|
|
|
|
// Centralized CSRF token source: server-issued meta tag, or double-submit cookie.
|
|
function csrfToken() {
|
|
const meta = document.querySelector('meta[name="csrf-token"]');
|
|
if (meta && meta.content) return meta.content;
|
|
const m = document.cookie.match(/(?:^|;\s*)aios_csrf=([^;]+)/);
|
|
return m ? decodeURIComponent(m[1]) : '';
|
|
}
|
|
|
|
async function fetchData() {
|
|
const r = await fetch('/admin/ai-evolution/api/data');
|
|
const data = await r.json();
|
|
document.getElementById('loading').style.display = 'none';
|
|
document.getElementById('error').style.display = 'none';
|
|
document.getElementById('content').style.display = '';
|
|
|
|
const hs = document.getElementById('header-status');
|
|
hs.className = 'status-badge ' + (data.healthy ? 'ok' : 'fail');
|
|
hs.textContent = data.healthy ? '▪ operational' : '⚠ degraded';
|
|
|
|
document.getElementById('base').textContent = data.baseline || '-';
|
|
document.getElementById('branch').textContent = data.main_branch || 'main';
|
|
document.getElementById('uptime').textContent = data.uptime || '-';
|
|
document.getElementById('bus-status').textContent = data.bus ? JSON.stringify(data.bus, null, 2) : 'no event bus data';
|
|
}
|
|
|
|
async function loadVersions() {
|
|
const el = document.getElementById('versions-list');
|
|
clearNode(el);
|
|
const loading = div('loading'); loading.textContent = 'Loading versions...'; el.appendChild(loading);
|
|
try {
|
|
const r = await fetch('/admin/ai-evolution/api/versions');
|
|
const data = await r.json();
|
|
clearNode(el);
|
|
if (data.error) { const e = div('error'); e.textContent = data.error; el.appendChild(e); return; }
|
|
const grid = div('grid');
|
|
for (const v of data.versions || []) {
|
|
const card = div('card');
|
|
const t = div('card-title'); t.textContent = v.tag || '';
|
|
const val = div('card-value'); val.textContent = v.version || v.tag || '';
|
|
card.appendChild(t); card.appendChild(val); grid.appendChild(card);
|
|
}
|
|
el.appendChild(grid);
|
|
} catch (e) {
|
|
const err = div('error'); err.textContent = 'Failed to load versions: ' + e.message; el.appendChild(err);
|
|
}
|
|
}
|
|
|
|
async function loadCommands() {
|
|
const el = document.getElementById('commands-list');
|
|
clearNode(el);
|
|
const loading = div('loading'); loading.textContent = 'Loading commands...'; el.appendChild(loading);
|
|
try {
|
|
const r = await fetch('/admin/ai-evolution/api/data');
|
|
const data = await r.json();
|
|
const cmds = data.commands || [];
|
|
clearNode(el);
|
|
const wrapper = div('');
|
|
wrapper.style.display = 'flex'; wrapper.style.gap = '24px'; wrapper.style.marginBottom = '16px';
|
|
const totalCard = div('card');
|
|
const tcTitle = div('card-title'); tcTitle.textContent = 'Total';
|
|
const tcVal = div('card-value'); tcVal.textContent = String(cmds.length);
|
|
totalCard.appendChild(tcTitle); totalCard.appendChild(tcVal); wrapper.appendChild(totalCard);
|
|
const catCard = div('card');
|
|
const ccTitle = div('card-title'); ccTitle.textContent = 'By Category';
|
|
catCard.appendChild(ccTitle);
|
|
const cats = {};
|
|
for (const c of cmds) { const k = c.category || 'uncategorized'; cats[k] = (cats[k] || 0) + 1; }
|
|
for (const [k, v] of Object.entries(cats)) {
|
|
const line = div(''); line.style.fontSize = '14px'; line.style.color = '#8b949e'; line.style.margin = '4px 0';
|
|
line.textContent = k + ': ' + v; catCard.appendChild(line);
|
|
}
|
|
wrapper.appendChild(catCard); el.appendChild(wrapper);
|
|
for (const cmd of cmds.slice().reverse()) {
|
|
const item = div('command-item');
|
|
const meta = div('meta'); meta.textContent = (cmd.category || '') + ' | ' + (cmd.filename || '');
|
|
const name = div('name'); name.textContent = cmd.title || '';
|
|
const summary = div(''); summary.style.fontSize = '13px'; summary.style.color = '#c9d1d9'; summary.style.marginTop = '4px';
|
|
summary.textContent = cmd.summary || '';
|
|
item.appendChild(meta); item.appendChild(name); item.appendChild(summary);
|
|
el.appendChild(item);
|
|
}
|
|
} catch (e) {
|
|
const err = div('error'); err.textContent = 'Failed to load commands: ' + e.message; el.appendChild(err);
|
|
}
|
|
}
|
|
|
|
async function loadAgents() {
|
|
const el = document.getElementById('agents-list');
|
|
clearNode(el);
|
|
const loading = div('loading'); loading.textContent = 'Loading agent runs...'; el.appendChild(loading);
|
|
try {
|
|
const r = await fetch('/admin/ai-evolution/api/agents');
|
|
const data = await r.json();
|
|
clearNode(el);
|
|
if (data.error) { const e = div('error'); e.textContent = data.error; el.appendChild(e); return; }
|
|
const agents = data.agents || [];
|
|
if (!agents.length) {
|
|
const none = div(''); none.style.padding = '24px'; none.style.color = '#8b949e';
|
|
none.textContent = 'No agent runs recorded yet.'; el.appendChild(none); return;
|
|
}
|
|
for (const agent of agents) {
|
|
const item = div('agent-item');
|
|
const title = div('agent-title');
|
|
title.appendChild(document.createTextNode(agent.name || ''));
|
|
const st = (agent.status === 'running') ? 'running' : ((agent.status === 'ok') ? 'completed' : 'failed');
|
|
const badge = span('state state-' + st, agent.status || '');
|
|
title.appendChild(badge);
|
|
const meta = div('agent-meta');
|
|
meta.textContent = (agent.runs || 0) + ' runs | last: ' + (agent.last_run || '-');
|
|
item.appendChild(title); item.appendChild(meta); el.appendChild(item);
|
|
}
|
|
} catch (e) {
|
|
const err = div('error'); err.textContent = 'Failed to load agents: ' + e.message; el.appendChild(err);
|
|
}
|
|
}
|
|
|
|
async function loadOwnerGate() {
|
|
const el = document.getElementById('arch-owner-content');
|
|
clearNode(el);
|
|
const loading = div('loading'); loading.textContent = 'Loading owner gate data...'; el.appendChild(loading);
|
|
try {
|
|
const r = await fetch('/admin/ai-evolution/api/owner-gate');
|
|
const data = await r.json();
|
|
clearNode(el);
|
|
const row = div('row');
|
|
const cols = [['Pending Requests', data.pending_count], ['Approved', data.approved_count], ['Rejected', data.rejected_count]];
|
|
for (const [label, value] of cols) {
|
|
const col = div('col'); const card = div('card');
|
|
const t = div('card-title'); t.textContent = label;
|
|
const val = div('card-value'); val.textContent = value;
|
|
card.appendChild(t); card.appendChild(val); col.appendChild(card); row.appendChild(col);
|
|
}
|
|
el.appendChild(row);
|
|
const listCard = div('card');
|
|
const lt = div('card-title'); lt.textContent = 'Pending Approval Requests'; listCard.appendChild(lt);
|
|
const reqs = data.pending_requests || [];
|
|
if (reqs.length) {
|
|
for (const req of reqs) {
|
|
const item = div('command-item');
|
|
const name = div('name'); name.textContent = req.title || req.change_id || '';
|
|
const meta = div('meta'); meta.textContent = 'ID: ' + (req.change_id || '') + ' | Risk: ' + (req.risk || '');
|
|
const reason = div(''); reason.style.fontSize = '13px'; reason.style.color = '#c9d1d9'; reason.style.marginTop = '4px';
|
|
reason.textContent = req.reason || '';
|
|
item.appendChild(name); item.appendChild(meta); item.appendChild(reason);
|
|
listCard.appendChild(item);
|
|
}
|
|
} else {
|
|
const none = div(''); none.style.color = '#8b949e'; none.style.padding = '12px';
|
|
none.textContent = 'No pending requests.'; listCard.appendChild(none);
|
|
}
|
|
el.appendChild(listCard);
|
|
} catch (e) {
|
|
const err = div('error'); err.textContent = 'Failed to load: ' + e.message; el.appendChild(err);
|
|
}
|
|
}
|
|
|
|
// Real governed approve: posts the decision to the server-side governed endpoint.
|
|
async function approveDecision(changeId, decision, btn) {
|
|
if (btn) btn.disabled = true;
|
|
try {
|
|
const res = await fetch('/admin/ai-evolution/api/owner-gate/decision', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json', 'X-CSRF-Token': csrfToken() },
|
|
body: JSON.stringify({ change_id: changeId, decision: decision })
|
|
});
|
|
const payload = await res.json();
|
|
if (!res.ok) throw new Error(payload.error || ('HTTP ' + res.status));
|
|
await loadApprove();
|
|
} catch (e) {
|
|
if (btn) btn.disabled = false;
|
|
const out = document.getElementById('arch-approve-content');
|
|
const err = div('error'); err.textContent = 'Decision failed: ' + e.message;
|
|
if (out) out.appendChild(err);
|
|
}
|
|
}
|
|
|
|
async function loadApprove() {
|
|
const el = document.getElementById('arch-approve-content');
|
|
clearNode(el);
|
|
const loading = div('loading'); loading.textContent = 'Loading pending approvals...'; el.appendChild(loading);
|
|
try {
|
|
const r = await fetch('/admin/ai-evolution/api/owner-gate');
|
|
const data = await r.json();
|
|
clearNode(el);
|
|
const reqs = data.pending_requests || [];
|
|
if (reqs.length) {
|
|
const card = div('card');
|
|
const t = div('card-title'); t.textContent = 'Pending changes requiring your approval'; card.appendChild(t);
|
|
for (const req of reqs) {
|
|
const item = div('command-item');
|
|
const name = div('name'); name.textContent = req.title || req.change_id || '';
|
|
const meta = div('meta');
|
|
const strongRisk = document.createElement('strong'); strongRisk.textContent = req.risk || '';
|
|
meta.appendChild(document.createTextNode('Risk: ')); meta.appendChild(strongRisk);
|
|
const reason = div(''); reason.style.fontSize = '13px'; reason.style.color = '#c9d1d9'; reason.style.margin = '8px 0';
|
|
reason.textContent = req.reason || '';
|
|
const actions = div(''); actions.style.display = 'flex'; actions.style.gap = '8px';
|
|
const btn = document.createElement('button');
|
|
btn.textContent = '✓ Approve';
|
|
btn.style.padding = '6px 16px'; btn.style.background = '#238636'; btn.style.color = '#fff';
|
|
btn.style.border = 'none'; btn.style.borderRadius = '6px'; btn.style.cursor = 'pointer'; btn.style.fontSize = '13px';
|
|
const changeId = req.change_id;
|
|
btn.addEventListener('click', function () { approveDecision(changeId, 'approved', btn); });
|
|
actions.appendChild(btn);
|
|
item.appendChild(name); item.appendChild(meta); item.appendChild(reason); item.appendChild(actions);
|
|
card.appendChild(item);
|
|
}
|
|
el.appendChild(card);
|
|
} else {
|
|
const done = div('card');
|
|
const inner = div(''); inner.style.padding = '24px'; inner.style.textAlign = 'center'; inner.style.color = '#8b949e';
|
|
inner.textContent = '✅ All changes processed. No pending approval requests.';
|
|
done.appendChild(inner); el.appendChild(done);
|
|
}
|
|
} catch (e) {
|
|
const err = div('error'); err.textContent = 'Failed to load: ' + e.message; el.appendChild(err);
|
|
}
|
|
}
|
|
|
|
function showPanel(name) {
|
|
document.querySelectorAll('.panel').forEach(p => p.style.display = 'none');
|
|
document.getElementById('panel-' + name).style.display = '';
|
|
document.querySelectorAll('.sidebar-nav li').forEach(li => li.classList.remove('active'));
|
|
document.querySelector('.sidebar-nav li:nth-child(' + ({overview:1,versions:2,commands:3,agents:4,['arch-owner']:5,['arch-approve']:6}[name] || 1) + ')').classList.add('active');
|
|
if (name === 'overview') fetchData();
|
|
const loaders = {
|
|
'arch-owner': loadOwnerGate,
|
|
'arch-approve': loadApprove,
|
|
versions: loadVersions,
|
|
commands: loadCommands,
|
|
agents: loadAgents,
|
|
};
|
|
const fn = loaders[name];
|
|
if (fn) fn();
|
|
}
|
|
|
|
fetchData();
|
|
</script>
|
|
</body>
|
|
</html>
|