Fix Agentic Ops 401 — reuse Desk global api() and session check.

Forward Authorization in nginx; accept Spec 027 roles in JWT decode.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Ligbox Spec Hub 2026-06-20 06:28:42 +00:00
parent 40bb16bbc9
commit 6daa692af8
5 changed files with 37 additions and 16 deletions

View file

@ -14,8 +14,8 @@ from typing import Any
from fastapi import Depends, Header, HTTPException, Request from fastapi import Depends, Header, HTTPException, Request
from jose import JWTError, jwt from jose import JWTError, jwt
import bcrypt import bcrypt
import time
from app.permissions import HUMAN_ROLES
from app.totp_util import verify_code as verify_totp_code from app.totp_util import verify_code as verify_totp_code
DB_PATH = Path(os.getenv("SQLITE_PATH", "/data/ops.db")) DB_PATH = Path(os.getenv("SQLITE_PATH", "/data/ops.db"))
@ -134,7 +134,7 @@ def decode_token(token: str) -> DeskUser:
raise HTTPException(401, "invalid or expired token") from exc raise HTTPException(401, "invalid or expired token") from exc
username = payload.get("sub") username = payload.get("sub")
role = payload.get("role") role = payload.get("role")
if not username or role not in {"super_admin", "ops_lead", "technician", "noc"}: if not username or role not in HUMAN_ROLES:
raise HTTPException(401, "invalid token claims") raise HTTPException(401, "invalid token claims")
with db() as conn: with db() as conn:
row = conn.execute( row = conn.execute(

View file

@ -2,10 +2,13 @@
const esc = (s) => String(s ?? '').replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;'); const esc = (s) => String(s ?? '').replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;');
let state = { threadId: null, selectedAgent: 'A6' }; let state = { threadId: null, selectedAgent: 'A6' };
async function api(path, opts = {}) { /** Usa o helper global do Desk (app.js) — garante JWT igual aos outros módulos. */
async function agentsApi(path, opts = {}) {
const deskApi = typeof globalThis.api === 'function' ? globalThis.api : null;
if (deskApi) return deskApi(`/v1/agents${path}`, opts);
const h = authHeaders({ ...(opts.headers || {}) }); const h = authHeaders({ ...(opts.headers || {}) });
if (!(opts.body instanceof FormData) && !h['Content-Type']) h['Content-Type'] = 'application/json'; if (!(opts.body instanceof FormData) && !h['Content-Type']) h['Content-Type'] = 'application/json';
const r = await fetchWithTimeout(`/api/v1/agents${path}`, { ...opts, headers: h }); const r = await fetchWithTimeout(`/api/v1/agents${path}`, { ...opts, headers: h }, 60000);
if (r.status === 401) { if (r.status === 401) {
logout(); logout();
throw new Error('sessão expirada — faça login novamente'); throw new Error('sessão expirada — faça login novamente');
@ -53,7 +56,7 @@
const box = el.querySelector('#agentic-thread-messages'); const box = el.querySelector('#agentic-thread-messages');
if (!box) return; if (!box) return;
box.innerHTML = '<p class="loading">Carregando thread…</p>'; box.innerHTML = '<p class="loading">Carregando thread…</p>';
const data = await api(`/threads/${threadId}/messages`); const data = await agentsApi(`/threads/${threadId}/messages`);
box.innerHTML = data.messages.map(threadBubble).join('') || '<p class="empty">Sem mensagens.</p>'; box.innerHTML = data.messages.map(threadBubble).join('') || '<p class="empty">Sem mensagens.</p>';
box.scrollTop = box.scrollHeight; box.scrollTop = box.scrollHeight;
} }
@ -62,13 +65,24 @@
const el = document.getElementById('agentic-ops-content'); const el = document.getElementById('agentic-ops-content');
if (!el) return; if (!el) return;
el.innerHTML = '<p class="loading">Carregando Agentic Ops…</p>'; el.innerHTML = '<p class="loading">Carregando Agentic Ops…</p>';
if (!getToken()) {
el.innerHTML = '<p class="error">Sessão não encontrada neste endereço. <a href="/login.html">Fazer login</a> (use sempre o mesmo URL — ex. desk.ligbox.com.br).</p>';
return;
}
if (typeof ensureValidSession === 'function') {
const ok = await ensureValidSession();
if (!ok) {
el.innerHTML = '<p class="error">Sessão expirada. <a href="/login.html">Fazer login</a></p>';
return;
}
}
try { try {
const [health, roster, inbox, threads, findings] = await Promise.all([ const [health, roster, inbox, threads, findings] = await Promise.all([
api('/health'), agentsApi('/health'),
api('/roster'), agentsApi('/roster'),
api('/inbox?limit=20'), agentsApi('/inbox?limit=20'),
api('/threads?limit=15'), agentsApi('/threads?limit=15'),
api('/findings?limit=15'), agentsApi('/findings?limit=15'),
]); ]);
const tier = health.tier === 't1' ? 'T1 LLM' : 'T0'; const tier = health.tier === 't1' ? 'T1 LLM' : 'T0';
const ollama = health.ollama const ollama = health.ollama
@ -149,7 +163,7 @@
}); });
el.querySelectorAll('[data-ack-msg]').forEach(btn => { el.querySelectorAll('[data-ack-msg]').forEach(btn => {
btn.addEventListener('click', async () => { btn.addEventListener('click', async () => {
await api(`/messages/${btn.dataset.ackMsg}/ack`, { method: 'POST' }); await agentsApi(`/messages/${btn.dataset.ackMsg}/ack`, { method: 'POST' });
await renderAgenticOps(); await renderAgenticOps();
}); });
}); });
@ -162,7 +176,7 @@
const tid = state.threadId || parseInt(el.querySelector('#agentic-thread-select')?.value, 10); const tid = state.threadId || parseInt(el.querySelector('#agentic-thread-select')?.value, 10);
const body = (input?.value || '').trim(); const body = (input?.value || '').trim();
if (!tid || !body) return; if (!tid || !body) return;
await api(`/threads/${tid}/reply`, { await agentsApi(`/threads/${tid}/reply`, {
method: 'POST', method: 'POST',
body: JSON.stringify({ body, target_agent: state.selectedAgent }), body: JSON.stringify({ body, target_agent: state.selectedAgent }),
}); });
@ -177,7 +191,7 @@
out.hidden = false; out.hidden = false;
out.innerHTML = '<p class="loading">A pensar…</p>'; out.innerHTML = '<p class="loading">A pensar…</p>';
try { try {
const res = await api('/chat', { const res = await agentsApi('/chat', {
method: 'POST', method: 'POST',
body: JSON.stringify({ question: q, include_findings: true, target_agent: state.selectedAgent }), body: JSON.stringify({ question: q, include_findings: true, target_agent: state.selectedAgent }),
}); });

View file

@ -443,7 +443,7 @@
<script src="/assets/tickets-workspace.js?v=20260619tickets2"></script> <script src="/assets/tickets-workspace.js?v=20260619tickets2"></script>
<script src="/assets/tickets-detail-panel.js?v=20260619tickets2"></script> <script src="/assets/tickets-detail-panel.js?v=20260619tickets2"></script>
<script src="/assets/servicos.js?v=20260620agentic"></script> <script src="/assets/servicos.js?v=20260620agentic"></script>
<script src="/assets/agentic-ops.js?v=20260620agentic2"></script> <script src="/assets/agentic-ops.js?v=20260620agentic3"></script>
<script src="/assets/app.js?v=20260620agentic"></script> <script src="/assets/app.js?v=20260620agentic3"></script>
</body> </body>
</html> </html>

View file

@ -4,7 +4,7 @@ server {
resolver 127.0.0.11 valid=10s ipv6=off; resolver 127.0.0.11 valid=10s ipv6=off;
location ~* \.(html)$ { location ~* \.(html|js)$ {
add_header Cache-Control "no-cache, no-store, must-revalidate"; add_header Cache-Control "no-cache, no-store, must-revalidate";
try_files $uri =404; try_files $uri =404;
} }
@ -26,6 +26,7 @@ server {
proxy_pass http://$upstream:8080$request_uri; proxy_pass http://$upstream:8080$request_uri;
proxy_set_header Host $host; proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Real-IP $remote_addr;
proxy_set_header Authorization $http_authorization;
proxy_connect_timeout 30s; proxy_connect_timeout 30s;
proxy_send_timeout 600s; proxy_send_timeout 600s;
proxy_read_timeout 600s; proxy_read_timeout 600s;

View file

@ -11,9 +11,15 @@ server {
proxy_set_header Host $host; proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header Authorization $http_authorization;
proxy_read_timeout 180s; proxy_read_timeout 180s;
} }
location ~* \.(html|js)$ {
add_header Cache-Control "no-cache, no-store, must-revalidate";
try_files $uri =404;
}
location / { location / {
try_files $uri $uri/ /index.html; try_files $uri $uri/ /index.html;
} }