From 4d55d9d82a4aeea8b090480421741043a88f6e5c Mon Sep 17 00:00:00 2001 From: Bryan Van Deusen Date: Mon, 9 Mar 2026 18:28:25 -0400 Subject: [PATCH 1/2] Fix scanner probes returning 200 via SPA catch-all The 404 handler was unconditionally serving index.html (200) for all non-API, non-static paths, including scanner probes for .php, .asp, .cgi etc. Added _SPA_EXTENSIONS set so paths with unknown extensions get a real 404 instead of a misleading 200. Co-Authored-By: Claude Sonnet 4.6 --- src/fabledassistant/app.py | 21 +++++++++++++++++++-- 1 file changed, 19 insertions(+), 2 deletions(-) diff --git a/src/fabledassistant/app.py b/src/fabledassistant/app.py index b823292..53d4585 100644 --- a/src/fabledassistant/app.py +++ b/src/fabledassistant/app.py @@ -238,17 +238,34 @@ def create_app() -> Quart: resp.headers["Cache-Control"] = "no-cache, no-store, must-revalidate" return resp + # File extensions that belong to the SPA or its assets. + # Anything else with an extension is not a valid app path and gets a hard 404. + _SPA_EXTENSIONS = { + "", ".html", ".js", ".css", ".ico", ".png", ".jpg", ".jpeg", + ".svg", ".webp", ".woff", ".woff2", ".ttf", ".otf", ".map", ".json", + } + @app.errorhandler(404) async def handle_404(error): # Return JSON 404 for API routes if request.path.startswith("/api/"): return jsonify({"error": "Not found"}), 404 - # Try to serve static file + + # Try to serve a real static file first path = request.path.lstrip("/") file_path = STATIC_DIR / path if path and file_path.is_file(): return await send_from_directory(STATIC_DIR, path) - # SPA fallback + + # Reject paths with file extensions the app doesn't serve. + # This turns scanner probes (.php, .asp, .cgi, etc.) into honest 404s + # instead of serving them the Vue SPA with a misleading 200. + from pathlib import PurePosixPath + suffix = PurePosixPath(request.path).suffix.lower() + if suffix not in _SPA_EXTENSIONS: + return "", 404 + + # SPA fallback for clean client-side routes (/notes/123, /chat, etc.) resp = await make_response( await send_from_directory(STATIC_DIR, "index.html") ) From 46672725a13dc59164f13beea5d03626a05f5050 Mon Sep 17 00:00:00 2001 From: Bryan Van Deusen Date: Mon, 9 Mar 2026 18:42:07 -0400 Subject: [PATCH 2/2] Skip semantic duplicate check for bare-title tasks/notes MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The semantic similarity check was flagging unrelated short-title tasks as duplicates (e.g. "Lore: Shell 0" matching "Lore: Reinitialization 0" at 91%) because with no body, the embedding is purely title-based and co-domain tasks in the same project share a tight embedding neighborhood. Only run the semantic check when the body is ≥ 80 chars — enough content to make a meaningful comparison. The fuzzy title check already covers exact/near-exact title duplicates. Co-Authored-By: Claude Sonnet 4.6 --- src/fabledassistant/services/tools.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/fabledassistant/services/tools.py b/src/fabledassistant/services/tools.py index 6b8378a..e8bccba 100644 --- a/src/fabledassistant/services/tools.py +++ b/src/fabledassistant/services/tools.py @@ -825,7 +825,7 @@ async def execute_tool(user_id: int, tool_name: str, arguments: dict) -> dict: item_type = "task" if near.status is not None else "note" return {"success": False, "error": f"A {item_type} with a very similar title '{near.title}' already exists (id: {near.id}, similarity: {ratio:.0%}). Use update_note to modify it instead of creating a duplicate."} - if not arguments.get("confirmed"): + if not arguments.get("confirmed") and len(task_body.strip()) >= 80: from fabledassistant.services.embeddings import semantic_search_notes as _ssn sem_query = f"{task_title}\n{task_body}".strip() sem_hits = await _ssn(user_id, sem_query, limit=3, threshold=0.87) @@ -903,7 +903,7 @@ async def execute_tool(user_id: int, tool_name: str, arguments: dict) -> dict: item_type = "task" if near.status is not None else "note" return {"success": False, "error": f"A {item_type} with a very similar title '{near.title}' already exists (id: {near.id}, similarity: {ratio:.0%}). Use update_note to modify it instead of creating a duplicate."} - if not arguments.get("confirmed"): + if not arguments.get("confirmed") and len(note_body.strip()) >= 80: from fabledassistant.services.embeddings import semantic_search_notes as _ssn sem_query = f"{note_title}\n{note_body}".strip() sem_hits = await _ssn(user_id, sem_query, limit=3, threshold=0.87)