YashVardhan-coder commited on
Commit
cb3d641
·
1 Parent(s): 085eb5d

Fix post-deployment issues: admin iframe login, false positives, responsive UI, chat memory, stop generation button, stream cut-offs, and alert retries

Browse files
app/main.py CHANGED
@@ -64,10 +64,7 @@ def get_login_page():
64
 
65
  @app.get("/dashboard")
66
  def get_dashboard_ui(request: Request):
67
- """Serves the security admin dashboard, checking session cookie."""
68
- token = request.cookies.get("session_token")
69
- if token != "guardrail_shield_admin_session_token_secure":
70
- return RedirectResponse(url="/login", status_code=303)
71
  return FileResponse(os.path.join(static_dir, "index.html"))
72
 
73
  @app.get("/health", tags=["Health"])
 
64
 
65
  @app.get("/dashboard")
66
  def get_dashboard_ui(request: Request):
67
+ """Serves the security admin dashboard."""
 
 
 
68
  return FileResponse(os.path.join(static_dir, "index.html"))
69
 
70
  @app.get("/health", tags=["Health"])
app/routers/auth.py CHANGED
@@ -12,31 +12,45 @@ class LoginRequest(BaseModel):
12
  password: str
13
 
14
  def check_admin_session(request: Request):
15
- """Dependency to check if the session cookie is valid."""
16
  token = request.cookies.get("session_token")
 
 
 
 
17
  if token != SESSION_TOKEN:
18
  raise HTTPException(status_code=401, detail="Unauthorized admin session.")
19
  return True
20
 
21
  @router.post("/login")
22
- def login(payload: LoginRequest, response: Response):
 
23
  if payload.username == rules_manager.admin_username and payload.password == rules_manager.admin_password:
24
  response.set_cookie(
25
  key="session_token",
26
  value=SESSION_TOKEN,
27
  httponly=True,
28
- samesite="lax",
29
- max_age=1800 # 30 mins session
 
30
  )
31
- return {"status": "success", "message": "Successfully authenticated."}
 
 
 
 
32
  raise HTTPException(status_code=401, detail="Invalid username or password.")
33
 
34
  @router.post("/logout")
35
- def logout(response: Response):
36
- response.delete_cookie("session_token")
 
37
  return {"status": "success", "message": "Logged out."}
38
 
39
  @router.get("/session")
40
  def get_session(request: Request):
41
  token = request.cookies.get("session_token")
 
 
 
42
  return {"authenticated": token == SESSION_TOKEN}
 
12
  password: str
13
 
14
  def check_admin_session(request: Request):
15
+ """Dependency to check if the session cookie or Authorization header is valid."""
16
  token = request.cookies.get("session_token")
17
+ auth_header = request.headers.get("Authorization")
18
+ if auth_header and auth_header.startswith("Bearer "):
19
+ token = auth_header.split(" ")[1]
20
+
21
  if token != SESSION_TOKEN:
22
  raise HTTPException(status_code=401, detail="Unauthorized admin session.")
23
  return True
24
 
25
  @router.post("/login")
26
+ def login(payload: LoginRequest, request: Request, response: Response):
27
+ is_secure = request.url.scheme == "https"
28
  if payload.username == rules_manager.admin_username and payload.password == rules_manager.admin_password:
29
  response.set_cookie(
30
  key="session_token",
31
  value=SESSION_TOKEN,
32
  httponly=True,
33
+ samesite="none", # Allow cross-site requests inside Hugging Face iframes
34
+ secure=is_secure, # Required for cross-site cookie but only over HTTPS (breaks testclient on HTTP otherwise)
35
+ max_age=1800 # 30 mins session
36
  )
37
+ return {
38
+ "status": "success",
39
+ "message": "Successfully authenticated.",
40
+ "token": SESSION_TOKEN
41
+ }
42
  raise HTTPException(status_code=401, detail="Invalid username or password.")
43
 
44
  @router.post("/logout")
45
+ def logout(request: Request, response: Response):
46
+ is_secure = request.url.scheme == "https"
47
+ response.delete_cookie("session_token", samesite="none", secure=is_secure)
48
  return {"status": "success", "message": "Logged out."}
49
 
50
  @router.get("/session")
51
  def get_session(request: Request):
52
  token = request.cookies.get("session_token")
53
+ auth_header = request.headers.get("Authorization")
54
+ if auth_header and auth_header.startswith("Bearer "):
55
+ token = auth_header.split(" ")[1]
56
  return {"authenticated": token == SESSION_TOKEN}
app/routers/shield.py CHANGED
@@ -123,6 +123,7 @@ def validate_payload(payload: ShieldRequest, background_tasks: BackgroundTasks):
123
  @router.post("/generate")
124
  def generate_response(payload: ShieldRequest, background_tasks: BackgroundTasks):
125
  prompt_text = ""
 
126
  if payload.prompt:
127
  prompt_text = payload.prompt
128
  elif payload.messages:
@@ -130,6 +131,9 @@ def generate_response(payload: ShieldRequest, background_tasks: BackgroundTasks)
130
  if user_messages:
131
  prompt_text = user_messages[-1].content
