refactor(scribe): retire calendar/events + person/place/list entities (backend)
CI & Build / Python lint (push) Successful in 3s
CI & Build / TypeScript typecheck (push) Successful in 11s
CI & Build / integration (push) Successful in 30s
CI & Build / Python tests (push) Failing after 31s
CI & Build / Build & push image (push) Has been skipped
CI & Build / Python lint (push) Successful in 3s
CI & Build / TypeScript typecheck (push) Successful in 11s
CI & Build / integration (push) Successful in 30s
CI & Build / Python tests (push) Failing after 31s
CI & Build / Build & push image (push) Has been skipped
Narrow Scribe to a Claude-Code work system-of-record (milestone #194, decision note #1759). Wholesale removal per rule #22 — backend + schema half. Calendar/events + CalDAV: delete models/event, services/{events,caldav, caldav_sync}, routes/events, mcp/tools/events; strip event branches from backup (bump v3->v4), dashboard (upcoming_events), trash, recent, and the mcp server read-only allowlist + instructions. Typed entities (person/place/list): delete mcp/tools/entities; drop the notes.metadata (entity_meta) column from model/service/routes and the knowledge browse service. note_type STAYS — it also marks 'process' notes. Scheduler: event_scheduler -> recurrence_scheduler, keeping only the recurring-task spawn job (drops event reminders + CalDAV sync). Schema: migration 0069 drops the events table + notes.metadata column + orphan caldav settings rows (faithful downgrade recreates them). KEEP: recurrence.py (task recurrence), notifications task reminders, graph view, and every work surface. Frontend + plugin/docs true-up follow next. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BPtbSzA4JLMAKgFZ8VTg7Q
This commit is contained in:
@@ -1,47 +1,19 @@
|
||||
"""User settings + integrations (CalDAV, SearXNG status).
|
||||
"""User settings + integrations (SearXNG status).
|
||||
|
||||
Chat-model picker endpoints (/models), KV-cache priming, and journal-schedule
|
||||
hooks were removed in Phase 8 alongside the chat/journal subsystems.
|
||||
"""
|
||||
import ipaddress
|
||||
import logging
|
||||
import socket
|
||||
from urllib.parse import urlparse
|
||||
|
||||
from quart import Blueprint, jsonify, request
|
||||
|
||||
from scribe.auth import login_required, get_current_user_id
|
||||
from scribe.config import Config
|
||||
from scribe.services.caldav import CALDAV_SETTING_KEYS, get_caldav_config, test_connection
|
||||
from scribe.services.settings import delete_setting, get_all_settings, get_setting, set_settings_batch
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def _is_private_url(url: str) -> bool:
|
||||
"""SSRF-blocking helper: returns True for URLs that resolve to private,
|
||||
loopback, or link-local addresses. Inlined here after services/llm.py
|
||||
(the original home) was removed in Phase 8."""
|
||||
try:
|
||||
host = urlparse(url).hostname
|
||||
if not host:
|
||||
return True
|
||||
# Resolve to all addresses; reject if any is private/loopback/link-local.
|
||||
infos = socket.getaddrinfo(host, None)
|
||||
for family, *_rest, sockaddr in infos:
|
||||
ip_str = sockaddr[0]
|
||||
try:
|
||||
ip = ipaddress.ip_address(ip_str)
|
||||
except ValueError:
|
||||
continue
|
||||
if ip.is_private or ip.is_loopback or ip.is_link_local or ip.is_reserved:
|
||||
return True
|
||||
except Exception:
|
||||
# Conservative: if we can't resolve, treat as private (reject).
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
settings_bp = Blueprint("settings", __name__, url_prefix="/api/settings")
|
||||
|
||||
|
||||
@@ -76,52 +48,6 @@ async def update_settings_route():
|
||||
return jsonify(settings)
|
||||
|
||||
|
||||
@settings_bp.route("/caldav", methods=["GET"])
|
||||
@login_required
|
||||
async def get_caldav():
|
||||
uid = get_current_user_id()
|
||||
config = await get_caldav_config(uid)
|
||||
if config.get("caldav_password"):
|
||||
config["caldav_password"] = "********"
|
||||
return jsonify(config)
|
||||
|
||||
|
||||
@settings_bp.route("/caldav", methods=["PUT"])
|
||||
@login_required
|
||||
async def update_caldav():
|
||||
uid = get_current_user_id()
|
||||
data = await request.get_json()
|
||||
|
||||
# Validate CalDAV URL before saving — block internal/private addresses
|
||||
if "caldav_url" in data:
|
||||
url = str(data.get("caldav_url") or "").strip()
|
||||
if url:
|
||||
parsed_scheme = url.split("://")[0].lower() if "://" in url else ""
|
||||
if parsed_scheme not in ("http", "https"):
|
||||
return jsonify({"error": "CalDAV URL must use http or https"}), 400
|
||||
if _is_private_url(url):
|
||||
return jsonify({"error": "CalDAV URL must not point to an internal or private address"}), 400
|
||||
|
||||
settings_to_save = {}
|
||||
for key in CALDAV_SETTING_KEYS:
|
||||
if key in data:
|
||||
if key == "caldav_password" and data[key] == "********":
|
||||
continue
|
||||
settings_to_save[key] = str(data[key])
|
||||
|
||||
if settings_to_save:
|
||||
await set_settings_batch(uid, settings_to_save)
|
||||
return jsonify({"status": "ok"})
|
||||
|
||||
|
||||
@settings_bp.route("/caldav/test", methods=["POST"])
|
||||
@login_required
|
||||
async def test_caldav():
|
||||
uid = get_current_user_id()
|
||||
result = await test_connection(uid)
|
||||
return jsonify(result)
|
||||
|
||||
|
||||
@settings_bp.route("/search", methods=["GET"])
|
||||
@login_required
|
||||
async def test_search():
|
||||
|
||||
Reference in New Issue
Block a user