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)} + +| JOB TYPE | RISK | APPROVAL | AUTO (MIN) | ENABLED | |
|---|---|---|---|---|---|
| ${esc(r.job_type)} | +${r.risk_level.toUpperCase()} | +${reqLabel} | +${r.auto_approve_after_min || '—'} | +${enLabel} | +
+
+
+ |
+
| # | JOB TYPE | RISK | STATUS | DECIDED BY | DECIDED AT | NOTE |
|---|---|---|---|---|---|---|
| ${h.id} | +${esc(h.job_type)} | +${h.risk_level} | +${h.status.toUpperCase()} | +${esc(h.decided_by||'—')} | +${ts} | +${esc(h.decision_note||'')} | +