132
 
 
 
 
133
  if not prompt_text:
134
  raise HTTPException(status_code=400, detail="Prompt input is required for generation.")
135
 
@@ -163,12 +167,20 @@ def generate_response(payload: ShieldRequest, background_tasks: BackgroundTasks)
163
  def input_blocked_generator():
164
  yield f"data: {json.dumps({'safe': False, 'response': 'Request blocked due to input safety violation.', 'details': response_details})}\n\n"
165
 
166
- return StreamingResponse(input_blocked_generator(), media_type="text/event-stream")
 
 
 
 
 
 
 
 
167
 
168
  def streaming_generator():
169
  accumulated_response = ""
170
  try:
171
- for chunk in llm_gateway.stream_from_llm(prompt_text):
172
  accumulated_response += chunk
173
 
174
  # Run Output Guard on accumulated text so far
@@ -226,7 +238,15 @@ def generate_response(payload: ShieldRequest, background_tasks: BackgroundTasks)
226
  except Exception as ex:
227
  yield f"data: {json.dumps({'safe': False, 'response': f'Stream error: {str(ex)}'})}\n\n"
228
 
229
- return StreamingResponse(streaming_generator(), media_type="text/event-stream")
 
 
 
 
 
 
 
 
230
 
231
  # Non-streaming flow
232
  input_report = input_guard.validate_input(prompt_text)
@@ -260,7 +280,7 @@ def generate_response(payload: ShieldRequest, background_tasks: BackgroundTasks)
260
  "details": response_details
261
  }
262
 
263
- llm_response = llm_gateway.send_to_llm(prompt_text)
264
 
265
  output_report = output_guard.validate_output(llm_response)
266
  if not output_report["safe"]:
 
123
  @router.post("/generate")
124
  def generate_response(payload: ShieldRequest, background_tasks: BackgroundTasks):
125
  prompt_text = ""
126
+ messages_list = None
127
  if payload.prompt:
128
  prompt_text = payload.prompt
129
  elif payload.messages:
 
131
  if user_messages:
132
  prompt_text = user_messages[-1].content
133
 
134
+ if payload.messages:
135
+ messages_list = [{"role": msg.role, "content": msg.content} for msg in payload.messages]
136
+
137
  if not prompt_text:
138
  raise HTTPException(status_code=400, detail="Prompt input is required for generation.")
139
 
 
167
  def input_blocked_generator():
168
  yield f"data: {json.dumps({'safe': False, 'response': 'Request blocked due to input safety violation.', 'details': response_details})}\n\n"
169
 
170
+ return StreamingResponse(
171
+ input_blocked_generator(),
172
+ media_type="text/event-stream",
173
+ headers={
174
+ "Cache-Control": "no-cache, no-transform",
175
+ "Connection": "keep-alive",
176
+ "X-Accel-Buffering": "no"
177
+ }
178
+ )
179
 
180
  def streaming_generator():
181
  accumulated_response = ""
182
  try:
183
+ for chunk in llm_gateway.stream_from_llm(prompt_text, messages=messages_list):
184
  accumulated_response += chunk
185
 
186
  # Run Output Guard on accumulated text so far
 
238
  except Exception as ex:
239
  yield f"data: {json.dumps({'safe': False, 'response': f'Stream error: {str(ex)}'})}\n\n"
240
 
241
+ return StreamingResponse(
242
+ streaming_generator(),
243
+ media_type="text/event-stream",
244
+ headers={
245
+ "Cache-Control": "no-cache, no-transform",
246
+ "Connection": "keep-alive",
247
+ "X-Accel-Buffering": "no"
248
+ }
249
+ )
250
 
251
  # Non-streaming flow
252
  input_report = input_guard.validate_input(prompt_text)
 
280
  "details": response_details
281
  }
282
 
283
+ llm_response = llm_gateway.send_to_llm(prompt_text, messages=messages_list)
284
 
285
  output_report = output_guard.validate_output(llm_response)
286
  if not output_report["safe"]:
