refactor(usage): close out round-5 nits on PR #871

Non-blocking follow-ups from the merge-ready review:

- semantic.py: bound the doc_types copied into embeddings_queries metadata
  to _USAGE_METADATA_MAX_DOC_TYPES (16). doc_types is caller-supplied with
  no max_length on the tool signature; capping the stored copy keeps one
  JSONB row from ballooning (not a billing/injection risk — CP ignores
  metadata, binds are parameterized).
- migration: note that `metric` is intentionally unconstrained Text and
  that adding a third metric requires keeping the CP-side catalog in sync,
  else the rollup silently ignores the new rows.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Chris Coutinho
2026-06-07 15:47:57 +02:00
co-authored by Claude Opus 4.8
parent 9c2f9fac46
commit c89f724585
2 changed files with 20 additions and 2 deletions
@@ -55,7 +55,11 @@ def upgrade() -> None:
postgresql.TIMESTAMP(timezone=True) if is_pg else sa.TIMESTAMP(),
nullable=False,
),
# Catalog metric: 'embeddings_queries' or 'pages_chunks'.
# Catalog metric: 'embeddings_queries' or 'pages_chunks'. Deliberately
# an unconstrained Text (no CHECK/enum) — the metric catalog lives in
# control-plane config, not the app-DB schema. If a third metric is
# ever added, the CP-side catalog must learn it too, or its rollup will
# silently ignore the new rows; keep the two in sync.
sa.Column("metric", sa.Text(), nullable=False),
sa.Column("value", sa.BigInteger(), nullable=False),
# Rawest unit per request (provider, model, tokens, doc_type, ...).
+15 -1
View File
@@ -46,6 +46,15 @@ from nextcloud_mcp_server.vector.qdrant_client import get_qdrant_client
logger = logging.getLogger(__name__)
# Cap how many doc_types we copy into a usage-metering metadata row. doc_types
# is caller-supplied and (unlike path_prefixes) has no max_length on the tool
# signature, so an adversarial caller could pass a huge list. The CP rollup
# ignores metadata for billing (GROUP BY day, metric) and the value is bound
# parameterized, so this is not a billing/injection risk — the cap just keeps
# a single JSONB row from ballooning. 16 is generous headroom over the handful
# of real indexed doc types.
_USAGE_METADATA_MAX_DOC_TYPES = 16
def configure_semantic_tools(mcp: FastMCP):
"""Configure semantic search tools for MCP server."""
@@ -538,7 +547,12 @@ def configure_semantic_tools(mcp: FastMCP):
metadata={
"user_id": username,
"fusion": fusion,
"doc_types": doc_types,
# Bounded copy — see _USAGE_METADATA_MAX_DOC_TYPES.
"doc_types": (
doc_types[:_USAGE_METADATA_MAX_DOC_TYPES]
if doc_types
else doc_types
),
},
# The outer guard already confirmed the flag, so pass
# enabled=True directly — the store then skips a second