CI & Build / Python lint (push) Successful in 3s
CI & Build / Plugin hooks (push) Successful in 7s
CI & Build / TypeScript typecheck (push) Successful in 22s
CI & Build / integration (push) Successful in 22s
CI & Build / Python tests (push) Successful in 53s
CI & Build / Build & push image (push) Successful in 39s
Second adapter consumer. POST /api/webhooks/forge validates Gitea's
X-Gitea-Signature (HMAC-SHA256, constant-time; no secret configured =
the endpoint 404s out of existence), extracts changed/removed paths,
and flags matched snippets by writing verification.invalidated_by
{commit_sha, at, path, removed} — the existing attention vocabulary
extended, not a new flag: needs_attention includes it, both filter
dialects (Python + jsonpath SQL) include it in 'attention' and exclude
it from 'ok', and recording ANY fresh verdict clears it by construction
because compose_verification builds a new dict. Unverified snippets are
skipped (already in their own bucket); replayed deliveries at the same
head commit are no-ops; processing failures return 200 with a WARNING +
AppLog canary so the forge never marks deliveries failed and operators
never disable the hook over a transient (#2663's lesson).
Matching goes through repo BINDINGS: recorded location repos are
free-form names ('Scribe') that cannot address a forge, so a snippet
reaches its forge repo through its project's binding — which also fixes
step 5's pull-time resolution for every real record via the same
fallback. O(bindings + snippets-in-project + changed files).
Settings: webhook secret beside the forge config (masked, sentinel-
skipped, Docker-secret env channel, endpoint documented in the UI).
Tests: signature gate, payload parsing, path semantics, both filter
dialects extended in the drift-check guard file, and real-Postgres
end-to-end (flag lands, attention lists it, replay quiet, re-verify
clears, unbound repo untouched).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
76 lines
2.5 KiB
Python
76 lines
2.5 KiB
Python
"""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 logging
|
|
|
|
from quart import Blueprint, jsonify, request
|
|
|
|
from scribe.auth import login_required, get_current_user_id
|
|
from scribe.config import Config
|
|
from scribe.services.settings import delete_setting, get_all_settings, get_setting, set_settings_batch
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
settings_bp = Blueprint("settings", __name__, url_prefix="/api/settings")
|
|
|
|
# Keys whose values are credentials. The admin endpoints that own them mask on
|
|
# read and skip the mask on write; this generic KV surface has to apply the
|
|
# same treatment, or it silently un-masks what those endpoints masked — the
|
|
# rows live on the admin's own user_id, so the plain GET returned them raw.
|
|
_SECRET_KEYS = frozenset({"smtp_password", "forge_token", "forge_webhook_secret"})
|
|
_SECRET_MASK = "********"
|
|
|
|
|
|
def _masked(settings: dict) -> dict:
|
|
return {
|
|
k: (_SECRET_MASK if k in _SECRET_KEYS and v else v)
|
|
for k, v in settings.items()
|
|
}
|
|
|
|
|
|
@settings_bp.route("", methods=["GET"])
|
|
@login_required
|
|
async def get_settings_route():
|
|
uid = get_current_user_id()
|
|
settings = await get_all_settings(uid)
|
|
return jsonify(_masked(settings))
|
|
|
|
|
|
@settings_bp.route("", methods=["PUT"])
|
|
@login_required
|
|
async def update_settings_route():
|
|
uid = get_current_user_id()
|
|
data = await request.get_json()
|
|
if not isinstance(data, dict):
|
|
return jsonify({"error": "Expected a JSON object"}), 400
|
|
|
|
to_save = {}
|
|
for k, v in data.items():
|
|
str_v = str(v)
|
|
# A masked secret round-tripping through a client is "unchanged", not
|
|
# a request to store the mask over the real credential.
|
|
if k in _SECRET_KEYS and str_v == _SECRET_MASK:
|
|
continue
|
|
if not str_v:
|
|
await delete_setting(uid, k)
|
|
else:
|
|
to_save[k] = str_v
|
|
|
|
if to_save:
|
|
await set_settings_batch(uid, to_save)
|
|
|
|
settings = await get_all_settings(uid)
|
|
return jsonify(_masked(settings))
|
|
|
|
|
|
@settings_bp.route("/search", methods=["GET"])
|
|
@login_required
|
|
async def test_search():
|
|
"""Report SearXNG configuration status (used by the Integrations tab)."""
|
|
if not Config.searxng_enabled():
|
|
return jsonify({"configured": False, "results": [], "searxng_url": ""})
|
|
return jsonify({"configured": True, "results": [], "searxng_url": Config.SEARXNG_URL})
|