From f8ceb1f0577c30509238ee13fdcd94079518fcc3 Mon Sep 17 00:00:00 2001 From: Tudor Cinteza Date: Fri, 12 Dec 2025 15:23:15 +0200 Subject: [PATCH 1/7] Cleaned version: contains python server that acts as backend for guard (semantic authentication already implemented); guard_openai.yaml file used as action in guard; README.md file contains a short documentation about starting server and initiating tunnel connection; .gitignore prevents commiting large files from virtual environment or __pycache__; requirments.txt lists imports necessary for python server, useful when creating the virtual environment. --- AI-agents-delegate-actions/README.md | 54 ------ README.md | 5 - llm-git-conflict-resolve/README.md | 14 -- llm-password-reset/.gitignore | 6 + llm-password-reset/README.md | 139 ++++++++++++++- llm-password-reset/guard_openai.yaml | 116 +++++++++++++ llm-password-reset/guard_server.py | 243 +++++++++++++++++++++++++++ llm-password-reset/requirements.txt | 8 + n8n/README.md | 3 - 9 files changed, 511 insertions(+), 77 deletions(-) delete mode 100644 AI-agents-delegate-actions/README.md delete mode 100644 llm-git-conflict-resolve/README.md create mode 100644 llm-password-reset/.gitignore create mode 100644 llm-password-reset/guard_openai.yaml create mode 100644 llm-password-reset/guard_server.py create mode 100644 llm-password-reset/requirements.txt delete mode 100644 n8n/README.md diff --git a/AI-agents-delegate-actions/README.md b/AI-agents-delegate-actions/README.md deleted file mode 100644 index b444e1e1..00000000 --- a/AI-agents-delegate-actions/README.md +++ /dev/null @@ -1,54 +0,0 @@ -# MCP proxy or delegate to sub-agents to reduce the main context of AI agents - -A big problem with using MCP servers in AI agents, especially, is that the context window increases like crazy, even when you want to make a simple tool call; all tools are loaded. - -## Ideas - -what I think can be done nicely is a kind of RAG for MCP tools :) -- you have an agent that responds with what tools from MCP make sense for your prompt -- in the main context only those tools are added, not all from the MCP server -- the main agent then calls only those tools -- this reduces what is loaded in the context but still brings definition to those tools - -Or so that you don't bring those either or you can make a kind of MPC proxy -- you have a sub-agent that responds with what tool names from MCP make sense for your prompt -- in the main context only those tool names are added, without definition -- the main agent then invokes and that sub-agent calls tools by name for what data it wants -- and the data mapping to the MCP payload and the actual call to the tools is done in the sub-agent context which is temp -- in the main context only pure result gets back -- the advantage: bring only tools names + output values ​​from the MCP server to the main context - -Those sub-agents can use skills that exist only in their context when invoked and do not enter the main context. - -## Possible solution - -One solution could be to delegate some actions to sub-agents for execution outside the main context window, or to have an MCP server handle them and use another LLM or agent with a different context window internally. - -1. Tool discovery sub-agent returns: - - Tool names + minimal descriptions (5-10 tokens each) - - NOT full schemas -2. Main context gets lightweight tool list -3. When main agent wants to call a tool: - - Passes intent to sub-agent - - Sub-agent sees full schemas, executes MCP call - - Returns just the result - -## Existing solutions - -Claude created something similar https://www.anthropic.com/engineering/advanced-tool-use with these - -> Today, we're releasing three features that make this possible: -> - Tool Search Tool, which allows Claude to use search tools to access thousands of tools without consuming its context window -> - Programmatic Tool Calling, which allows Claude to invoke tools in a code execution environment reducing the impact on the model’s context window -> - Tool Use Examples, which provides a universal standard for demonstrating how to effectively use a given tool - -## Still existing motivation - -Of course, they have access to the agent code and are able to change also specs for MCP, implemented this very nicely. -But we could create a solution that is agent-agnostic, which can then be used with any agents that support concepts like sub-agents, MCP, and others. - -## Docs - -- https://www.claude.com/blog/skills -- https://code.claude.com/docs/en/sub-agents -- https://code.claude.com/docs/en/mcp diff --git a/README.md b/README.md index 2d35553f..a39ebe87 100644 --- a/README.md +++ b/README.md @@ -2,11 +2,6 @@ A sandbox for ideas: imagine freely, experiment boldly, learn from every misstep, and simply play. -> [!WARNING] -> This is just experimental at this point; it is still under development. Please do not use it with sensitive data for now; please wait for a -stable release. -> It's mostly ideal for experimental and learning projects.** - ## Contribute Feel free to fork, change, and use it however you want. We always appreciate it if you build something interesting and feel like sharing pull requests. diff --git a/llm-git-conflict-resolve/README.md b/llm-git-conflict-resolve/README.md deleted file mode 100644 index 35d67971..00000000 --- a/llm-git-conflict-resolve/README.md +++ /dev/null @@ -1,14 +0,0 @@ -# LLM Git conflict resolve - -A growing issue with these AI coding agents is that we end up with more code we don't fully understand, and when we work in a team, it's inevitable to make changes to the same file and run into git merge conflicts. -And given that the conflicted code was most likely not written by us, resolving such merge conflicts could be complicated and unpleasant work for a dev. - -So we are experimenting with how AI agents could help mitigate this. After all, they will be the ones writing that code :) - -One idea is to use Claude Code skills to instruct the agent on how it could help, like -- instructions on steps that would help, something similar to https://subagents.cc/ -- analyze previous commits to see how the code evolved -- include in the skill any code that might help to be executed while investigating -- ... open to suggestions - -Additionally, maybe it would help to save the AI coding agent's context window when it generates code for a feat to the PR or commits, so that when it analyzes commits to resolve the conflict, it can have the reasoning for why the code was generated that way. This info could also help with bug investigation. diff --git a/llm-password-reset/.gitignore b/llm-password-reset/.gitignore new file mode 100644 index 00000000..9f8a3c08 --- /dev/null +++ b/llm-password-reset/.gitignore @@ -0,0 +1,6 @@ +__pycache__/ +*.py[cod] +*.pyo +.venv/ +venv/ +guard_secure.db diff --git a/llm-password-reset/README.md b/llm-password-reset/README.md index 97b172b3..7a1c4458 100644 --- a/llm-password-reset/README.md +++ b/llm-password-reset/README.md @@ -1,4 +1,4 @@ -# Semantic Zero-Knowledge-Like Proof +# **Semantic Zero-Knowledge-Like Proof** Proposing a move from Syntactic Authentication (exact string matching) to Semantic Authentication (meaning-based matching). This approach allows for "fuzzy" logic, where the user proves they possess specific memories or knowledge without having to act like a robot memorizing an exact, case-sensitive phrase. @@ -13,3 +13,140 @@ We can use https://platform.claude.com/docs/en/agent-sdk/overview See more details https://gemini.google.com/share/f9a6075f22d1 [See Issue #4](https://github.com/xoriors/experimental/issues/4) + +## **How to Run GUARD Locally** + +### **1. Prerequisites** + +Make sure you work inside an environment that has these installed: + +``` bash +python3 -m venv .venv +source .venv/bin/activate +pip install fastapi uvicorn bcrypt openai +``` + +Also, verify to have `ngrok-v3` installed. If not, install by running following commands inside Linux terminal: + +``` bash +cd ~ +wget https://bin.equinox.io/c/bNyj1mQVY4c/ngrok-v3-stable-linux-amd64.tgz +tar -xvzf ngrok-v3-stable-linux-amd64.tgz +sudo mv ngrok /usr/local/bin/ +``` + +Add the **authentication token** for `ngrok` by running: + +``` bash +ngrok config add-authtoken $(AUTH_TOKEN) +``` + +--- + +### **2. Run the local FastAPI server** + +In your terminal: + +``` bash +uvicorn guard_server:app --reload --port 8000 +``` + +This command starts FastAPI locally at: `http://127.0.0.1:8000`. Here, you will see live logs of requests and responses. + +--- + +### **3. Expose it to the Internet (with `ngrok`)** + +Open another *terminal window* and run: + +```bash +ngrok http 8000 +``` + +If everything was correct so far, `ngrok` should return you a public HTTPS URL like: `https://something.ngrok.io`. + +You can now send **API requests** from anywhere using that URL instead of localhost. + +--- + +### **4. Using the API** + +#### **POST/enroll** + +Enrolls a new user with password and phrases (array of strings). + +##### **Request example**: + +```bash +curl -X POST "http://127.0.0.1:8000/enroll" \ + -H "Content-Type: application/json" \ + -d '{ + "user_id": "user1", + "password": "StrongPass123!", + "phrases": ["open sesame", "blue moon", "night watch"] + }' +``` + +##### **Response**: + +```json +{"status": "enrolled"} +``` + +##### **Notes**: + +- Storing only **hashes** inside `guard_data.txt`. +- Rejects duplicate enrollments for same user ID. + +--- + +#### **POST/verify** + +Checks whether a given phrase matches any enrolled phrase. + +##### **Request example**: + +```bash +curl -X POST "http://127.0.0.1:8000/verify" \ + -H "Content-Type: application/json" \ + -d '{ + "user_id": "user1", + "phrase": "blue moon" + }' +``` + +##### **Response**: + +- On success: + +```json +{"authorized": true} +``` + +- On wrong phrase : `HTTP 401 - Denied`; +- Too many failures (set to 5) : `HTTP 429 - Too many failed attempts`; +- Suspicious words like `password`, `hint` etc. : `HTTP 403 - Session closed for security reasons`. + +--- + +### **5. Data file (`guard_data.txt`)** + +Structure: + +```json +{ + "user1": { + "password_hash": "$2b$12$...", + "phrases": ["3d6f0c9b...", "1f9a7a1e..."], + "locked_until": 0, + "attempts": 0 + } +} +``` + +--- + + + + + diff --git a/llm-password-reset/guard_openai.yaml b/llm-password-reset/guard_openai.yaml new file mode 100644 index 00000000..2f82814b --- /dev/null +++ b/llm-password-reset/guard_openai.yaml @@ -0,0 +1,116 @@ +openapi: 3.1.0 +info: + title: GUARD backend + version: 1.0.0 +paths: + + /user/{user_id}: + get: + operationId: checkUserStatus + summary: Check if a username exists and return its status (locked/active) + parameters: + - name: user_id + in: path + required: true + schema: + type: string + responses: + "200": + description: User status returned + "404": + description: User not found + + /enroll: + post: + operationId: enrollUser + summary: Enroll a new user in the password reset system + requestBody: + required: true + content: + application/json: + schema: + type: object + properties: + user_id: + type: string + description: Unique identifier for the user + password: + type: string + description: The user's chosen password (will be hashed) + phrases: + type: array + items: + type: string + description: Secret phrases used for verification (raw text format) + responses: + "200": + description: Enrollment success + "400": + description: User already enrolled + "500": + description: Internal Server Error + + /verify: + post: + operationId: verifyIdentity + summary: Verify identity using either Password (exact) or Passphrase (semantic) + requestBody: + required: true + content: + application/json: + schema: + type: object + properties: + user_id: + type: string + input_text: + type: string + auth_type: + type: string + enum: [password, phrase] + responses: + "200": + description: Verification processed + content: + application/json: + schema: + type: object + properties: + status: + type: string + enum: [authorized, denied, ambiguous] + score: + type: number + message: + type: string + "404": + description: User not found + "429": + description: Account Locked + + /update_account: + post: + operationId: updateAccountCredentials + summary: Update the user's password AFTER successful verification. + description: This endpoint allows setting a new password. Recovery passphrases cannot be changed. + requestBody: + required: true + content: + application/json: + schema: + type: object + required: [user_id, new_password] + properties: + user_id: + type: string + new_password: + type: string + description: The new password (unhashed) + responses: + "200": + description: Password update successful + "500": + description: Database error +servers: + # https://retral-maire-lily.ngrok-free.dev + - url: https://NGROK-SPECIFIED-CONNECTION.ngrok-free.dev diff --git a/llm-password-reset/guard_server.py b/llm-password-reset/guard_server.py new file mode 100644 index 00000000..e71fd84b --- /dev/null +++ b/llm-password-reset/guard_server.py @@ -0,0 +1,243 @@ +import sqlite3 +import bcrypt +import logging +import json +import re +import torch +from datetime import datetime, timedelta +from fastapi import FastAPI, HTTPException +from pydantic import BaseModel +from sentence_transformers import SentenceTransformer, util + +DB_FILE = "guard_secure.db" +MAX_ATTEMPTS = 3 +COOLDOWN_MINUTES = 10 + +device = "cpu" # "cuda" if torch.cuda.is_available() else "cpu" +model = SentenceTransformer('all-MiniLM-L6-v2', device=device) + +logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s') +logger = logging.getLogger(__name__) + +app = FastAPI(title="GUARD Backend", version="3.0.0") + +# TODO: +# 1. update instructions inside guard (force backend call for password reset of any kind) +# 2. handle ambiguous reponse +# 3. documentation inside README.md +# 4. creation of dockerfile (image) +# 5. privacy policy URL +# 6. presentation + +# creation of database if file not already created (handles both users and phrases tables) +def init_db(): + """Initialize SQL database with proper schema.""" + with sqlite3.connect(DB_FILE) as conn: + conn.execute(""" + CREATE TABLE IF NOT EXISTS users ( + user_id TEXT PRIMARY KEY, + password_hash TEXT NOT NULL, + locked_until REAL DEFAULT 0, + attempts INTEGER DEFAULT 0 + ) + """) + # phrases are storred in a separate table + # not stored in raw format, but after embedding mechanism is applied + conn.execute(""" + CREATE TABLE IF NOT EXISTS phrases ( + user_id TEXT, + phrase TEXT, + FOREIGN KEY(user_id) REFERENCES users(user_id) + ) + """) +init_db() + +# get user function, used as first measure of safety when trying to modify a password; +# first, guard verifies that a specified username exists in the database +def get_user(user_id): + with sqlite3.connect(DB_FILE) as conn: + conn.row_factory = sqlite3.Row + return conn.execute("SELECT * FROM users WHERE user_id = ?", (user_id,)).fetchone() + +# set the attempts, good measure of how many attempts remaining before cooldown of 15 minutes, +# user cannot access in that time any account information +def update_attempts(user_id, attempts, locked_until=0): + with sqlite3.connect(DB_FILE) as conn: + conn.execute("UPDATE users SET attempts = ?, locked_until = ? WHERE user_id = ?", + (attempts, locked_until, user_id)) + +# remove noisy punctuation, but keep '.,!?', replace '&' with 'and', normalize spaces. +def clean_text(text: str) -> str: + text = str(text) + text = text.replace("&", " and ") + text = re.sub(r"[\[\]\{\}\(\)\<\>\"\'\:\;\#\@\-\_]", " ", text) + text = re.sub(r"\s+", " ", text) + return text.strip() + +# core function of guard's semantic authentication, uses embeddings to encode meaning +# SBERT model 'all-Mini-L6-v2' for embeddings, then use of cosine similarity +def calculate_similarity(input_phrase, stored_embeddings): + if not stored_embeddings or len(stored_embeddings) == 0: + return 0.0 + + cleaned_input = clean_text(input_phrase) + input_emb = model.encode(cleaned_input, convert_to_tensor=True) + + stored_tensor = torch.tensor(stored_embeddings).to(device) + cosine_scores = util.cos_sim(input_emb, stored_tensor)[0] + + return float(torch.max(cosine_scores)) + +# MODELS + +# model for the enrollment stage +class EnrollRequest(BaseModel): + user_id: str + password: str + phrases: list[str] + +# model for the verification process +class VerifyRequest(BaseModel): + user_id: str + input_text: str + auth_type: str + +# model for updating the password +class UpdateRequest(BaseModel): + user_id: str + new_password: str + +# ENDPOINTS + +# user verification phase +@app.get("/user/{user_id}") +def check_user_status(user_id: str): + user = get_user(user_id) + if not user: + raise HTTPException(404, "User not found") + + # check if currently locked + is_locked = user['locked_until'] > datetime.utcnow().timestamp() + return { + "exists": True, + "is_locked": is_locked, + "locked_until": user['locked_until'] + } + +# enrollment phase +@app.post("/enroll") +def enroll(req: EnrollRequest): + if get_user(req.user_id): + raise HTTPException(400, "User already enrolled.") + + pw_hash = bcrypt.hashpw(req.password.encode(), bcrypt.gensalt()).decode() + + try: + with sqlite3.connect(DB_FILE) as conn: + conn.execute("INSERT INTO users (user_id, password_hash) VALUES (?, ?)", (req.user_id, pw_hash)) + for p in req.phrases: + cleaned_p = clean_text(p) + vector = model.encode(cleaned_p) + vector_json = json.dumps(vector.tolist()) + + conn.execute("INSERT INTO phrases (user_id, phrase) VALUES (?, ?)", (req.user_id, vector_json)) + logger.info(f"New user enrolled with embeddings: {req.user_id}") + return {"status": "enrolled"} + except Exception as e: + logger.error(f"Enrollment error: {e}") + raise HTTPException(500, "Internal Server Error") + +# verification logic +@app.post("/verify") +def verify(req: VerifyRequest): + user = get_user(req.user_id) + if not user: + raise HTTPException(404, "User not found.") + + current_time = datetime.utcnow().timestamp() + if user['locked_until'] > current_time: + remaining = int(user['locked_until'] - current_time) + # fail; do not procced to authentication + raise HTTPException(429, f"Account is locked. Wait {remaining} seconds.") + + auth_status = "denied" # Default to denied + similarity_score = 0.0 + + # Scenario G_Phase1: + if req.auth_type == "password": + # check bcrypt hash + if bcrypt.checkpw(req.input_text.encode(), user['password_hash'].encode()): + auth_status = "authorized" + # SCENARIO G_Phase2: semantic match + elif req.auth_type == "phrase": + stored_embeddings = [] + # retrieve stored phrases + with sqlite3.connect(DB_FILE) as conn: + stored_phrases = conn.execute("SELECT phrase FROM phrases WHERE user_id = ?", (req.user_id,)).fetchall() + for phrase in stored_phrases: + try: + embedding = json.loads(phrase[0]) + stored_embeddings.append(embedding) + except json.JSONDecodeError: + logger.error("Failed to decode embedding from DB") + + # semantic check + similarity_score = calculate_similarity(req.input_text, stored_embeddings) + logger.info(f"User: {req.user_id} | Input: {req.input_text} | Score: {similarity_score:.2f}") + + if similarity_score >= 0.85: + auth_status = "authorized" + elif 0.60 <= similarity_score < 0.85: + auth_status = "ambiguous" + + # handle cases + + # strong match => access granted + if auth_status == "authorized": + update_attempts(req.user_id, 0) # reset attempts + return {"status": "authorized", "score": similarity_score} + + # ambiguous (gray zone) => request clarification + elif auth_status == "ambiguous": + return { + "status": "ambiguous", + "message": "Verification unclear. Request clarification.", + "score": similarity_score + } + + # weak match => deny access + else: + new_attempts = user['attempts'] + 1 + lock_until = 0 + + if new_attempts >= MAX_ATTEMPTS: + lock_until = (datetime.utcnow() + timedelta(minutes=COOLDOWN_MINUTES)).timestamp() + new_attempts = 0 + + update_attempts(req.user_id, new_attempts, lock_until) + + if lock_until > 0: + raise HTTPException(429, "Too many failed attempts. Account locked.") + + return {"status": "denied", "attempts_remaining": MAX_ATTEMPTS - new_attempts} + +# update entry inside database if a previous authentication granted access +@app.post("/update_account") +def update_account(req: UpdateRequest): + user = get_user(req.user_id) + if not user: + raise HTTPException(404, "User not found.") + + try: + with sqlite3.connect(DB_FILE) as conn: + pw_hash = bcrypt.hashpw(req.new_password.encode(), bcrypt.gensalt()).decode() + + conn.execute("UPDATE users SET password_hash = ? WHERE user_id = ?", + (pw_hash, req.user_id)) + logger.info(f"Password updated for {req.user_id}") + + return {"status": "success", "message": "Password updated successfully."} + + except Exception as e: + logger.error(f"Update error: {e}") + raise HTTPException(500, "Failed to update account.") diff --git a/llm-password-reset/requirements.txt b/llm-password-reset/requirements.txt new file mode 100644 index 00000000..c577e56c --- /dev/null +++ b/llm-password-reset/requirements.txt @@ -0,0 +1,8 @@ +--extra-index-url https://download.pytorch.org/whl/cpu +fastapi +uvicorn +bcrypt +sentence-transformers +numpy +torch +pydantic \ No newline at end of file diff --git a/n8n/README.md b/n8n/README.md deleted file mode 100644 index fc10fcdf..00000000 --- a/n8n/README.md +++ /dev/null @@ -1,3 +0,0 @@ -# Showcase n8n - -A space where we can play around with n8n From 8bc34508065e8e151a60748ad2bf8e8d20541470 Mon Sep 17 00:00:00 2001 From: Tudor Cinteza Date: Fri, 12 Dec 2025 16:54:17 +0200 Subject: [PATCH 2/7] added ambiguous response handler by introducing new context table inside the database to store a partial response --- llm-password-reset/guard_server.py | 94 ++++++++++++++++++++---------- 1 file changed, 62 insertions(+), 32 deletions(-) diff --git a/llm-password-reset/guard_server.py b/llm-password-reset/guard_server.py index e71fd84b..56de0d5c 100644 --- a/llm-password-reset/guard_server.py +++ b/llm-password-reset/guard_server.py @@ -29,7 +29,7 @@ # 5. privacy policy URL # 6. presentation -# creation of database if file not already created (handles both users and phrases tables) +# creation of database if file not already created (handles users and phrases tables, as well as the table of contexts) def init_db(): """Initialize SQL database with proper schema.""" with sqlite3.connect(DB_FILE) as conn: @@ -50,6 +50,15 @@ def init_db(): FOREIGN KEY(user_id) REFERENCES users(user_id) ) """) + + # short-term memory for ambiguous verification flow + conn.execute(""" + CREATE TABLE IF NOT EXISTS auth_context ( + user_id TEXT PRIMARY KEY, + partial_phrase TEXT, + timestamp REAL + ) + """) init_db() # get user function, used as first measure of safety when trying to modify a password; @@ -160,8 +169,9 @@ def verify(req: VerifyRequest): # fail; do not procced to authentication raise HTTPException(429, f"Account is locked. Wait {remaining} seconds.") - auth_status = "denied" # Default to denied + auth_status = "denied" # default to denied similarity_score = 0.0 + final_input_text = req.input_text # default to current input # Scenario G_Phase1: if req.auth_type == "password": @@ -170,6 +180,19 @@ def verify(req: VerifyRequest): auth_status = "authorized" # SCENARIO G_Phase2: semantic match elif req.auth_type == "phrase": + with sqlite3.connect(DB_FILE) as conn: + cursor = conn.execute("SELECT partial_phrase, timestamp FROM auth_context WHERE user_id = ?", (req.user_id,)) + row = cursor.fetchone() + if row: + saved_phrase, saved_ts = row + # context valid for 5 mins + if current_time - saved_ts < 300: + final_input_text = f"{saved_phrase} {req.input_text}" + logger.info(f"Context combined successfully.") + else: + # expired context, clean it up + conn.execute("DELETE FROM auth_context WHERE user_id = ?", (req.user_id,)) + stored_embeddings = [] # retrieve stored phrases with sqlite3.connect(DB_FILE) as conn: @@ -182,8 +205,8 @@ def verify(req: VerifyRequest): logger.error("Failed to decode embedding from DB") # semantic check - similarity_score = calculate_similarity(req.input_text, stored_embeddings) - logger.info(f"User: {req.user_id} | Input: {req.input_text} | Score: {similarity_score:.2f}") + similarity_score = calculate_similarity(final_input_text, stored_embeddings) + logger.info(f"User: {req.user_id} | Input: {final_input_text} | Score: {similarity_score:.2f}") if similarity_score >= 0.85: auth_status = "authorized" @@ -191,35 +214,42 @@ def verify(req: VerifyRequest): auth_status = "ambiguous" # handle cases + with sqlite3.connect(DB_FILE) as conn: - # strong match => access granted - if auth_status == "authorized": - update_attempts(req.user_id, 0) # reset attempts - return {"status": "authorized", "score": similarity_score} - - # ambiguous (gray zone) => request clarification - elif auth_status == "ambiguous": - return { - "status": "ambiguous", - "message": "Verification unclear. Request clarification.", - "score": similarity_score - } - - # weak match => deny access - else: - new_attempts = user['attempts'] + 1 - lock_until = 0 - - if new_attempts >= MAX_ATTEMPTS: - lock_until = (datetime.utcnow() + timedelta(minutes=COOLDOWN_MINUTES)).timestamp() - new_attempts = 0 - - update_attempts(req.user_id, new_attempts, lock_until) - - if lock_until > 0: - raise HTTPException(429, "Too many failed attempts. Account locked.") - - return {"status": "denied", "attempts_remaining": MAX_ATTEMPTS - new_attempts} + # strong match => access granted + if auth_status == "authorized": + update_attempts(req.user_id, 0) # reset attempts + conn.execute("DELETE FROM auth_context WHERE user_id = ?", (req.user_id,)) + return {"status": "authorized", "score": similarity_score} + + # ambiguous (gray zone) => request clarification + elif auth_status == "ambiguous": + conn.execute( + "INSERT OR REPLACE INTO auth_context (user_id, partial_phrase, timestamp) VALUES (?, ?, ?)", + (req.user_id, final_input_text, current_time) + ) + return { + "status": "ambiguous", + "message": "Verification unclear. Request clarification.", + "score": similarity_score + } + + # weak match => deny access + else: + new_attempts = user['attempts'] + 1 + lock_until = 0 + + if new_attempts >= MAX_ATTEMPTS: + lock_until = (datetime.utcnow() + timedelta(minutes=COOLDOWN_MINUTES)).timestamp() + new_attempts = 0 + + update_attempts(req.user_id, new_attempts, lock_until) + conn.execute("DELETE FROM auth_context WHERE user_id = ?", (req.user_id,)) + + if lock_until > 0: + raise HTTPException(429, "Too many failed attempts. Account locked.") + + return {"status": "denied", "attempts_remaining": MAX_ATTEMPTS - new_attempts} # update entry inside database if a previous authentication granted access @app.post("/update_account") From 4f495d64b0903fe7f46def81773656922ededd92 Mon Sep 17 00:00:00 2001 From: Tudor Cinteza Date: Fri, 12 Dec 2025 17:04:47 +0200 Subject: [PATCH 3/7] modified logic of ambiguous response by allowing only one extra clarification question. --- llm-password-reset/guard_server.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/llm-password-reset/guard_server.py b/llm-password-reset/guard_server.py index 56de0d5c..089add6a 100644 --- a/llm-password-reset/guard_server.py +++ b/llm-password-reset/guard_server.py @@ -180,6 +180,8 @@ def verify(req: VerifyRequest): auth_status = "authorized" # SCENARIO G_Phase2: semantic match elif req.auth_type == "phrase": + context_used = False + with sqlite3.connect(DB_FILE) as conn: cursor = conn.execute("SELECT partial_phrase, timestamp FROM auth_context WHERE user_id = ?", (req.user_id,)) row = cursor.fetchone() @@ -188,6 +190,7 @@ def verify(req: VerifyRequest): # context valid for 5 mins if current_time - saved_ts < 300: final_input_text = f"{saved_phrase} {req.input_text}" + context_used = True logger.info(f"Context combined successfully.") else: # expired context, clean it up @@ -211,7 +214,11 @@ def verify(req: VerifyRequest): if similarity_score >= 0.85: auth_status = "authorized" elif 0.60 <= similarity_score < 0.85: - auth_status = "ambiguous" + if context_used: + auth_status = "denied" # No 3rd chances + logger.info("Clarification failed. Denying access.") + else: + auth_status = "ambiguous" # allow only one follow-up question # handle cases with sqlite3.connect(DB_FILE) as conn: From 54cc93708ddc8d7f0b276cdaee8cd51a54c668d9 Mon Sep 17 00:00:00 2001 From: Tudor Cinteza Date: Fri, 12 Dec 2025 17:28:08 +0200 Subject: [PATCH 4/7] modified treshold for grant access to 0.8 as previous one seemed hard to achieve using current model --- llm-password-reset/guard_server.py | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/llm-password-reset/guard_server.py b/llm-password-reset/guard_server.py index 089add6a..9aa6fd63 100644 --- a/llm-password-reset/guard_server.py +++ b/llm-password-reset/guard_server.py @@ -23,11 +23,10 @@ # TODO: # 1. update instructions inside guard (force backend call for password reset of any kind) -# 2. handle ambiguous reponse -# 3. documentation inside README.md -# 4. creation of dockerfile (image) -# 5. privacy policy URL -# 6. presentation +# 2. documentation inside README.md +# 3. creation of dockerfile (image) +# 4. privacy policy URL +# 5. presentation # creation of database if file not already created (handles users and phrases tables, as well as the table of contexts) def init_db(): @@ -211,11 +210,11 @@ def verify(req: VerifyRequest): similarity_score = calculate_similarity(final_input_text, stored_embeddings) logger.info(f"User: {req.user_id} | Input: {final_input_text} | Score: {similarity_score:.2f}") - if similarity_score >= 0.85: + if similarity_score >= 0.80: auth_status = "authorized" - elif 0.60 <= similarity_score < 0.85: + elif 0.60 <= similarity_score < 0.80: if context_used: - auth_status = "denied" # No 3rd chances + auth_status = "denied" logger.info("Clarification failed. Denying access.") else: auth_status = "ambiguous" # allow only one follow-up question From 28c49a3da17ea19ef96e4a27d85220ae92e5c79b Mon Sep 17 00:00:00 2001 From: Tudor Cinteza Date: Fri, 12 Dec 2025 18:03:37 +0200 Subject: [PATCH 5/7] Introduced containerization for the backend server and the ngrok tunnel. Secured auth tokens by moving them to .env and updating .gitignore --- llm-password-reset/.dockerignore | 6 ++++++ llm-password-reset/.gitignore | 1 + llm-password-reset/Dockerfile | 24 ++++++++++++++++++++++++ llm-password-reset/docker-compose.yml | 23 +++++++++++++++++++++++ 4 files changed, 54 insertions(+) create mode 100644 llm-password-reset/.dockerignore create mode 100644 llm-password-reset/Dockerfile create mode 100644 llm-password-reset/docker-compose.yml diff --git a/llm-password-reset/.dockerignore b/llm-password-reset/.dockerignore new file mode 100644 index 00000000..9a1b3021 --- /dev/null +++ b/llm-password-reset/.dockerignore @@ -0,0 +1,6 @@ +__pycache__/ +*.py[cod] +.venv/ +venv/ +*.db +.git \ No newline at end of file diff --git a/llm-password-reset/.gitignore b/llm-password-reset/.gitignore index 9f8a3c08..bddcef6c 100644 --- a/llm-password-reset/.gitignore +++ b/llm-password-reset/.gitignore @@ -4,3 +4,4 @@ __pycache__/ .venv/ venv/ guard_secure.db +.env \ No newline at end of file diff --git a/llm-password-reset/Dockerfile b/llm-password-reset/Dockerfile new file mode 100644 index 00000000..16f9481f --- /dev/null +++ b/llm-password-reset/Dockerfile @@ -0,0 +1,24 @@ +# extended on a minimal Linux OS with pre-installed python 3.12 +FROM python:3.12-slim + +# prevent python from writing .pyc files +ENV PYTHONDONTWRITEBYTECODE=1 +# send print statements directly to terminal (logs) +ENV PYTHONUNBUFFERED=1 + +WORKDIR /app + +# copy and install libraries inside the container +COPY requirements.txt . + +RUN pip install --no-cache-dir --upgrade pip && \ + pip install --no-cache-dir -r requirements.txt + +# copy the rest of the structure into the /app folder +COPY . . + +# listen on port 8000 +EXPOSE 8000 + +# launch the web server +CMD ["uvicorn", "guard_server:app", "--host", "0.0.0.0", "--port", "8000"] \ No newline at end of file diff --git a/llm-password-reset/docker-compose.yml b/llm-password-reset/docker-compose.yml new file mode 100644 index 00000000..7fda67fc --- /dev/null +++ b/llm-password-reset/docker-compose.yml @@ -0,0 +1,23 @@ +services: + # python backend + backend: + build: . + container_name: guard_backend + ports: + - "8000:8000" + volumes: + # maps file on the computer with files inside the container + - ./guard_secure.db:/app/guard_secure.db + restart: always + + ngrok: + image: ngrok/ngrok:latest + container_name: guard_tunnel + # looks for the service named 'backend' + command: "http backend:8000" + ports: + - "4040:4040" + environment: + NGROK_AUTHTOKEN : ${NGROK_AUTHTOKEN} + depends-on: + backend \ No newline at end of file From 9f93fdb0d256225c2de4f928f10d91b8dcfc4992 Mon Sep 17 00:00:00 2001 From: Tudor Cinteza Date: Sat, 13 Dec 2025 00:46:42 +0200 Subject: [PATCH 6/7] modified syntax error inside docker-compose.yml --- llm-password-reset/docker-compose.yml | 4 ++-- llm-password-reset/guard_server.py | 5 ++--- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/llm-password-reset/docker-compose.yml b/llm-password-reset/docker-compose.yml index 7fda67fc..5ab8916b 100644 --- a/llm-password-reset/docker-compose.yml +++ b/llm-password-reset/docker-compose.yml @@ -19,5 +19,5 @@ services: - "4040:4040" environment: NGROK_AUTHTOKEN : ${NGROK_AUTHTOKEN} - depends-on: - backend \ No newline at end of file + depends_on: + - backend \ No newline at end of file diff --git a/llm-password-reset/guard_server.py b/llm-password-reset/guard_server.py index 9aa6fd63..a4f23dac 100644 --- a/llm-password-reset/guard_server.py +++ b/llm-password-reset/guard_server.py @@ -24,9 +24,8 @@ # TODO: # 1. update instructions inside guard (force backend call for password reset of any kind) # 2. documentation inside README.md -# 3. creation of dockerfile (image) -# 4. privacy policy URL -# 5. presentation +# 3. privacy policy URL +# 4. presentation # creation of database if file not already created (handles users and phrases tables, as well as the table of contexts) def init_db(): From 5fad10abe6be370e83b6023c6fadb1cacc8af83e Mon Sep 17 00:00:00 2001 From: Tudor Cinteza Date: Sat, 13 Dec 2025 13:08:49 +0200 Subject: [PATCH 7/7] update README.md with Docker configuration, new endpoints, guard frontend description and SQLite schema --- llm-password-reset/README.md | 255 +++++++++++++++++++++++------ llm-password-reset/guard_server.py | 8 +- 2 files changed, 209 insertions(+), 54 deletions(-) diff --git a/llm-password-reset/README.md b/llm-password-reset/README.md index 7a1c4458..529a149f 100644 --- a/llm-password-reset/README.md +++ b/llm-password-reset/README.md @@ -1,29 +1,36 @@ # **Semantic Zero-Knowledge-Like Proof** Proposing a move from Syntactic Authentication (exact string matching) to Semantic Authentication (meaning-based matching). -This approach allows for "fuzzy" logic, where the user proves they possess specific memories or knowledge without having to act like a robot memorizing an exact, case-sensitive phrase. +This approach allows for "fuzzy" logic, where the user proves they possess specific memories or knowledge without having to memorize an exact, case-sensitive phrase. -Experiment on how we could use llms maybe with RAG and embeddings to handle the reset pass functionality with a secret question you set but instead of needed to write the word as an answer the setup would be like this -- when you define the secret question you would explain to an llm your answer like writing about an event in your life or information you want to have as answer -- then when you need to reset pass you could be shown the question of some hint and you would respond the same describing the answer but not necessary with the same wording or words order like when you defined it, but should be on the same like -- then llm compares the answer you defined before with the one you give now and confirm in percentage that you are actually the holder of the account +Experiment on how we could use `LLMs` with embeddings to handle the reset pass functionality with a secret question you set but instead of needed to write the word as an answer the setup would look like this: +- when you define the secret question you would explain to an `LLM` your answer like writing about an event in your life or information you want to have as answer; +- then when you need to reset pass you could be shown the question or some hint and you would respond the same describing the answer but not necessary with the same wording or word order as when you defined it, but should present similar meaning; +- then `LLM` compares the answer you defined before with the one you give now and confirm in percentage that you are actually the holder of the account. -We can use https://platform.claude.com/docs/en/agent-sdk/overview +**GUARD** : https://chatgpt.com/g/g-692e07d058048191b018d764bc75d71d-g-u-a-r-d -See more details https://gemini.google.com/share/f9a6075f22d1 +See more details : https://gemini.google.com/share/f9a6075f22d1 [See Issue #4](https://github.com/xoriors/experimental/issues/4) ## **How to Run GUARD Locally** -### **1. Prerequisites** +### 1. Prerequisites + +Clone the repository and navigate to the project folder: + +```bash +git clone https://github.com/xoriors/experimental.git +cd experimental/llm-password-reset +``` Make sure you work inside an environment that has these installed: ``` bash python3 -m venv .venv source .venv/bin/activate -pip install fastapi uvicorn bcrypt openai +pip install -r requirements.txt ``` Also, verify to have `ngrok-v3` installed. If not, install by running following commands inside Linux terminal: @@ -41,9 +48,7 @@ Add the **authentication token** for `ngrok` by running: ngrok config add-authtoken $(AUTH_TOKEN) ``` ---- - -### **2. Run the local FastAPI server** +### 2. Run the local FastAPI server In your terminal: @@ -53,9 +58,7 @@ uvicorn guard_server:app --reload --port 8000 This command starts FastAPI locally at: `http://127.0.0.1:8000`. Here, you will see live logs of requests and responses. ---- - -### **3. Expose it to the Internet (with `ngrok`)** +### 3. Expose it to the Internet (with `ngrok`) Open another *terminal window* and run: @@ -67,86 +70,244 @@ If everything was correct so far, `ngrok` should return you a public HTTPS URL l You can now send **API requests** from anywhere using that URL instead of localhost. +## **How to Run GUARD Using Docker** + +### 1. Prerequisites +Ensure you have **Docker Desktop** installed and running. +* [Download Docker Desktop](https://www.docker.com/products/docker-desktop/) + +### 2. Setup the Project +Clone the repository and navigate to the project folder: + +```bash +git clone https://github.com/xoriors/experimental.git +cd experimental/llm-password-reset +``` + +### 3. Ensure Security + +You must create a local environment file to store your Ngrok authentication token securely. This file is ignored by igt to protect your privacy. + +```bash +echo "NGROK_AUTHTOKEN=your_actual_token_here" > .env +``` + +### 4. Build and run + +Use Docker Compose to build the backend image and start the tunnel automatically. + +```bash +docker-compose up --build +``` + +## **Endpoints** + +### **GET /user/{user_id}** + +Checks if a specific *user ID* exists in the database and retrieves their current account status (e.g., `locked`/`active`). + +#### Request Example: + +```bash +curl -X GET "https:///user/tudor" +``` + +#### Response Example: + +1. User exists + +```json +{ + "exists": boolean, + "is_locked": boolean, + "locked_until": timestamp +} +``` + +2. User not found : `HTTPException(404, "User not found")` + --- -### **4. Using the API** +### **POST /enroll** -#### **POST/enroll** +Enrolls a new user by hashing their password and converting their semantic passphrases into vector embeddings. -Enrolls a new user with password and phrases (array of strings). +#### Logic -##### **Request example**: +1. Validates password strength. +2. Hashes the password using `bcrypt`. +3. Uses `sentence-transformers` (SBERT) to convert each passphrase into a 384-dimensional vector. +4. Discards the raw text of the phrases immediately (Zero-Knowledge). + +#### Request example: ```bash -curl -X POST "http://127.0.0.1:8000/enroll" \ +curl -X POST "https:///enroll" \ -H "Content-Type: application/json" \ -d '{ "user_id": "user1", - "password": "StrongPass123!", - "phrases": ["open sesame", "blue moon", "night watch"] + "password": "StrongPassword123!", + "phrases": [ + "The old oak tree in my backyard where I built a fort", + "My first car was a red convertible from the 90s" + ] }' ``` -##### **Response**: +#### Response example: ```json {"status": "enrolled"} ``` -##### **Notes**: +--- -- Storing only **hashes** inside `guard_data.txt`. -- Rejects duplicate enrollments for same user ID. +### **POST /verify** ---- +Verifies a user's identity by comparing the semantic meaning of their input against stored embeddings (or exact match using old password). -#### **POST/verify** +#### Logic -Checks whether a given phrase matches any enrolled phrase. +1. Converts `input_text` into a vector (embedding). +2. Calculates **cosine similarity** against all stored vectors for that user. +3. Determines status based on thresholds: + - > above 0.80: `authorized` (high confidence). + - > 0.65 - 0.80: `ambiguous` (needs clarification). + - > below 0.65: `denied`. -##### **Request example**: +#### Request example: ```bash -curl -X POST "http://127.0.0.1:8000/verify" \ +curl -X POST "https:///verify" \ -H "Content-Type: application/json" \ -d '{ "user_id": "user1", - "phrase": "blue moon" + "input_text": "The tree where I had a fort", + "auth_type": "phrase" }' ``` -##### **Response**: +#### Response Example: -- On success: +1. Success (Authorized) ```json -{"authorized": true} +{ + "status": "authorized", + "score": 0.89, + "message": "Identity verified." +} ``` -- On wrong phrase : `HTTP 401 - Denied`; -- Too many failures (set to 5) : `HTTP 429 - Too many failed attempts`; -- Suspicious words like `password`, `hint` etc. : `HTTP 403 - Session closed for security reasons`. +2. Ambiguous (Request Clarification) ---- +``` json +{ + "status": "ambiguous", + "message": "Verification unclear. Request clarification.", + "score": 0.74 +} +``` -### **5. Data file (`guard_data.txt`)** +*GUARD is trained to ask a follow-up question when it sees this status.* -Structure: +3. Failure (Denied) ```json { - "user1": { - "password_hash": "$2b$12$...", - "phrases": ["3d6f0c9b...", "1f9a7a1e..."], - "locked_until": 0, - "attempts": 0 - } + "status": "denied", + "score": 0.45, + "message": "Access denied." } ``` --- +### **POST /update** + +Updates the credentials (password) for an existing user. This endpoint is called **only** after the user has successfully completed the Semantic Verification or Password Verification phase. + +#### Logic + +1. Checks if the user exists. +2. Hashes the **new_password** using `bcrypt`(Salted). +3. Overwrites the old password hash in the `users` table. + +#### Request Example: + +```json +curl -X POST "https:///update" \ + -H "Content-Type: application/json" \ + -d '{ + "user_id": "tudor", + "new_password": "NewSuperSecurePassword456!" + }' +``` + +#### Response Example: + +```json +{ + "status": "updated", + "message": "Password updated successfully." +} +``` + + +## **Database Structure** + +File Location: `./guard_secure.db` (inside the Docker container volume). + +- >TABLE 1: **USERS** + +Stores authentication credentials and security counters (locking mechanism). + +| Column | Type | Description | +| :--- | :--- | :--- | +| `user_id` | `TEXT (PK)` | Unique username. | +| `password_hash` | `TEXT` | Bcrypt salted hash. | +| `locked_until` | `REAL` | Timestamp for when the account unlock (0 if unlocked). | +| `attempts` | `INTEGER` | Counter for failed login attempts. | + +- >TABLE 2: **PASSPHRASES** + +Stores the semantic memory of the user. Raw text is never stored here. + +| Column | Type | Description | +| :--- | :--- | :--- | +| `user_id` | `TEXT (FK)` | Links to the `users` table. | +| `phrase` | `TEXT` | **The serialized Vector Embedding** (JSON string of floats). | + +- >TABLE 3: **AUTH_CONTEXT** + +This table acts as the **Short-Term Memory** for the system. It handles the `ambiguous` flow by temporarily saving the state of a verification attempt. + +| Column | Type | Description | +| :--- | :--- | :--- | +| `user_id` | `TEXT (PK)` | The user attempting verification. | +| `partial_phrase` | `TEXT` | The first (ambiguous) input provided by the user. | +| `timestamp` | `REAL` | Unix timestamp of when the attempt started. | + +**How it works**: When a user gives a vague answer, their input is saved here. When they reply to the clarification question, the system retrieves this last_input, combines it with the new answer, and performs a final check. This entry is deleted immediately after the check finishes (One-Strike Policy). + +## **Frontend: Custom GPT Architecture** + +Instead of a traditional website, the "Frontend" is a **Custom GPT** (ChatGPT Agent) named **GUARD**, configured to act as a secure conversational interface. + +### 1. Architecture & System Instructions +The GPT acts as a secure intermediary between the user and the Python backend. It operates on a strict **System Prompt** that enforces security protocols, Zero-Knowledge handling, and the ambiguity resolution flow. + +### 2. Configuration Setup +To recreate the frontend agent in ChatGPT: + +* **Actions Schema:** Use the `guard_openapi.yaml` file provided in this repository (exported from FastAPI). This defines how the GPT talks to your endpoints (`/enroll`, `/verify`, etc.). +* **Server URL:** Set to your public Ngrok address (e.g., `https://xyz.ngrok-free.app`). +* **Authentication:** None (OpenAPI); the backend handles user authentication semantically. +### 3. Privacy Policy (Required) +Since the Agent handles user data, a Privacy Policy is hosted on **GitHub Gist**. +* **Function:** Informs users that passphrases are converted to **Zero-Knowledge Vectors** and raw text is never stored. +* **Implementation:** The Gist URL is added to the GPT's privacy settings to comply with OpenAI policies. diff --git a/llm-password-reset/guard_server.py b/llm-password-reset/guard_server.py index a4f23dac..af90c92b 100644 --- a/llm-password-reset/guard_server.py +++ b/llm-password-reset/guard_server.py @@ -19,13 +19,7 @@ logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s') logger = logging.getLogger(__name__) -app = FastAPI(title="GUARD Backend", version="3.0.0") - -# TODO: -# 1. update instructions inside guard (force backend call for password reset of any kind) -# 2. documentation inside README.md -# 3. privacy policy URL -# 4. presentation +app = FastAPI(title="GUARD Backend", version="3.2.0") # creation of database if file not already created (handles users and phrases tables, as well as the table of contexts) def init_db():