From 93d7594c4f8ab3adbab9ef89a8b9b59763c39de5 Mon Sep 17 00:00:00 2001 From: Myron Blair Date: Thu, 11 Jun 2026 12:19:14 +0000 Subject: [PATCH] =?UTF-8?q?Phase=209:=20Clearance=20Protocol=20=E2=80=94?= =?UTF-8?q?=20intercept,=20approve/deny,=20HUD,=20voice=20commands?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - reactor.py v9.0.0: clearance endpoints, watchdog, create_job intercept - arc.php: 7 clearance actions (pending/history/approve/deny/rules/rule_update/create) - chat.php: Tier 0.9j voice commands — approve/deny/status clearance - index.html: clearance banner, CLEARANCE tab with pending requests + rules + history - admin/index.php: CLEARANCE nav + tab with full CRUD for rules and approve/deny UI --- api/endpoints/arc.php | 46 ++++++ api/endpoints/chat.php | 86 +++++++++++ public_html/admin/index.php | 282 ++++++++++++++++++++++++++++++++++++ public_html/index.html | 201 +++++++++++++++++++++++++ 4 files changed, 615 insertions(+) diff --git a/api/endpoints/arc.php b/api/endpoints/arc.php index e7f08dd..c0a375a 100644 --- a/api/endpoints/arc.php +++ b/api/endpoints/arc.php @@ -292,6 +292,52 @@ switch ($action) { echo json_encode(arc_request('PUT', "/missions/{$id}", ['enabled' => $enabled])); break; + // GET /api/arc?action=clearance_pending + case 'clearance_pending': + echo json_encode(arc_request('GET', '/clearance/pending')); + break; + + // GET /api/arc?action=clearance_history + case 'clearance_history': + $limit = (int)($_GET['limit'] ?? 50); + echo json_encode(arc_request('GET', "/clearance/history?limit={$limit}")); + break; + + // POST /api/arc?action=clearance_approve&id=123 body: { decided_by: "..." } + case 'clearance_approve': + $id = (int)($_GET['id'] ?? $data['id'] ?? 0); + if (!$id) { http_response_code(400); echo json_encode(['error' => 'Missing id']); break; } + $decided_by = $data['decided_by'] ?? 'admin'; + echo json_encode(arc_request('POST', "/clearance/{$id}/approve", ['decided_by' => $decided_by])); + break; + + // POST /api/arc?action=clearance_deny&id=123 body: { decided_by: "...", note: "..." } + case 'clearance_deny': + $id = (int)($_GET['id'] ?? $data['id'] ?? 0); + if (!$id) { http_response_code(400); echo json_encode(['error' => 'Missing id']); break; } + $decided_by = $data['decided_by'] ?? 'admin'; + $note = $data['note'] ?? ''; + echo json_encode(arc_request('POST', "/clearance/{$id}/deny", ['decided_by' => $decided_by, 'note' => $note])); + break; + + // GET /api/arc?action=clearance_rules + case 'clearance_rules': + echo json_encode(arc_request('GET', '/clearance/rules')); + break; + + // PUT /api/arc?action=clearance_rule_update&id=123 body: { require_approval: 0|1, auto_approve_after_min: N, ... } + case 'clearance_rule_update': + $id = (int)($_GET['id'] ?? $data['id'] ?? 0); + if (!$id) { http_response_code(400); echo json_encode(['error' => 'Missing id']); break; } + unset($data['id']); + echo json_encode(arc_request('PUT', "/clearance/rules/{$id}", $data)); + break; + + // POST /api/arc?action=clearance_rule_create body: { job_type, risk_level, require_approval, ... } + case 'clearance_rule_create': + echo json_encode(arc_request('POST', '/clearance/rules', $data)); + break; + default: http_response_code(404); echo json_encode(['error' => "Unknown arc action: {$action}"]); diff --git a/api/endpoints/chat.php b/api/endpoints/chat.php index eaf2f9d..7babf1b 100644 --- a/api/endpoints/chat.php +++ b/api/endpoints/chat.php @@ -1082,6 +1082,33 @@ if (!$reply && preg_match('/\b(news|headlines|latest|what.?s happening|current e $arcJobId = null; // Helper: submit job to Arc Reactor +function arcPost(string $path, array $body): ?array { + $ch = curl_init('http://127.0.0.1:7474' . $path); + curl_setopt_array($ch, [ + CURLOPT_RETURNTRANSFER => true, + CURLOPT_POST => true, + CURLOPT_POSTFIELDS => json_encode($body), + CURLOPT_HTTPHEADER => ['Content-Type: application/json'], + CURLOPT_TIMEOUT => 5, + CURLOPT_CONNECTTIMEOUT => 3, + ]); + $res = json_decode(curl_exec($ch), true); + curl_close($ch); + return $res; +} + +function arcGet(string $path): ?array { + $ch = curl_init('http://127.0.0.1:7474' . $path); + curl_setopt_array($ch, [ + CURLOPT_RETURNTRANSFER => true, + CURLOPT_TIMEOUT => 5, + CURLOPT_CONNECTTIMEOUT => 3, + ]); + $res = json_decode(curl_exec($ch), true); + curl_close($ch); + return $res; +} + function arcSubmitJob(string $type, array $payload, string $sessionId): ?array { $ch = curl_init('http://127.0.0.1:7474/job'); curl_setopt_array($ch, [ @@ -1395,6 +1422,65 @@ if (!$reply) { } } +// ── Tier 0.9j: Clearance Protocol — approve/deny voice commands ───────────── +if (!$reply) { + // "approve clearance 5", "authorize clearance", "approve all clearance" + if (preg_match('/^(?:jarvis[,\s]+)?(?:approve|authorize|grant)\s+(?:all\s+)?clearance(?:\s+(?:request\s+)?#?(\d+))?/i', $message, $m)) { + $crId = isset($m[1]) && $m[1] ? (int)$m[1] : null; + if ($crId) { + $resp = arcPost('/clearance/' . $crId . '/approve', ['decided_by' => 'voice']); + if (isset($resp['ok']) && $resp['ok']) { + $reply = "◈ Clearance request #{$crId} authorized, {$userAddr}. Job dispatched."; + } else { + $reply = "Clearance #{$crId} not found or already decided."; + } + } else { + // Approve all pending + $pending = arcGet('/clearance/pending') ?: []; + if (empty($pending)) { + $reply = "No pending clearance requests, {$userAddr}."; + } else { + $approved = 0; + foreach ($pending as $cr) { + $resp = arcPost('/clearance/' . $cr['id'] . '/approve', ['decided_by' => 'voice']); + if (isset($resp['ok']) && $resp['ok']) $approved++; + } + $reply = "◈ Authorized {$approved} clearance request" . ($approved !== 1 ? 's' : '') . ", {$userAddr}."; + } + } + $source = 'arc:clearance_approve'; + } +} +if (!$reply) { + // "deny clearance 5", "reject clearance 5" + if (preg_match('/^(?:jarvis[,\s]+)?(?:deny|reject|refuse)\s+clearance(?:\s+(?:request\s+)?#?(\d+))?/i', $message, $m)) { + $crId = isset($m[1]) && $m[1] ? (int)$m[1] : null; + if ($crId) { + $resp = arcPost('/clearance/' . $crId . '/deny', ['decided_by' => 'voice', 'note' => 'denied by voice command']); + $reply = isset($resp['ok']) && $resp['ok'] + ? "Clearance #{$crId} denied, {$userAddr}." + : "Clearance #{$crId} not found or already decided."; + } else { + $reply = "Which clearance request should I deny? Say: deny clearance [number]."; + } + $source = 'arc:clearance_deny'; + } +} +if (!$reply) { + // "any pending clearance", "clearance status", "show clearance" + if (preg_match('/^(?:jarvis[,\s]+)?(?:(?:any\s+|show\s+)?pending\s+clearance|clearance\s+(?:status|requests?|pending|queue))/i', $message)) { + $pending = arcGet('/clearance/pending') ?: []; + $count = count($pending); + if ($count === 0) { + $reply = "No pending clearance requests, {$userAddr}. All clear."; + } else { + $list = array_map(fn($cr) => "#{$cr['id']} {$cr['job_type']} ({$cr['risk_level']})", $pending); + $reply = "◈ {$count} pending clearance request" . ($count !== 1 ? 's' : '') . ": " . implode(', ', $list) . ". Say 'approve clearance [number]' to authorize."; + } + $source = 'arc:clearance_status'; + } +} + // ── Tier 1: Intent Engine (instant, no LLM) ─────────────────────────────── if (!$reply) { $matched = KBEngine::match($message); diff --git a/public_html/admin/index.php b/public_html/admin/index.php index 8f042c2..9413b73 100644 --- a/public_html/admin/index.php +++ b/public_html/admin/index.php @@ -763,6 +763,76 @@ if ($action) { $raw = curl_exec($ch); curl_close($ch); j(json_decode($raw, true) ?: ['ok'=>true]); + // ── CLEARANCE PROTOCOL ─────────────────────────────────────────────── + case 'clearance_pending': + $ch = curl_init('http://127.0.0.1:7474/clearance/pending'); + curl_setopt_array($ch, [CURLOPT_RETURNTRANSFER=>true, CURLOPT_TIMEOUT=>5]); + $raw = curl_exec($ch); curl_close($ch); + j(json_decode($raw, true) ?: []); + + case 'clearance_history': + $limit = min((int)($_GET['limit'] ?? 50), 200); + $ch = curl_init('http://127.0.0.1:7474/clearance/history?limit=' . $limit); + curl_setopt_array($ch, [CURLOPT_RETURNTRANSFER=>true, CURLOPT_TIMEOUT=>5]); + $raw = curl_exec($ch); curl_close($ch); + j(json_decode($raw, true) ?: []); + + case 'clearance_approve': + $id = (int)($_GET['id'] ?? 0); if (!$id) bad('Missing id'); + $body = json_decode(file_get_contents('php://input'), true) ?: []; + $decidedBy = $body['decided_by'] ?? 'admin'; + $ch = curl_init('http://127.0.0.1:7474/clearance/' . $id . '/approve'); + curl_setopt_array($ch, [ + CURLOPT_RETURNTRANSFER=>true, CURLOPT_TIMEOUT=>10, CURLOPT_POST=>true, + CURLOPT_POSTFIELDS => json_encode(['decided_by'=>$decidedBy]), + CURLOPT_HTTPHEADER => ['Content-Type: application/json'], + ]); + $raw = curl_exec($ch); curl_close($ch); + j(json_decode($raw, true) ?: ['ok'=>true]); + + case 'clearance_deny': + $id = (int)($_GET['id'] ?? 0); if (!$id) bad('Missing id'); + $body = json_decode(file_get_contents('php://input'), true) ?: []; + $decidedBy = $body['decided_by'] ?? 'admin'; + $note = $body['note'] ?? ''; + $ch = curl_init('http://127.0.0.1:7474/clearance/' . $id . '/deny'); + curl_setopt_array($ch, [ + CURLOPT_RETURNTRANSFER=>true, CURLOPT_TIMEOUT=>5, CURLOPT_POST=>true, + CURLOPT_POSTFIELDS => json_encode(['decided_by'=>$decidedBy,'note'=>$note]), + CURLOPT_HTTPHEADER => ['Content-Type: application/json'], + ]); + $raw = curl_exec($ch); curl_close($ch); + j(json_decode($raw, true) ?: ['ok'=>true]); + + case 'clearance_rules': + $ch = curl_init('http://127.0.0.1:7474/clearance/rules'); + curl_setopt_array($ch, [CURLOPT_RETURNTRANSFER=>true, CURLOPT_TIMEOUT=>5]); + $raw = curl_exec($ch); curl_close($ch); + j(json_decode($raw, true) ?: []); + + case 'clearance_rule_update': + $id = (int)($_GET['id'] ?? 0); if (!$id) bad('Missing id'); + $body = json_decode(file_get_contents('php://input'), true) ?: []; + $ch = curl_init('http://127.0.0.1:7474/clearance/rules/' . $id); + curl_setopt_array($ch, [ + CURLOPT_RETURNTRANSFER=>true, CURLOPT_TIMEOUT=>5, CURLOPT_CUSTOMREQUEST=>'PUT', + CURLOPT_POSTFIELDS => json_encode($body), + CURLOPT_HTTPHEADER => ['Content-Type: application/json'], + ]); + $raw = curl_exec($ch); curl_close($ch); + j(json_decode($raw, true) ?: ['ok'=>true]); + + case 'clearance_rule_create': + $body = json_decode(file_get_contents('php://input'), true) ?: []; + $ch = curl_init('http://127.0.0.1:7474/clearance/rules'); + curl_setopt_array($ch, [ + CURLOPT_RETURNTRANSFER=>true, CURLOPT_TIMEOUT=>5, CURLOPT_POST=>true, + CURLOPT_POSTFIELDS => json_encode($body), + CURLOPT_HTTPHEADER => ['Content-Type: application/json'], + ]); + $raw = curl_exec($ch); curl_close($ch); + j(json_decode($raw, true) ?: ['ok'=>true]); + // ── VISION PROTOCOL ────────────────────────────────────────────────── case 'vision_list': $limit = min((int)($_GET['limit'] ?? 30), 100); @@ -1100,6 +1170,7 @@ select.filter-sel:focus{border-color:var(--cyan)} + @@ -1657,6 +1728,56 @@ select.filter-sel:focus{border-color:var(--cyan)} + +
+
🔒 CLEARANCE PROTOCOL
+
+ +
+
+ +
+ +
+
PENDING AUTHORIZATION
+
LOADING...
+
+ +
+
CLEARANCE RULES
+
LOADING...
+
+
ADD CUSTOM RULE
+
+
JOB TYPE
+
RISK LEVEL
+ +
+
REQUIRE APPROVAL
+ +
+
AUTO-APPROVE AFTER (MIN)
+
+ + +
+
+
+ + +
+
DECISION HISTORY
+
LOADING...
+
+
+ @@ -1772,6 +1893,7 @@ function loadTab(tab) { outbox: loadOutbox, missions: loadMissions, directives: loadDirectives, + clearance: loadClearance, vision: loadVision, guardian: loadGuardian, tasks: loadTasks, @@ -3820,6 +3942,166 @@ async function syncCalNow() { }); } +// ── CLEARANCE PROTOCOL ──────────────────────────────────────────────────────── +async function loadClearance() { + const [pending, rules, history] = await Promise.all([ + api('clearance_pending'), + api('clearance_rules'), + api('clearance_history&limit=30'), + ]); + const pList = Array.isArray(pending) ? pending : []; + const rList = Array.isArray(rules) ? rules : []; + const hList = Array.isArray(history) ? history : []; + + document.getElementById('clearance-badge').textContent = + pList.length ? pList.length + ' PENDING' : 'ALL CLEAR'; + + // Pending + const pelEl = document.getElementById('clearance-pending-list'); + if (!pList.length) { + pelEl.innerHTML = '
No pending requests
'; + } else { + pelEl.innerHTML = pList.map(cr => { + const pl = typeof cr.job_payload === 'string' ? JSON.parse(cr.job_payload||'{}') : (cr.job_payload||{}); + const ts = cr.created_at ? new Date(cr.created_at).toLocaleString() : ''; + const riskColor = {critical:'var(--red)',high:'var(--yellow)',medium:'var(--orange)'}[cr.risk_level] || 'var(--dim)'; + return `
+
+ #${cr.id} ${esc(cr.job_type.toUpperCase().replace(/_/g,' '))} + ${ts} +
+
${esc(cr.description||'No description')}
+
Payload: ${esc(JSON.stringify(pl))}
+
+ + +
+
`; + }).join(''); + } + + // Rules + const rElEl = document.getElementById('clearance-rules-list'); + if (!rList.length) { + rElEl.innerHTML = '
No rules configured
'; + } else { + rElEl.innerHTML = '' + + rList.map(r => { + const enLabel = r.enabled ? 'ON' : 'OFF'; + const reqLabel = r.require_approval ? 'REQUIRED' : 'BYPASS'; + const riskColor = {critical:'var(--red)',high:'var(--yellow)',medium:'var(--orange)'}[r.risk_level] || 'var(--dim)'; + return ` + + + + + + + `; + }).join('') + '
JOB TYPERISKAPPROVALAUTO (MIN)ENABLED
${esc(r.job_type)}${r.risk_level.toUpperCase()}${reqLabel}${r.auto_approve_after_min || '—'}${enLabel}
+ + +
'; + } + + // History + const hEl = document.getElementById('clearance-history-list'); + const decided = hList.filter(h => h.status !== 'pending').slice(0,20); + if (!decided.length) { + hEl.innerHTML = '
No history yet
'; + } else { + const statusColor = {approved:'var(--green)',denied:'var(--red)',expired:'var(--dim)',auto_approved:'var(--cyan)'}; + hEl.innerHTML = '' + + decided.map(h => { + const sc = statusColor[h.status] || 'var(--dim)'; + const ts = h.decided_at ? new Date(h.decided_at).toLocaleString() : ''; + return ` + + + + + + + + `; + }).join('') + '
#JOB TYPERISKSTATUSDECIDED BYDECIDED ATNOTE
${h.id}${esc(h.job_type)}${h.risk_level}${h.status.toUpperCase()}${esc(h.decided_by||'—')}${ts}${esc(h.decision_note||'')}
'; + } +} + +async function clearanceDecide(id, action) { + const label = action === 'approve' ? 'AUTHORIZE' : 'DENY'; + if (!confirm(`${label} clearance request #${id}?`)) return; + let note = ''; + if (action === 'deny') note = prompt('Reason for denial (optional):') || ''; + const body = {decided_by: 'admin'}; + if (note) body.note = note; + try { + const r = await fetch(location.href + '?action=clearance_' + action + '&id=' + id, { + method: 'POST', headers: {'Content-Type':'application/json'}, body: JSON.stringify(body) + }); + const d = await r.json(); + if (d.ok || d.job_id) { + toast(label + 'D clearance #' + id, 'ok'); + loadClearance(); + } else { + toast('Error: ' + (d.error || d.detail || 'unknown'), 'err'); + } + } catch(e) { toast('Request failed', 'err'); } +} + +async function clearanceRuleToggle(id, newEnabled) { + try { + await fetch(location.href + '?action=clearance_rule_update&id=' + id, { + method: 'POST', headers: {'Content-Type':'application/json'}, + body: JSON.stringify({enabled: newEnabled}) + }); + toast(newEnabled ? 'Rule enabled' : 'Rule disabled', 'ok'); + loadClearance(); + } catch(e) { toast('Failed', 'err'); } +} + +async function clearanceRuleEdit(id) { + // Open a simple prompt-based edit for auto_approve_after_min + const mins = prompt('Auto-approve after N minutes (blank = never require auto-approval):'); + if (mins === null) return; + const body = {auto_approve_after_min: mins === '' ? null : parseInt(mins)}; + try { + await fetch(location.href + '?action=clearance_rule_update&id=' + id, { + method: 'POST', headers: {'Content-Type':'application/json'}, body: JSON.stringify(body) + }); + toast('Rule updated', 'ok'); + loadClearance(); + } catch(e) { toast('Failed', 'err'); } +} + +async function clearanceRuleCreate() { + const jobType = document.getElementById('clr-new-type').value.trim(); + if (!jobType) { toast('Job type required', 'err'); return; } + const body = { + job_type: jobType, + risk_level: document.getElementById('clr-new-risk').value, + require_approval: parseInt(document.getElementById('clr-new-req').value), + auto_approve_after_min: document.getElementById('clr-new-auto').value || null, + description: document.getElementById('clr-new-desc').value.trim(), + enabled: 1, + }; + try { + const r = await fetch(location.href + '?action=clearance_rule_create', { + method: 'POST', headers: {'Content-Type':'application/json'}, body: JSON.stringify(body) + }); + const d = await r.json(); + if (d.ok) { + toast('Rule created', 'ok'); + document.getElementById('clr-new-type').value = ''; + document.getElementById('clr-new-desc').value = ''; + document.getElementById('clr-new-auto').value = ''; + loadClearance(); + } else { + toast('Error: ' + (d.detail || 'unknown'), 'err'); + } + } catch(e) { toast('Failed', 'err'); } +} + diff --git a/public_html/index.html b/public_html/index.html index 37ede2c..e138d61 100644 --- a/public_html/index.html +++ b/public_html/index.html @@ -980,6 +980,37 @@ body::after{ .mission-run-status.running{color:#ffd700;animation:pulse 1.5s ease-in-out infinite} .mission-new-btn{width:100%;background:rgba(0,212,255,0.06);border:1px solid rgba(0,212,255,0.3);border-radius:4px;padding:5px;color:var(--cyan);font-family:var(--font-display);font-size:0.52rem;letter-spacing:2px;cursor:pointer;margin-bottom:7px} .mission-new-btn:hover{background:rgba(0,212,255,0.12)} +/* ── CLEARANCE PROTOCOL ──────────────────────────────────────────── */ +#clearance-banner{display:none;background:rgba(255,34,68,0.08);border:1px solid rgba(255,34,68,0.4);border-radius:var(--r);padding:6px 10px;margin:0 0 8px;font-family:var(--font-display);font-size:0.55rem;letter-spacing:1px;color:#ff6680;animation:borderPulse 2s ease-in-out infinite} +@keyframes borderPulse{0%,100%{border-color:rgba(255,34,68,0.4)}50%{border-color:rgba(255,34,68,0.9)}} +#clearance-banner.active{display:flex;align-items:center;gap:8px} +#clearance-banner .clr-count{background:rgba(255,34,68,0.3);border-radius:3px;padding:1px 5px;font-size:0.6rem;color:#ff2244} +#clearance-banner .clr-view{margin-left:auto;cursor:pointer;color:#ff6680;text-decoration:underline} +.clr-card{background:rgba(255,34,68,0.04);border:1px solid rgba(255,34,68,0.3);border-radius:var(--r);margin-bottom:7px;overflow:hidden} +.clr-card-head{display:flex;align-items:center;gap:8px;padding:8px 10px;cursor:pointer;user-select:none} +.clr-card-head:hover{background:rgba(255,34,68,0.06)} +.clr-card-type{font-family:var(--font-display);font-size:0.6rem;letter-spacing:1px;flex:1;color:#ff8899} +.clr-card-risk{font-family:var(--font-mono);font-size:0.5rem;padding:2px 5px;border-radius:2px;border:1px solid} +.clr-card-risk.critical{color:#ff2244;border-color:rgba(255,34,68,0.5)} +.clr-card-risk.high{color:#ffd700;border-color:rgba(255,215,0,0.4)} +.clr-card-risk.medium{color:#ff9900;border-color:rgba(255,153,0,0.4)} +.clr-card-body{display:none;padding:8px 10px 10px;border-top:1px solid rgba(255,34,68,0.2)} +.clr-card.open .clr-card-body{display:block} +.clr-card-desc{font-family:var(--font-mono);font-size:0.55rem;color:var(--text-dim);margin-bottom:8px;line-height:1.5;white-space:pre-wrap} +.clr-action-bar{display:flex;gap:6px;margin-top:8px} +.clr-approve-btn{flex:1;background:rgba(0,255,136,0.08);border:1px solid rgba(0,255,136,0.4);border-radius:3px;padding:4px 8px;color:#00ff88;font-family:var(--font-display);font-size:0.5rem;letter-spacing:2px;cursor:pointer} +.clr-approve-btn:hover{background:rgba(0,255,136,0.18)} +.clr-deny-btn{flex:1;background:rgba(255,34,68,0.08);border:1px solid rgba(255,34,68,0.4);border-radius:3px;padding:4px 8px;color:#ff2244;font-family:var(--font-display);font-size:0.5rem;letter-spacing:2px;cursor:pointer} +.clr-deny-btn:hover{background:rgba(255,34,68,0.18)} +.clr-history-row{display:flex;align-items:center;gap:6px;padding:3px 0;border-bottom:1px solid rgba(255,255,255,0.04);font-family:var(--font-mono);font-size:0.52rem;color:var(--text-dim)} +.clr-status-approved{color:#00ff88}.clr-status-denied{color:#ff2244}.clr-status-pending{color:#ffd700}.clr-status-expired{color:rgba(255,255,255,0.3)} +.clr-rule-row{display:flex;align-items:center;gap:6px;padding:5px 0;border-bottom:1px solid rgba(255,255,255,0.04);font-family:var(--font-mono);font-size:0.52rem} +.clr-rule-type{flex:1;color:var(--cyan)} +.clr-rule-toggle{cursor:pointer;padding:2px 6px;border-radius:2px;font-size:0.48rem;border:1px solid} +.clr-rule-enabled{color:#00ff88;border-color:rgba(0,255,136,0.4)} +.clr-rule-disabled{color:rgba(255,255,255,0.3);border-color:rgba(255,255,255,0.15)} +.clr-admin-btn{width:100%;background:rgba(255,34,68,0.06);border:1px solid rgba(255,34,68,0.3);border-radius:4px;padding:5px;color:#ff6680;font-family:var(--font-display);font-size:0.52rem;letter-spacing:2px;cursor:pointer;margin-bottom:7px} +.clr-admin-btn:hover{background:rgba(255,34,68,0.12)} /* ── INTEL PROTOCOL — research result cards ──────────────────────── */ .intel-card{background:rgba(0,212,255,0.04);border:1px solid var(--panel-border);border-radius:var(--r);margin-bottom:8px;overflow:hidden} .intel-card-head{display:flex;align-items:center;gap:8px;padding:8px 10px;cursor:pointer;user-select:none} @@ -1196,6 +1227,13 @@ body::after{
+ +
+ ◈ CLEARANCE REQUIRED — + 0 + PENDING AUTHORIZATION + VIEW → +
HOME
@@ -1208,6 +1246,7 @@ body::after{
GUARDIAN
MISSIONS
DIRECTIVES
+
CLEARANCE
@@ -1241,6 +1280,9 @@ body::after{
+
+
+
@@ -2461,6 +2503,11 @@ function showApp(name, greeting, silent = false) { startGuardianPolling(); setInterval(_pollProactiveChat, 30000); }, 5000); + // Clearance banner — poll every 30s + setTimeout(() => { + updateClearanceBanner(); + setInterval(updateClearanceBanner, 30000); + }, 6000); } async function logout() { @@ -3163,6 +3210,7 @@ function switchTab(name) { if (name === 'guardian') loadGuardian(); if (name === 'missions') loadMissionsHud(); if (name === 'directives') loadDirectivesHud(); + if (name === 'clearance') loadClearanceHud(); if (name === 'alerts') loadAlerts(); } @@ -4360,6 +4408,159 @@ async function hudDirectiveReview(id) { } } +// ── CLEARANCE PROTOCOL HUD ───────────────────────────────────────────────────── +const _clrOpenCards = new Set(); + +async function updateClearanceBanner() { + try { + const pending = await api('arc?action=clearance_pending'); + const list = Array.isArray(pending) ? pending : []; + const count = list.length; + const banner = document.getElementById('clearance-banner'); + const badge = document.getElementById('clr-tab-badge'); + const bcount = document.getElementById('clr-banner-count'); + if (banner) { + if (count > 0) { + banner.classList.add('active'); + if (bcount) bcount.textContent = count; + } else { + banner.classList.remove('active'); + } + } + if (badge) { + if (count > 0) { badge.style.display = 'inline'; badge.textContent = count; } + else badge.style.display = 'none'; + } + } catch(e) {} +} + +async function loadClearanceHud() { + const el = document.getElementById('clearance-hud'); + if (!el) return; + try { + const [pendingRes, rulesRes, historyRes] = await Promise.all([ + api('arc?action=clearance_pending'), + api('arc?action=clearance_rules'), + api('arc?action=clearance_history&limit=20') + ]); + const pending = Array.isArray(pendingRes) ? pendingRes : []; + const rules = Array.isArray(rulesRes) ? rulesRes : []; + const history = Array.isArray(historyRes) ? historyRes : []; + + let html = ''; + + // Pending requests + html += `
PENDING AUTHORIZATION (${pending.length})
`; + if (!pending.length) { + html += '
◈ NO PENDING CLEARANCE REQUESTS
'; + } else { + for (const cr of pending) { + const isOpen = _clrOpenCards.has(cr.id); + const pl = typeof cr.job_payload === 'string' ? JSON.parse(cr.job_payload || '{}') : (cr.job_payload || {}); + const created = cr.created_at ? new Date(cr.created_at).toLocaleString() : ''; + const expires = cr.expires_at ? new Date(cr.expires_at).toLocaleString() : ''; + html += `
+
+ ${escHtml(cr.job_type.toUpperCase().replace(/_/g,' '))} + ${cr.risk_level.toUpperCase()} + #${cr.id} +
+
+
${escHtml(cr.description || 'No description')}
+
+ Requested: ${created}${expires ? ' · Expires: ' + expires : ''} +
+
+ Payload: ${escHtml(JSON.stringify(pl))} +
+
+ + +
+
+
`; + } + } + + // Rules + html += `
CLEARANCE RULES
`; + if (!rules.length) { + html += '
No rules configured
'; + } else { + html += '
'; + for (const r of rules) { + const enClass = r.enabled ? 'clr-rule-enabled' : 'clr-rule-disabled'; + const enLabel = r.enabled ? 'ON' : 'OFF'; + const reqLabel = r.require_approval ? 'REQUIRES APPROVAL' : 'AUTO-ALLOW'; + const autoTxt = r.auto_approve_after_min ? ` · AUTO ${r.auto_approve_after_min}m` : ''; + html += `
+ ${r.job_type.replace(/_/g,' ').toUpperCase()} + ${r.risk_level.toUpperCase()} + ${reqLabel}${autoTxt} + +
`; + } + html += '
'; + } + + // Recent history + html += `
RECENT HISTORY
`; + const recentDecided = history.filter(h => h.status !== 'pending').slice(0, 10); + if (!recentDecided.length) { + html += '
No history yet
'; + } else { + html += '
'; + for (const h of recentDecided) { + const ts = h.decided_at ? new Date(h.decided_at).toLocaleString() : ''; + html += `
+ + ${h.job_type.replace(/_/g,' ').toUpperCase()} + ${h.status.toUpperCase()} + ${ts} +
`; + } + html += '
'; + } + + el.innerHTML = html; + await updateClearanceBanner(); + } catch(e) { + if (el) el.innerHTML = '
CLEARANCE SYSTEM OFFLINE
'; + } +} + +function toggleClrCard(id) { + const card = document.getElementById('clr-card-' + id); + if (!card) return; + if (_clrOpenCards.has(id)) _clrOpenCards.delete(id); + else _clrOpenCards.add(id); + card.classList.toggle('open'); +} + +async function hudClearanceDecide(id, action) { + const label = action === 'approve' ? 'AUTHORIZE' : 'DENY'; + if (!confirm(`${label} clearance request #${id}?`)) return; + const note = action === 'deny' ? (prompt('Reason for denial (optional):') || '') : ''; + try { + const res = await api(`arc?action=clearance_${action}&id=${id}`, 'POST', { decided_by: 'admin', note }); + const msg = action === 'approve' + ? `◈ Clearance #${id} authorized. Job dispatched.` + : `◈ Clearance #${id} denied${note ? ': ' + note : ''}.`; + addMessage('jarvis', msg); + speak(action === 'approve' ? 'Clearance granted. Job dispatched.' : 'Request denied.'); + await loadClearanceHud(); + } catch(e) { + addMessage('system', 'Clearance action failed.'); + } +} + +async function hudClearanceRuleToggle(id, newEnabled) { + try { + await api(`arc?action=clearance_rule_update&id=${id}`, 'POST', { enabled: newEnabled }); + await loadClearanceHud(); + } catch(e) {} +} + async function loadAgents() { const [listData, metricsData] = await Promise.all([ api('agent/list'),