nikeshn commited on
Commit
20217cc
Β·
verified Β·
1 Parent(s): 9f7cb6d

Upload 2 files

Browse files
Files changed (2) hide show
  1. src/api/admin.py +38 -4
  2. src/api/agent.py +57 -3
src/api/admin.py CHANGED
@@ -40,6 +40,15 @@ _CONFIG_DEFAULTS: Dict[str, Any] = {
40
 
41
  admin_sessions: Dict[str, float] = {}
42
 
 
 
 
 
 
 
 
 
 
43
 
44
  def _sessions_file() -> str:
45
  cfg_path = os.environ.get("CONFIG_STORE_PATH", "")
@@ -138,16 +147,41 @@ class ConfigBulkUpdate(BaseModel):
138
  # ── Auth Endpoints ─────────────────────────────────────────────────────────────
139
 
140
  @router.post("/auth")
141
- async def admin_auth(req: LoginRequest):
142
  settings = get_settings()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
143
  if not settings.admin_password or req.password != settings.admin_password:
144
- raise HTTPException(status_code=401, detail="Invalid password")
 
 
 
 
 
 
 
 
 
 
145
  token = secrets.token_hex(32)
146
  admin_sessions[token] = time.time() + 86400
147
  _save_sessions()
148
  resp = Response(content='{"status":"ok"}', media_type="application/json")
149
  resp.set_cookie(key="admin_session", value=token, httponly=True,
150
- max_age=86400, samesite="lax")
151
  return resp
152
 
153
 
@@ -208,7 +242,7 @@ async function doLogin(e){
208
  const pw=document.getElementById('pw').value;
209
  const r=await fetch('/admin/auth',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({password:pw})});
210
  if(r.ok){window.location.href='/admin';}
211
- else{document.getElementById('err').style.display='block';document.getElementById('pw').value='';document.getElementById('pw').focus();}
212
  return false;
213
  }
214
  </script></body></html>""")
 
40
 
41
  admin_sessions: Dict[str, float] = {}
42
 
43
+ # ── Admin login lockout ────────────────────────────────────────────────────────
44
+ # Tracks failed login attempts per IP.
45
+ # After _MAX_LOGIN_ATTEMPTS failures within _LOCKOUT_WINDOW seconds,
46
+ # that IP is locked out for _LOCKOUT_DURATION seconds.
47
+ _login_attempts: Dict[str, list] = {} # ip β†’ [timestamp, ...]
48
+ _MAX_LOGIN_ATTEMPTS = 5
49
+ _LOCKOUT_WINDOW = 300 # 5 minutes β€” window in which attempts are counted
50
+ _LOCKOUT_DURATION = 900 # 15 minutes β€” lockout duration after max attempts
51
+
52
 
53
  def _sessions_file() -> str:
54
  cfg_path = os.environ.get("CONFIG_STORE_PATH", "")
 
147
  # ── Auth Endpoints ─────────────────────────────────────────────────────────────
148
 
149
  @router.post("/auth")
150
+ async def admin_auth(req: LoginRequest, request: Request):
151
  settings = get_settings()
152
+ client_ip = request.client.host if request.client else "unknown"
153
+ now = time.time()
154
+
155
+ # ── Lockout check ─────────────────────────────────────────────────────────
156
+ attempts = _login_attempts.get(client_ip, [])
157
+ # Purge attempts outside the lockout window
158
+ attempts = [t for t in attempts if now - t < _LOCKOUT_WINDOW]
159
+ if len(attempts) >= _MAX_LOGIN_ATTEMPTS:
160
+ wait = int(_LOCKOUT_DURATION - (now - attempts[0]))
161
+ raise HTTPException(
162
+ status_code=429,
163
+ detail=f"Too many failed login attempts. Try again in {wait} seconds."
164
+ )
165
+
166
+ # ── Password check ────────────────────────────────────────────────────────
167
  if not settings.admin_password or req.password != settings.admin_password:
168
+ attempts.append(now)
169
+ _login_attempts[client_ip] = attempts
170
+ remaining = _MAX_LOGIN_ATTEMPTS - len(attempts)
171
+ if remaining <= 0:
172
+ detail = f"Too many failed login attempts. Try again in {int(_LOCKOUT_DURATION / 60)} minutes."
173
+ else:
174
+ detail = f"Invalid password. {remaining} attempt{'s' if remaining != 1 else ''} remaining before lockout."
175
+ raise HTTPException(status_code=401, detail=detail)
176
+
177
+ # ── Success β€” clear attempts and issue session ─────────────────────────────
178
+ _login_attempts.pop(client_ip, None)
179
  token = secrets.token_hex(32)
180
  admin_sessions[token] = time.time() + 86400
181
  _save_sessions()
182
  resp = Response(content='{"status":"ok"}', media_type="application/json")
183
  resp.set_cookie(key="admin_session", value=token, httponly=True,
184
+ max_age=86400, samesite="lax", secure=True)
185
  return resp
186
 
187
 
 
242
  const pw=document.getElementById('pw').value;
243
  const r=await fetch('/admin/auth',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({password:pw})});
244
  if(r.ok){window.location.href='/admin';}
245
+ else{const data=await r.json().catch(()=>({}));const msg=data.detail||'Incorrect password. Try again.';const e=document.getElementById('err');e.textContent=msg;e.style.display='block';document.getElementById('pw').value='';document.getElementById('pw').focus();}
246
  return false;
247
  }
248
  </script></body></html>""")