app/services/alert_service.py CHANGED
@@ -103,24 +103,36 @@ def send_telegram_alert(prompt: str, category: str, risk_score: float, matched_r
103
  url = f"https://api.telegram.org/bot{bot_token}/sendMessage"
104
  message = _format_telegram_html(prompt, category, risk_score, matched_rule, timestamp)
105
 
106
- try:
107
- with httpx.Client(timeout=10.0) as client:
108
- resp = client.post(url, json={
109
- "chat_id": chat_id,
110
- "text": message,
111
- "parse_mode": "HTML"
112
- })
113
-
114
- if resp.status_code == 200 and resp.json().get("ok"):
115
- logger.info(f"Telegram alert sent successfully to chat_id={chat_id}")
116
- return {"success": True, "detail": "Telegram alert sent successfully."}
117
- else:
118
- error_detail = resp.json().get("description", resp.text)
119
- logger.error(f"Telegram API error: {error_detail}")
120
- return {"success": False, "detail": f"Telegram API error: {error_detail}"}
121
- except Exception as e:
122
- logger.error(f"Telegram alert failed: {e}")
123
- return {"success": False, "detail": f"Telegram alert failed: {str(e)}"}
 
 
 
 
 
 
 
 
 
 
 
 
124
 
125
 
126
  def send_slack_alert(prompt: str, category: str, risk_score: float, matched_rule: str, timestamp: str = None) -> dict:
 
103
  url = f"https://api.telegram.org/bot{bot_token}/sendMessage"
104
  message = _format_telegram_html(prompt, category, risk_score, matched_rule, timestamp)
105
 
106
+ import time
107
+ max_retries = 3
108
+ retry_delay = 2.0
109
+ last_error = "Unknown error"
110
+
111
+ for attempt in range(1, max_retries + 1):
112
+ try:
113
+ with httpx.Client(timeout=10.0) as client:
114
+ resp = client.post(url, json={
115
+ "chat_id": chat_id,
116
+ "text": message,
117
+ "parse_mode": "HTML"
118
+ })
119
+
120
+ if resp.status_code == 200 and resp.json().get("ok"):
121
+ logger.info(f"Telegram alert sent successfully to chat_id={chat_id} (attempt {attempt})")
122
+ return {"success": True, "detail": "Telegram alert sent successfully."}
123
+ else:
124
+ error_detail = resp.json().get("description", resp.text)
125
+ logger.warning(f"Telegram API error on attempt {attempt}/{max_retries}: {error_detail}")
126
+ last_error = f"Telegram API error: {error_detail}"
127
+ except Exception as e:
128
+ logger.warning(f"Telegram alert connection failed on attempt {attempt}/{max_retries}: {e}")
129
+ last_error = f"Connection failed: {str(e)}"
130
+
131
+ if attempt < max_retries:
132
+ time.sleep(retry_delay * attempt)
133
+
134
+ logger.error(f"Telegram alert failed after {max_retries} attempts: {last_error}")
135
+ return {"success": False, "detail": f"Telegram alert failed: {last_error}"}
136
 
137
 
138
  def send_slack_alert(prompt: str, category: str, risk_score: float, matched_rule: str, timestamp: str = None) -> dict:
app/services/input_guard.py CHANGED
@@ -45,11 +45,18 @@ def validate_input(prompt: str) -> dict:
45
 
46
  # Detect benign greetings / introductions to bypass DeBERTa false positives
47
  is_benign_greeting = bool(re.match(
48
- r"(?i)^\s*(hello|hi|hey|greetings|good\s+morning|good\s+afternoon|good\s+evening|howdy)?[,\s]*(i\s+am\s+[a-zA-Z0-9_-]+|my\s+name\s+is\s+[a-zA-Z0-9_-]+)?[,\s]*(nice\s+to\s+meet\s+you|pleased\s+to\s+meet\s+you|how\s+are\s+you)?[.!?\s]*$",
49
  prompt
50
  ))
 
 
 
 
 
 
 
51
 
52
- if not model_result["safe"] and (is_coding_request or is_benign_greeting):
53
  return {
54
  "safe": True,
55
  "risk_score": model_result["risk_score"],
 
45
 
46
  # Detect benign greetings / introductions to bypass DeBERTa false positives
47
  is_benign_greeting = bool(re.match(
48
+ r"(?i)^\s*(hello|hi|hey|greetings|good\s+morning|good\s+afternoon|good\s+evening|howdy)?[,\s]*(i\s+am|my\s+name\s+is|this\s+is|call\s+me)?\s*[a-zA-Z0-9_-]+[,\s]*(remember\s+it|remember\s+this|who\s+are\s+you|how\s+are\s+you)?[.!?\s]*$",
49
  prompt
50
  ))
51
+
52
+ # Detect mathematical expressions
53
+ is_math = bool(re.match(r"^\s*[\d\s+\-*/%^()=.]+\s*$", prompt))
54
+
55
+ # Detect long repetitive gibberish/random strings (e.g. repeating characters)
56
+ clean_prompt = prompt.strip().lower()
57
+ is_repetitive = len(clean_prompt) > 20 and len(set(clean_prompt.replace(" ", ""))) <= 4
58
 
59
+ if not model_result["safe"] and (is_coding_request or is_benign_greeting or is_math or is_repetitive):
60
  return {
61
  "safe": True,
62
  "risk_score": model_result["risk_score"],
app/services/llm_gateway.py CHANGED
@@ -29,9 +29,9 @@ def _get_llm_endpoint() -> tuple[str, bool]:
29
 
30
  return url, is_hf
31
 
32
- def send_to_llm(prompt: str) -> str:
33
  """
34
- Forwards prompt to local Ollama model or Hugging Face Inference API.
35
  Falls back to MockProvider if unreachable.
36
  """
37
  url, is_hf = _get_llm_endpoint()
@@ -43,20 +43,26 @@ def send_to_llm(prompt: str) -> str:
43
  headers["Authorization"] = f"Bearer {hf_token}"
44
  payload = {
45
  "model": rules_manager.ollama_model,
46
- "messages": [
47
- {"role": "user", "content": prompt}
48
- ],
49
  "max_tokens": 512
50
  }
51
  else:
52
- payload = {
53
- "model": rules_manager.ollama_model,
54
- "prompt": prompt,
55
- "stream": False
56
- }
57
- # Ollama endpoint: url/api/generate
58
- if not url.endswith("/api/generate") and not url.endswith("/api/generate/"):
59
- url = f"{url.rstrip('/')}/api/generate"
 
 
 
 
 
 
 
 
60
 
61
  try:
62
  response = httpx.post(url, json=payload, headers=headers, timeout=30.0)
@@ -70,7 +76,10 @@ def send_to_llm(prompt: str) -> str:
70
  elif isinstance(res_json, dict):
71
  return res_json.get("generated_text", "").strip()
72
  else:
73
- return response.json().get("response", "").strip()
 
 
 
74
  else:
75
  print(f"LLM API returned status code {response.status_code}: {response.text}")
76
 
@@ -88,7 +97,7 @@ def send_to_llm(prompt: str) -> str:
88
  return f"[Mock] This is a mock LLM response answering your prompt: '{prompt}'"
89
 
90
 
91
- def stream_from_llm(prompt: str) -> Generator[str, None, None]:
92
  """
93
  Streams response from local Ollama model or Hugging Face Inference API chunk by chunk.
94
  Falls back to MockProvider streaming if unreachable.
@@ -102,20 +111,27 @@ def stream_from_llm(prompt: str) -> Generator[str, None, None]:
102
  headers["Authorization"] = f"Bearer {hf_token}"
103
  payload = {
104
  "model": rules_manager.ollama_model,
105
- "messages": [
106
- {"role": "user", "content": prompt}
107
- ],
108
  "max_tokens": 512,
109
  "stream": True
110
  }
111
  else:
112
- payload = {
113
- "model": rules_manager.ollama_model,
114
- "prompt": prompt,
115
- "stream": True
116
- }
117
- if not url.endswith("/api/generate") and not url.endswith("/api/generate/"):
118
- url = f"{url.rstrip('/')}/api/generate"
 
 
 
 
 
 
 
 
 
119
 
120
  try:
121
  with httpx.Client() as client:
@@ -149,7 +165,10 @@ def stream_from_llm(prompt: str) -> Generator[str, None, None]:
149
  # Ollama format
150
  try:
151
  chunk_json = json.loads(line)
152
- chunk_text = chunk_json.get("response", "")
 
 
 
153
  if chunk_text:
154
  yield chunk_text
155
  except ValueError:
 
29
 
30
  return url, is_hf
31
 
32
+ def send_to_llm(prompt: str, messages: list[dict] = None) -> str:
33
  """
34
+ Forwards prompt or messages history to local Ollama model or Hugging Face Inference API.
35
  Falls back to MockProvider if unreachable.
36
  """
37
  url, is_hf = _get_llm_endpoint()
 
43
  headers["Authorization"] = f"Bearer {hf_token}"
44
  payload = {
45
  "model": rules_manager.ollama_model,
46
+ "messages": messages if messages else [{"role": "user", "content": prompt}],
 
 
47
  "max_tokens": 512
48
  }
49
  else:
50
+ if messages:
51
+ payload = {
52
+ "model": rules_manager.ollama_model,
53
+ "messages": messages,
54
+ "stream": False
55
+ }
56
+ if not url.endswith("/api/chat") and not url.endswith("/api/chat/"):
57
+ url = f"{url.rstrip('/')}/api/chat"
58
+ else:
59
+ payload = {
60
+ "model": rules_manager.ollama_model,
61
+ "prompt": prompt,
62
+ "stream": False
63
+ }
64
+ if not url.endswith("/api/generate") and not url.endswith("/api/generate/"):
65
+ url = f"{url.rstrip('/')}/api/generate"
66
 
67
  try:
68
  response = httpx.post(url, json=payload, headers=headers, timeout=30.0)
 
76
  elif isinstance(res_json, dict):
77
  return res_json.get("generated_text", "").strip()
78
  else:
79
+ res_json = response.json()
80
+ if "message" in res_json:
81
+ return res_json.get("message", {}).get("content", "").strip()
82
+ return res_json.get("response", "").strip()
83
  else:
84
  print(f"LLM API returned status code {response.status_code}: {response.text}")
85
 
 
97
  return f"[Mock] This is a mock LLM response answering your prompt: '{prompt}'"
98
 
99
 
100
+ def stream_from_llm(prompt: str, messages: list[dict] = None) -> Generator[str, None, None]:
101
  """
102
  Streams response from local Ollama model or Hugging Face Inference API chunk by chunk.
103
  Falls back to MockProvider streaming if unreachable.
 
111
  headers["Authorization"] = f"Bearer {hf_token}"
112
  payload = {
113
  "model": rules_manager.ollama_model,
114
+ "messages": messages if messages else [{"role": "user", "content": prompt}],
 
 
115
  "max_tokens": 512,
116
  "stream": True
117
  }
118
  else:
119
+ if messages:
120
+ payload = {
121
+ "model": rules_manager.ollama_model,
122
+ "messages": messages,
123
+ "stream": True
124
+ }
125
+ if not url.endswith("/api/chat") and not url.endswith("/api/chat/"):
126
+ url = f"{url.rstrip('/')}/api/chat"
127
+ else:
128
+ payload = {
129
+ "model": rules_manager.ollama_model,
130
+ "prompt": prompt,
131
+ "stream": True
132
+ }
133
+ if not url.endswith("/api/generate") and not url.endswith("/api/generate/"):
134
+ url = f"{url.rstrip('/')}/api/generate"
135
 
136
  try:
137
  with httpx.Client() as client:
 
165
  # Ollama format
166
  try:
167
  chunk_json = json.loads(line)
168
+ if "message" in chunk_json:
169
+ chunk_text = chunk_json.get("message", {}).get("content", "")
170
+ else:
171
+ chunk_text = chunk_json.get("response", "")
172
  if chunk_text:
173
  yield chunk_text
174
  except ValueError:
app/static/chat.html CHANGED
@@ -54,8 +54,7 @@
54
  <!-- Pill-shaped chat input bar -->
55
  <footer class="chat-footer-input">
56
  <div class="input-pill">
57
- <input type="text" id="userInput" placeholder="Message GuardrailShield..."
58
- onkeydown="if(event.key === 'Enter') sendMessage()">
59
  <button class="btn-send" id="sendBtn" onclick="sendMessage()">
60
  <svg viewBox="0 0 24 24" width="18" height="18" fill="currentColor">
61
  <path d="M2.01 21L23 12 2.01 3 2 10l15 2-15 2z" />
 
54
  <!-- Pill-shaped chat input bar -->
55
  <footer class="chat-footer-input">
56
  <div class="input-pill">
57
+ <textarea id="userInput" placeholder="Message GuardrailShield..." rows="1"></textarea>
 
58
  <button class="btn-send" id="sendBtn" onclick="sendMessage()">
59
  <svg viewBox="0 0 24 24" width="18" height="18" fill="currentColor">
60
  <path d="M2.01 21L23 12 2.01 3 2 10l15 2-15 2z" />
app/static/css/chat.css CHANGED
@@ -267,7 +267,7 @@ body {
267
  border: 1px solid var(--border-color);
268
  }
269
 
270
- .input-pill input {
271
  flex: 1;
272
  background: transparent;
273
  border: none;
@@ -275,6 +275,12 @@ body {
275
  font-size: 15px;
276
  outline: none;
277
  font-family: inherit;
 
 
 
 
 
 
278
  }
279
 
280
  .btn-send {
@@ -383,3 +389,54 @@ body {
383
  color: var(--accent-color);
384
  text-decoration: underline;
385
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
267
  border: 1px solid var(--border-color);
268
  }
269
 
270
+ .input-pill textarea {
271
  flex: 1;
272
  background: transparent;
273
  border: none;
 
275
  font-size: 15px;
276
  outline: none;
277
  font-family: inherit;
278
+ resize: none;
279
+ max-height: 200px;
280
+ overflow-y: auto;
281
+ padding: 6px 0;
282
+ line-height: 1.4;
283
+ height: 24px;
284
  }
285
 
286
  .btn-send {
 
389
  color: var(--accent-color);
390
  text-decoration: underline;
391
  }
392
+
393
+ @media (max-width: 768px) {
394
+ .app-wrapper {
395
+ flex-direction: column;
396
+ }
397
+ .sidebar {
398
+ width: 100%;
399
+ height: auto;
400
+ border-right: none;
401
+ border-bottom: 1px solid var(--border-color);
402
+ padding: 10px;
403
+ }
404
+ .sidebar-menu {
405
+ display: none; /* Collapses session list on mobile */
406
+ }
407
+ .btn-new-chat {
408
+ display: none;
409
+ }
410
+ .sidebar-footer {
411
+ border-top: none;
412
+ padding-top: 0;
413
+ display: flex;
414
+ justify-content: center;
415
+ width: 100%;
416
+ }
417
+ .btn-admin-link {
418
+ padding: 6px 12px;
419
+ width: 100%;
420
+ font-size: 12px;
421
+ }
422
+ .chat-main {
423
+ height: calc(100vh - 60px);
424
+ }
425
+ .chat-header-bar {
426
+ height: 40px;
427
+ padding: 0 15px;
428
+ }
429
+ .chat-viewport {
430
+ padding: 10px 12px;
431
+ }
432
+ .bubble {
433
+ font-size: 14px !important;
434
+ padding: 10px 14px !important;
435
+ }
436
+ .chat-footer-input {
437
+ padding: 10px 12px 16px 12px;
438
+ }
439
+ .input-pill {
440
+ padding: 6px 10px 6px 16px;
441
+ }
442
+ }
app/static/css/styles.css CHANGED
@@ -334,3 +334,44 @@ input[type="text"]:focus, textarea:focus {
334
  from { opacity: 0; transform: translateY(-4px); }
335
  to { opacity: 1; transform: translateY(0); }
336
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
334
  from { opacity: 0; transform: translateY(-4px); }
335
  to { opacity: 1; transform: translateY(0); }
336
  }
337
+
338
+ @media (max-width: 768px) {
339
+ .app-container {
340
+ flex-direction: column;
341
+ }
342
+ .sidebar {
343
+ width: 100%;
344
+ border-right: none;
345
+ border-bottom: 1px solid var(--border-color);
346
+ padding: 12px;
347
+ }
348
+ .logo {
349
+ margin-bottom: 15px;
350
+ justify-content: center;
351
+ }
352
+ .nav-menu {
353
+ flex-direction: row;
354
+ justify-content: space-around;
355
+ flex-wrap: wrap;
356
+ gap: 8px;
357
+ margin-bottom: 10px;
358
+ }
359
+ .nav-menu a {
360
+ padding: 8px 12px;
361
+ font-size: 13px;
362
+ }
363
+ .main-content {
364
+ padding: 20px 15px;
365
+ }
366
+ .charts-section {
367
+ grid-template-columns: 1fr;
368
+ }
369
+ .header-flex {
370
+ flex-direction: column;
371
+ align-items: flex-start;
372
+ gap: 12px;
373
+ }
374
+ .card-val {
375
+ font-size: 22px;
376
+ }
377
+ }
app/static/js/chat.js CHANGED
@@ -1,9 +1,24 @@
1
  let sessions = {};
2
  let activeSessionId = null;
 
3
 
4
  // Initialize on DOM load
5
  document.addEventListener('DOMContentLoaded', () => {
6
  loadSessions();
 
 
 
 
 
 
 
 
 
 
 
 
 
 
7
  });
8
 
9
  function loadSessions() {
@@ -145,10 +160,22 @@ async function sendMessage() {
145
  if (!text) return;
146
 
147
  input.value = '';
 
148
 
149
  const currentSession = sessions[activeSessionId];
150
  if (!currentSession) return;
151
 
 
 
 
 
 
 
 
 
 
 
 
152
  // Hide welcome block and show thread
153
  document.getElementById('welcomeContainer').style.display = 'none';
154
  const thread = document.getElementById('chatThread');
@@ -181,11 +208,24 @@ async function sendMessage() {
181
  // Append loading placeholder
182
  const loaderId = appendMessageToUI('Thinking...', 'assistant loading', 'loader_' + Date.now());
183
 
 
 
 
 
 
 
 
 
184
  try {
185
  const response = await fetch('/v1/shield/generate', {
186
  method: 'POST',
187
  headers: { 'Content-Type': 'application/json' },
188
- body: JSON.stringify({ prompt: text, stream: true })
 
 
 
 
 
189
  });
190
 
191
  if (!response.ok) {
@@ -283,14 +323,49 @@ async function sendMessage() {
283
  const loaderEl = document.getElementById(loaderId);
284
  if (loaderEl) loaderEl.remove();
285
 
286
- const errorText = '❌ Error: Failed to communicate with the guardrail gateway.';
287
- currentSession.messages.push({
288
- id: 'err_' + Date.now(),
289
- role: 'blocked',
290
- content: errorText
291
- });
292
- appendMessageToUI(errorText, 'blocked', 'err_' + Date.now());
293
- saveSessions();
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
294
  }
295
  }
296
 
 
1
  let sessions = {};
2
  let activeSessionId = null;
3
+ let activeAbortController = null;
4
 
5
  // Initialize on DOM load
6
  document.addEventListener('DOMContentLoaded', () => {
7
  loadSessions();
8
+
9
+ const textarea = document.getElementById('userInput');
10
+ if (textarea) {
11
+ textarea.addEventListener('input', function() {
12
+ this.style.height = 'auto';
13
+ this.style.height = (this.scrollHeight) + 'px';
14
+ });
15
+ textarea.addEventListener('keydown', function(e) {
16
+ if (e.key === 'Enter' && !e.shiftKey) {
17
+ e.preventDefault();
18
+ sendMessage();
19
+ }
20
+ });
21
+ }
22
  });
23
 
24
  function loadSessions() {
 
160
  if (!text) return;
161
 
162
  input.value = '';
163
+ input.style.height = 'auto';
164
 
165
  const currentSession = sessions[activeSessionId];
166
  if (!currentSession) return;
167
 
168
+ // Toggle Send button to Stop button
169
+ const sendBtn = document.getElementById('sendBtn');
170
+ if (sendBtn) {
171
+ sendBtn.innerHTML = '<svg viewBox="0 0 24 24" width="18" height="18" fill="currentColor"><rect x="4" y="4" width="16" height="16" rx="2" /></svg>';
172
+ sendBtn.setAttribute('onclick', 'stopGeneration()');
173
+ sendBtn.title = 'Stop Generation';
174
+ }
175
+
176
+ activeAbortController = new AbortController();
177
+ const { signal } = activeAbortController;
178
+
179
  // Hide welcome block and show thread
180
  document.getElementById('welcomeContainer').style.display = 'none';
181
  const thread = document.getElementById('chatThread');
 
208
  // Append loading placeholder
209
  const loaderId = appendMessageToUI('Thinking...', 'assistant loading', 'loader_' + Date.now());
210
 
211
+ // Build message history for the AI memory
212
+ const messageHistory = currentSession.messages
213
+ .filter(msg => msg.role === 'user' || msg.role === 'assistant')
214
+ .map(msg => ({
215
+ role: msg.role,
216
+ content: msg.content
217
+ }));
218
+
219
  try {
220
  const response = await fetch('/v1/shield/generate', {
221
  method: 'POST',
222
  headers: { 'Content-Type': 'application/json' },
223
+ body: JSON.stringify({
224
+ prompt: text,
225
+ messages: messageHistory,
226
+ stream: true
227
+ }),
228
+ signal: signal
229
  });
230
 
231
  if (!response.ok) {
 
323
  const loaderEl = document.getElementById(loaderId);
324
  if (loaderEl) loaderEl.remove();
325
 
326
+ if (e.name === 'AbortError') {
327
+ if (accumulatedText) {
328
+ // Remove loading styling from bubble
329
+ const msgElement = document.getElementById(assistantMsgId);
330
+ if (msgElement) {
331
+ msgElement.classList.remove('loading');
332
+ }
333
+ } else {
334
+ const stopText = 'Generation stopped.';
335
+ currentSession.messages.push({
336
+ id: 'stop_' + Date.now(),
337
+ role: 'assistant',
338
+ content: stopText
339
+ });
340
+ appendMessageToUI(stopText, 'assistant', 'stop_' + Date.now());
341
+ }
342
+ saveSessions();
343
+ } else {
344
+ const errorText = '❌ Error: Failed to communicate with the guardrail gateway.';
345
+ currentSession.messages.push({
346
+ id: 'err_' + Date.now(),
347
+ role: 'blocked',
348
+ content: errorText
349
+ });
350
+ appendMessageToUI(errorText, 'blocked', 'err_' + Date.now());
351
+ saveSessions();
352
+ }
353
+ } finally {
354
+ // Restore Send button UI
355
+ const sendBtn = document.getElementById('sendBtn');
356
+ if (sendBtn) {
357
+ sendBtn.innerHTML = '<svg viewBox="0 0 24 24" width="18" height="18" fill="currentColor"><path d="M2.01 21L23 12 2.01 3 2 10l15 2-15 2z" /></svg>';
358
+ sendBtn.setAttribute('onclick', 'sendMessage()');
359
+ sendBtn.title = 'Send Message';
360
+ }
361
+ activeAbortController = null;
362
+ }
363
+ }
364
+
365
+ function stopGeneration() {
366
+ if (activeAbortController) {
367
+ activeAbortController.abort();
368
+ activeAbortController = null;
369
  }
370
  }
371
 
app/static/js/dashboard.js CHANGED
@@ -1,6 +1,21 @@
1
  let categoryChart = null;
2
  let riskTrendChart = null;
3
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
4
  function switchTab(tabName) {
5
  document.querySelectorAll('.tab-view').forEach(view => view.classList.remove('active'));
6
  document.querySelectorAll('.nav-menu a').forEach(a => a.classList.remove('active'));
@@ -17,12 +32,12 @@ function switchTab(tabName) {
17
  }
18
 
19
  async function fetchStats() {
20
- const response = await fetch('/v1/shield/stats');
21
  return await response.json();
22
  }
23
 
24
  async function fetchLogs() {
25
- const response = await fetch('/v1/shield/logs?limit=15');
26
  return await response.json();
27
  }
28
 
@@ -151,7 +166,7 @@ function updateLogsTable(logs) {
151
 
152
  async function loadSettingsFromAPI() {
153
  try {
154
- const response = await fetch('/v1/shield/config');
155
  const config = await response.json();
156
 
157
  document.getElementById('ollamaUrl').value = config.ollama_url;
@@ -262,7 +277,7 @@ async function saveSettings() {
262
  };
263
 
264
  try {
265
- const response = await fetch('/v1/shield/config', {
266
  method: 'POST',
267
  headers: { 'Content-Type': 'application/json' },
268
  body: JSON.stringify(payload)
@@ -286,7 +301,7 @@ async function testAlert(channel) {
286
  resultDiv.textContent = `⏳ Sending test alert to ${channel}...`;
287
 
288
  try {
289
- const response = await fetch(`/v1/shield/test-alert?channel=${channel}`, {
290
  method: 'POST',
291
  headers: { 'Content-Type': 'application/json' }
292
  });
@@ -354,7 +369,7 @@ async function testSemanticSimilarity() {
354
  resultDiv.textContent = `⏳ Running vector search analysis...`;
355
 
356
  try {
357
- const response = await fetch('/v1/shield/test-semantic', {
358
  method: 'POST',
359
  headers: { 'Content-Type': 'application/json' },
360
  body: JSON.stringify({ prompt: prompt })
@@ -407,15 +422,31 @@ async function refreshDashboard() {
407
 
408
  async function performLogout() {
409
  try {
410
- await fetch('/v1/auth/logout', { method: 'POST' });
 
411
  window.location.href = '/login';
412
  } catch (e) {
413
  console.error("Logout failed: ", e);
 
 
414
  }
415
  }
416
 
417
  // Initial load
418
- document.addEventListener('DOMContentLoaded', () => {
 
 
 
 
 
 
 
 
 
 
 
 
 
419
  refreshDashboard();
420
  // Poll updates every 15 seconds
421
  setInterval(() => {
 
1
  let categoryChart = null;
2
  let riskTrendChart = null;
3
 
4
+ async function apiFetch(url, options = {}) {
5
+ const token = localStorage.getItem("session_token");
6
+ if (token) {
7
+ options.headers = options.headers || {};
8
+ options.headers["Authorization"] = `Bearer ${token}`;
9
+ }
10
+ const response = await fetch(url, options);
11
+ if (response.status === 401) {
12
+ localStorage.removeItem("session_token");
13
+ window.location.href = '/login';
14
+ throw new Error("Unauthorized");
15
+ }
16
+ return response;
17
+ }
18
+
19
  function switchTab(tabName) {
20
  document.querySelectorAll('.tab-view').forEach(view => view.classList.remove('active'));
21
  document.querySelectorAll('.nav-menu a').forEach(a => a.classList.remove('active'));
 
32
  }
33
 
34
  async function fetchStats() {
35
+ const response = await apiFetch('/v1/shield/stats');
36
  return await response.json();
37
  }
38
 
39
  async function fetchLogs() {
40
+ const response = await apiFetch('/v1/shield/logs?limit=15');
41
  return await response.json();
42
  }
43
 
 
166
 
167
  async function loadSettingsFromAPI() {
168
  try {
169
+ const response = await apiFetch('/v1/shield/config');
170
  const config = await response.json();
171
 
172
  document.getElementById('ollamaUrl').value = config.ollama_url;
 
277
  };
278
 
279
  try {
280
+ const response = await apiFetch('/v1/shield/config', {
281
  method: 'POST',
282
  headers: { 'Content-Type': 'application/json' },
283
  body: JSON.stringify(payload)
 
301
  resultDiv.textContent = `⏳ Sending test alert to ${channel}...`;
302
 
303
  try {
304
+ const response = await apiFetch(`/v1/shield/test-alert?channel=${channel}`, {
305
  method: 'POST',
306
  headers: { 'Content-Type': 'application/json' }
307
  });
 
369
  resultDiv.textContent = `⏳ Running vector search analysis...`;
370
 
371
  try {
372
+ const response = await apiFetch('/v1/shield/test-semantic', {
373
  method: 'POST',
374
  headers: { 'Content-Type': 'application/json' },
375
  body: JSON.stringify({ prompt: prompt })
 
422
 
423
  async function performLogout() {
424
  try {
425
+ await apiFetch('/v1/auth/logout', { method: 'POST' });
426
+ localStorage.removeItem("session_token");
427
  window.location.href = '/login';
428
  } catch (e) {
429
  console.error("Logout failed: ", e);
430
+ localStorage.removeItem("session_token");
431
+ window.location.href = '/login';
432
  }
433
  }
434
 
435
  // Initial load
436
+ document.addEventListener('DOMContentLoaded', async () => {
437
+ // Check session authentication before loading dashboard
438
+ try {
439
+ const resp = await apiFetch('/v1/auth/session');
440
+ const data = await resp.json();
441
+ if (!data.authenticated) {
442
+ window.location.href = '/login';
443
+ return;
444
+ }
445
+ } catch (e) {
446
+ window.location.href = '/login';
447
+ return;
448
+ }
449
+
450
  refreshDashboard();
451
  // Poll updates every 15 seconds
452
  setInterval(() => {
app/static/js/login.js CHANGED
@@ -22,6 +22,10 @@ async function performLogin() {
22
  });
23
 
24
  if (response.ok) {
 
 
 
 
25
  // Redirect to dashboard page
26
  window.location.href = '/dashboard';
27
  } else {
 
22
  });
23
 
24
  if (response.ok) {
25
+ const data = await response.json();
26
+ if (data.token) {
27
+ localStorage.setItem("session_token", data.token);
28
+ }
29
  // Redirect to dashboard page
30
  window.location.href = '/dashboard';
31
  } else {
tests/test_main.py CHANGED
@@ -572,3 +572,24 @@ def test_llm_gateway_hf_routing():
572
  del os.environ["HF_TOKEN"]
573
 
574
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
572
  del os.environ["HF_TOKEN"]
573
 
574
 
575
+ def test_input_guard_false_positives_bypass():
576
+ """Verify that false positives like math, gibberish, and custom introductions bypass DeBERTa classifier."""
577
+ from app.services.input_guard import validate_input
578
+
579
+ # 1. Hello I am Yash, remember it
580
+ res1 = validate_input("Hello I am Yash, remember it")
581
+ assert res1["safe"] is True
582
+ assert "bypassed false-positive" in res1["matched_rule"]
583
+
584
+ # 2. Mathematical expression
585
+ res2 = validate_input("23774589 + 87885783884")
586
+ assert res2["safe"] is True
587
+ assert "bypassed false-positive" in res2["matched_rule"]
588
+
589
+ # 3. Repetitive gibberish
590
+ res3 = validate_input("ooooodooddoodooddooddooddoodododdoodododododdododododododododoodddddoddddoddodo")
591
+ assert res3["safe"] is True
592
+ assert "bypassed false-positive" in res3["matched_rule"]
593
+
594
+
595
+