src/api/agent.py CHANGED
@@ -223,6 +223,39 @@ CURRENT_INFO_RE = re.compile(
223
  r"|news|update|updated|prime minister|ceo|minister|king|ruler|who won)\b",
224
  re.IGNORECASE,
225
  )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
226
  SUMMARY_RE = re.compile(
227
  r"\b(summar(y|ize|ise|izing|ising)|overview|explain|what is|what are|describe"
228
  r"|tell me about|give me a summary|give me an overview|give me a brief"
@@ -1112,7 +1145,7 @@ async def _libbee_casual_response(question: str, history: List[ChatMessage], mod
1112
  msgs = [{"role": "system", "content": (
1113
  "You are LibBee, the Khalifa University Library AI Assistant. "
1114
  "Respond warmly and naturally in 1-3 sentences as a friendly librarian. "
1115
- "No markdown, no bullet points."
1116
  )}]
1117
  _cfg = _get_runtime_config()
1118
  _ci = _cfg.get("custom_instructions", "").strip()
@@ -1137,7 +1170,8 @@ async def _rag_answer(question: str, rag_results: List[dict], history: List[Chat
1137
  "Be concise, friendly, and use HTML <br> for line breaks where needed. "
1138
  f"If the context does not contain enough information, say so and suggest Ask a Librarian at {ASK_LIBRARIAN_URL}. "
1139
  "Never invent facts or URLs.\n\n"
1140
- f"CONTEXT:\n{context}"
 
1141
  )
1142
  _cfg = _get_runtime_config()
1143
  _ci = _cfg.get("custom_instructions", "").strip()
@@ -1168,7 +1202,8 @@ async def _llm_general_answer(question: str, history: List[ChatMessage], model:
1168
  system = (
1169
  "You are LibBee, the Khalifa University Library AI Assistant (Abu Dhabi, UAE). "
1170
  "Answer factually and concisely in 3-5 sentences. Use HTML <br> for line breaks. "
1171
- "KU means Khalifa University. If the topic is academic, briefly offer to help find library resources."
 
1172
  )
1173
  _cfg = _get_runtime_config()
1174
  _ci = _cfg.get("custom_instructions", "").strip()
@@ -3765,6 +3800,25 @@ async def _agent_query_inner(
3765
  start_time: float,
3766
  ) -> AgentResponse:
3767
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
3768
  if not question:
3769
  follow_up_question, follow_up_suggestions = _social_follow_up()
3770
  return AgentResponse(
 
223
  r"|news|update|updated|prime minister|ceo|minister|king|ruler|who won)\b",
224
  re.IGNORECASE,
225
  )
226
+
227
+ # ── Academic guardrail β€” appended to all LLM system prompts ──────────────────
228
+ _GUARDRAIL = (
229
+ "IMPORTANT β€” GUARDRAILS: "
230
+ "You must never make negative, critical, or controversial statements about "
231
+ "Khalifa University, its leadership, staff, or policies; the UAE government, "
232
+ "its leaders, laws, or institutions; or any religion. "
233
+ "If asked for opinions on these topics, politely decline and redirect to library services. "
234
+ "Never generate content that is politically sensitive, religiously offensive, "
235
+ "or inappropriate for a university academic environment. "
236
+ "You may answer general factual questions β€” such as historical facts, science, "
237
+ "geography, or current affairs β€” but always from a neutral, academic perspective. "
238
+ "Never produce content that could embarrass Khalifa University or its library."
239
+ )
240
+
241
+ # ── Prompt injection detection ────────────────────────────────────────────────
242
+ _INJECTION_RE = re.compile(
243
+ r"\b(ignore\s+(previous|above|all|prior)\s+instructions?"
244
+ r"|disregard\s+(all|previous|above|prior)\s+instructions?"
245
+ r"|override\s+(system|instructions?|prompt)"
246
+ r"|new\s+instructions?\s*(are|follow|:|;)"
247
+ r"|you\s+are\s+now\s+(a|an|the|going)"
248
+ r"|act\s+as\s+(a|an|if|though|when)\b"
249
+ r"|act\s+like\s+(a|an)\s+\w"
250
+ r"|pretend\s+(you\s+are|to\s+be)"
251
+ r"|jailbreak|dan\s+mode|developer\s+mode|god\s+mode"
252
+ r"|system\s+prompt\s*(is|was|says|leak|show|reveal|print|output)"
253
+ r"|reveal\s+(your\s+)?(system\s+)?prompt"
254
+ r"|print\s+(your\s+)?(system\s+)?instructions?"
255
+ r"|forget\s+(you\s+are|your\s+role|everything)"
256
+ r"|do\s+anything\s+now|no\s+restrictions?)\b",
257
+ re.IGNORECASE,
258
+ )
259
  SUMMARY_RE = re.compile(
260
  r"\b(summar(y|ize|ise|izing|ising)|overview|explain|what is|what are|describe"
261
  r"|tell me about|give me a summary|give me an overview|give me a brief"
 
1145
  msgs = [{"role": "system", "content": (
1146
  "You are LibBee, the Khalifa University Library AI Assistant. "
1147
  "Respond warmly and naturally in 1-3 sentences as a friendly librarian. "
1148
+ "No markdown, no bullet points. " + _GUARDRAIL
1149
  )}]
1150
  _cfg = _get_runtime_config()
1151
  _ci = _cfg.get("custom_instructions", "").strip()
 
1170
  "Be concise, friendly, and use HTML <br> for line breaks where needed. "
1171
  f"If the context does not contain enough information, say so and suggest Ask a Librarian at {ASK_LIBRARIAN_URL}. "
1172
  "Never invent facts or URLs.\n\n"
1173
+ f"CONTEXT:\n{context}\n\n"
1174
+ + _GUARDRAIL
1175
  )
1176
  _cfg = _get_runtime_config()
1177
  _ci = _cfg.get("custom_instructions", "").strip()
 
1202
  system = (
1203
  "You are LibBee, the Khalifa University Library AI Assistant (Abu Dhabi, UAE). "
1204
  "Answer factually and concisely in 3-5 sentences. Use HTML <br> for line breaks. "
1205
+ "KU means Khalifa University. If the topic is academic, briefly offer to help find library resources. "
1206
+ + _GUARDRAIL
1207
  )
1208
  _cfg = _get_runtime_config()
1209
  _ci = _cfg.get("custom_instructions", "").strip()
 
3800
  start_time: float,
3801
  ) -> AgentResponse:
3802
 
3803
+ # ── Prompt injection detection ────────────────────────────────────────────
3804
+ # Fires before any processing. Catches attempts to override system prompt,
3805
+ # jailbreak, or extract internal instructions.
3806
+ if question and _INJECTION_RE.search(question):
3807
+ _safe_metrics_bucket("intents", "injection_blocked")
3808
+ return AgentResponse(
3809
+ answer=(
3810
+ "I'm sorry, but I can't process that request. "
3811
+ "I'm LibBee, the KU Library AI Assistant β€” here to help with "
3812
+ "library services, research, and academic resources. "
3813
+ f'How can I help you today? <a href="{ASK_LIBRARIAN_URL}" target="_blank">Ask a Librarian</a>'
3814
+ ),
3815
+ intent="blocked",
3816
+ tools_used=["injection_guard"],
3817
+ model_used=model,
3818
+ response_time=time.time() - start_time,
3819
+ )
3820
+ # ───────────────────────────────��─────────────────────────────────────────
3821
+
3822
  if not question:
3823
  follow_up_question, follow_up_suggestions = _social_follow_up()
3824
  return AgentResponse(