* feat(store-guardrails): admin-configurable content thresholds Adds the flea-market content guardrail floors to the /admin/server-config editor so operators can tune the bar without code changes. Defaults are unchanged (60 chars description, 25 chars command, 5 distinct words, 200 chars body) — patching guardrails.* in instance.yaml or via the admin UI overrides any of them and the next inline check picks up the new value. src/store_guardrails/content_check.py now resolves the four floors via helper functions (_min_desc_chars / _min_command_desc_chars / _min_distinct_words / _min_body_chars) that read app.instance_config at call time. Module-level _DEFAULT_* constants remain as fallbacks if the import fails (defensive — keeps the guardrail module loadable without the app package on its path). app/instance_config.py grows four matching getters returning the live value with sane defaults + integer coercion. app/api/admin.py registers 'guardrails' as an editable section + ships nine known-fields entries (min_description_chars, min_command_description_chars, min_distinct_words, min_body_chars, enabled, review_model, blocked_quota_per_day, blocked_bundle_ttl_days, stuck_review_grace_seconds) with operator-facing hint copy explaining what each knob does. app/web/templates/admin_server_config.html gets a SECTION_META entry so the section renders as 'Flea-market guardrails' with a help string instead of a bare section ID. app/web/router.py threads the live thresholds into /store/new and /store/examples via a small _guardrail_thresholds() helper so the disclosure copy, char counter, and "Why these limits" table render the configured value (not a hardcoded 60). End-to-end smoke verified: PATCH guardrails.min_description_chars=90 → /store/new immediately renders "90 characters" + JS DESC_MIN=90 on the next request, no restart required (helpers read live config per call). * chore(store-guardrails): address PR review safe-fix findings Code-review safe_auto findings on PR #281 (review run 20260513-100126-64052520): - CHANGELOG: add Unreleased entry covering the new /admin/server-config Flea-market guardrails section, the four live threshold getters, and the route-helper rendering knobs. Required by the project's non-negotiable "Changelog discipline" rule. - content_check.py: narrow `except Exception` to `except ImportError` on the four `_min_*()` resolver helpers. Surface-level TypeError / ValueError on a malformed YAML value belongs to the instance_config getters' own try/except — the resolvers should only defend against the in-tree import itself failing, not silently swallow real bugs in the getters. - store_upload.html: refresh the stale "30-char threshold" comment to reflect the configurable floor (default 60), and add `|default(60)` / `|default(25)` / `|default(5)` filters to the disclosure-copy bindings so the upload form matches store_examples.html's belt-and-suspenders rendering if a future route ever renders the template without populating the `guardrail` context. - router.py: tighten `_guardrail_thresholds()` return annotation from bare `dict` to `dict[str, int]`. Residual work (left for separate change after operator direction): - Add round-trip test (PATCH guardrails -> next inline check uses new value) — primary testing gap. - Decide policy on `min_*=0` (currently coerced to 1 via `max(1, int(val))`) vs treating 0 as a disable sentinel like neighbour getters (`blocked_quota_per_day`, `blocked_bundle_ttl_days`). - Add POST-time integer validation for `guardrails.*` so a typo'd YAML value (bool / string / float) errors loudly instead of silently falling back to the default. * test(store-guardrails): cover admin-configurable thresholds + PATCH round-trip Closes the "primary testing gap" Vojta noted in the safe-fix commit on PR #281 — the four new `get_guardrails_min_*` getters and the PATCH-takes-effect-on-next-check live-config flow had no direct coverage. 10 new tests in `tests/test_store_guardrails_admin_config.py`: - TestGuardrailGetterDefaults (4 tests) — each new getter returns the documented default (60 / 25 / 5 / 200) when nothing is configured. - TestGuardrailGetterOverlay (5 tests) — overlay-driven overrides win, string values that look numeric coerce via int(), garbage strings fall back to default via the (TypeError, ValueError) branch, and the `max(1, int(val))` floor pins zero/negative inputs to 1. - TestPatchRoundTrip (1 test) — PATCH `/api/admin/server-config` `guardrails.min_description_chars=90`, then call content_check against a 75-char description that previously passed: must now fail with `too_short`. Then PATCH back to 60 and verify the next check passes again. Closes the cache-invalidation contract Vojta relies on for the "no app restart" claim — broken without the reset_cache() bracket in /api/admin/server-config. The TestGuardrailGetterOverlay.test_zero_or_negative_floored_to_one test pins the current `max(1, int(val))` policy. Vojta's safe-fix commit explicitly left "policy on min_*=0 vs disable-sentinel" as residual work — pinning the current behavior here ensures any future change to use 0 as a disable sentinel must update this test (and the reviewer sees the policy decision). Verified: 4509 tests pass locally (4499 existing + 10 new). * release: 0.54.2 — admin-configurable flea-market guardrail thresholds + tests Last commit on the PR per CLAUDE.md hard rule. Patch bump (0.54.1 → 0.54.2) bundling Vojta's admin-configurable thresholds for the flea-market content guardrail (9 knobs in /admin/server-config) plus the test coverage closing the "primary testing gap" he punted in the safe-fix commit. No DB migration; defaults unchanged from PR #276 — instances that don't set `guardrails.*` keep the original bar transparently. --------- Co-authored-by: ZdenekSrotyr <zdenek.srotyr@keboola.com> Co-authored-by: ZdenekSrotyr <139972147+ZdenekSrotyr@users.noreply.github.com>
182 lines
7.9 KiB
TOML
182 lines
7.9 KiB
TOML
[project]
|
||
name = "agnes-the-ai-analyst"
|
||
version = "0.54.2"
|
||
description = "Agnes — AI Data Analyst platform for AI analytical systems"
|
||
requires-python = ">=3.11,<3.14"
|
||
license = "MIT"
|
||
readme = "README.md"
|
||
|
||
dependencies = [
|
||
# Core database
|
||
# 1.5.2 fixes a FK-dependency regression that affected ALTER TABLE on
|
||
# tables referenced by other tables — broke the test_db migration
|
||
# ladder replay on 1.5.1. CI runs 1.5.2; local devs need it too.
|
||
"duckdb>=1.5.2",
|
||
# Web framework (FastAPI)
|
||
"fastapi>=0.115.0",
|
||
"uvicorn[standard]>=0.32.0",
|
||
"python-multipart>=0.0.27",
|
||
"jinja2>=3.1.0",
|
||
"starlette>=0.41.0",
|
||
# Authentication
|
||
"PyJWT>=2.8.0",
|
||
"itsdangerous>=2.1.0",
|
||
"authlib>=1.6.12",
|
||
"argon2-cffi>=23.1.0",
|
||
# HTTP client. `h2` enables HTTP/2 multiplexing for the persistent
|
||
# CLI client used by `agnes pull` (one TCP connection serves N
|
||
# concurrent parquet streams + range chunks). `cli/client.py`
|
||
# gracefully falls back to HTTP/1.1 if h2 is missing, so this
|
||
# extra is for performance, not correctness.
|
||
"httpx>=0.27.0",
|
||
"h2>=4.1.0",
|
||
# CLI
|
||
"typer>=0.12.0",
|
||
"rich>=13.0.0",
|
||
# Configuration
|
||
"python-dotenv>=1.0.0",
|
||
"pyyaml>=6.0",
|
||
# Data processing
|
||
"pandas>=2.0.0",
|
||
"pyarrow>=12.0.0",
|
||
"pytz>=2024.1",
|
||
# SQL parsing — server-side WHERE validator for /api/v2/scan (app/api/where_validator.py)
|
||
# Minimum 30.x — older versions had walk() yielding (node, parent, key)
|
||
# tuples instead of expression nodes, which would silently bypass the
|
||
# WHERE-validator structural checks (isinstance(tuple, exp.Subquery)
|
||
# is always False). 30.x yields nodes directly.
|
||
"sqlglot>=30.0.0",
|
||
# Data source connectors
|
||
"google-cloud-bigquery>=3.0.0",
|
||
"google-cloud-bigquery-storage>=2.0.0",
|
||
# Google Workspace Cloud Identity / Admin SDK (Workspace group membership sync)
|
||
"google-api-python-client>=2.0.0",
|
||
# Profiler visualizations
|
||
"matplotlib>=3.8.0",
|
||
"numpy>=1.24.0",
|
||
# Claude Code marketplace endpoint — pure-Python git server mounted in FastAPI
|
||
"dulwich>=0.22.0",
|
||
"a2wsgi>=1.10.0",
|
||
# In-process TTL cache for marketplace etag (transitively present via
|
||
# google-auth, declared explicitly here because we depend on it directly).
|
||
"cachetools>=5.3.0",
|
||
# Per-IP rate limiting on auth endpoints (#45). In-process counters by
|
||
# default — fine for single-replica deploys. Multi-replica rollouts can
|
||
# swap the storage backend via slowapi's `storage_uri` (Redis, Memcached).
|
||
"slowapi>=0.1.9",
|
||
# LLM provider SDKs — core (not dev) because connectors/llm/*_provider.py
|
||
# is imported by services/{corporate_memory, verification_detector} which
|
||
# the scheduler drives in production. Promoted from [dev] in #176 to fix
|
||
# ModuleNotFoundError boot loops on default Compose deploys.
|
||
"anthropic>=0.30.0",
|
||
"openai>=1.30.0",
|
||
# Keboola Storage API SDK — used by:
|
||
# - `connectors/keboola/client.py` for admin-side bucket / table list
|
||
# (consumed from `app/api/admin.py` discover-and-register, table
|
||
# metadata refresh).
|
||
# Extraction itself uses the lightweight `connectors/keboola/storage_api.py`
|
||
# module (export-async + signed-URL download) which talks to Storage API
|
||
# directly via `requests` — no SDK dependency on the data-path side. The
|
||
# SDK stays for the metadata reads.
|
||
#
|
||
# NOTE: kbcstorage moved to the [server] extra below — see the rationale
|
||
# in [project.optional-dependencies].server. CLI wheels installed via
|
||
# `uv tool install` deliberately ship without it.
|
||
"sse-starlette>=2.0",
|
||
# Optional observability — pure-Python, no compilation. Lazily initialized
|
||
# in src/observability/posthog_client.py and only emits events when
|
||
# POSTHOG_API_KEY is set in the environment. With the key unset the
|
||
# integration is fully off (no network, no init). See docs/observability.md.
|
||
"posthog>=3.7.0",
|
||
# Rust-backed (ammonia) HTML sanitizer for admin-edited rich content
|
||
# (news intro + body, curated marketplace-metadata.json descriptions).
|
||
# Allowlist-based with per-tag attribute scoping; closes the bypass
|
||
# shapes the legacy regex sanitizer in src/welcome_template.py was
|
||
# vulnerable to. Pre-built wheels published for all supported
|
||
# (mac/linux/windows × arm64/x86_64) targets.
|
||
"nh3>=0.2",
|
||
# CommonMark markdown renderer for curator-authored marketplace-metadata.json
|
||
# rich content (plugin description / sample_interaction.assistant). Pure
|
||
# Python, no compilation. Rendered output is funneled through nh3 above.
|
||
"markdown-it-py>=3.0",
|
||
# Cross-platform advisory file locking for the `agnes push` single-instance
|
||
# guard. Wraps fcntl.flock on POSIX and msvcrt.locking on Windows behind
|
||
# a uniform API; OS releases the lock automatically on process exit (no
|
||
# stale-lock detection required). Used by cli/lib/push_lock.py.
|
||
"filelock>=3.13,<4",
|
||
# Transitive dependency hardened directly to dodge 5 dependabot advisories
|
||
# (4 high, 1 medium) flagged on urllib3<2.7.0: cross-origin sensitive
|
||
# header leak on proxied low-level redirects, decompression-bomb bypass
|
||
# + unbounded decompression chain on the streaming API, redirects-when-
|
||
# retries-disabled. The `[server]` extra below adds kbcstorage which
|
||
# transitively caps urllib3<2.0.0; `[tool.uv] override-dependencies`
|
||
# forces 2.7+ in workspace installs (Dockerfile + dev). Wheel consumers
|
||
# who install only the CLI (`uv tool install <wheel>`) get no kbcstorage
|
||
# and no conflict.
|
||
"urllib3>=2.7.0",
|
||
]
|
||
|
||
[project.optional-dependencies]
|
||
# Server-side connectors. The CLI wheel does NOT need these — analysts who
|
||
# `uv tool install` the wheel never reach a kbcstorage import. Splitting it
|
||
# out keeps the wheel's METADATA `Requires-Dist` set free of the
|
||
# `kbcstorage<=0.9.5 → urllib3<2.0.0` cap that conflicts with our
|
||
# `urllib3>=2.7.0` security pin under any fresh resolver context (where
|
||
# `[tool.uv] override-dependencies` does NOT apply — see comment on
|
||
# [tool.uv] below). Server install pulls it in via Dockerfile's
|
||
# `uv pip install --system --no-cache .[server]`.
|
||
server = [
|
||
"kbcstorage>=0.9.0",
|
||
]
|
||
observability = [
|
||
# Already in base dependencies — listed here so operators who want to
|
||
# be explicit can `pip install -e ".[observability]"` and signal intent.
|
||
"posthog>=3.7.0",
|
||
]
|
||
dev = [
|
||
"pytest>=9.0.0",
|
||
"pytest-timeout>=2.0.0",
|
||
"pytest-xdist>=3.0.0",
|
||
"faker>=24.0.0",
|
||
# jsonschema validates the corporate-memory extraction-tool golden fixtures
|
||
# under tests/test_corporate_memory_v1.py (extraction.json, correction.json,
|
||
# confidence_calibration.json). Production code does not depend on it.
|
||
"jsonschema>=4.0.0",
|
||
# FastAPI debug toolbar — gated behind DEBUG=1 env var in app/main.py.
|
||
# Provides per-request panels (headers, routes, timer, profiling, etc.)
|
||
# for local development. Never loaded in production (no DEBUG=1 there).
|
||
"fastapi-debug-toolbar>=0.6.3",
|
||
]
|
||
|
||
[project.scripts]
|
||
agnes = "cli.main:main"
|
||
|
||
[build-system]
|
||
requires = ["hatchling"]
|
||
build-backend = "hatchling.build"
|
||
|
||
[tool.hatch.build.targets.wheel]
|
||
packages = ["app", "src", "connectors", "cli", "services", "config"]
|
||
|
||
[tool.ruff]
|
||
line-length = 120
|
||
target-version = "py313"
|
||
|
||
[tool.uv]
|
||
dev-dependencies = [
|
||
"pytest>=9.0.0",
|
||
"pytest-timeout>=2.0.0",
|
||
"pytest-xdist>=3.0.0",
|
||
"faker>=24.0.0",
|
||
"anthropic>=0.30.0",
|
||
"openai>=1.30.0",
|
||
"fastapi-debug-toolbar>=0.6.3",
|
||
]
|
||
# Override the urllib3<2.0.0 ceiling kbcstorage 0.9.5 declares (upstream
|
||
# hasn't relaxed it as of 2026-05-12 but the SDK works fine against
|
||
# urllib3 2.x in practice — we only use `Client` + `Tables` from it and
|
||
# both go through `requests`, which natively supports both lines). Lets
|
||
# the resolver pick a urllib3 line that closes Dependabot advisories
|
||
# CVE-2024-37891 / CVE-2025-{xxx}. See `urllib3>=2.7.0` in [project]
|
||
# dependencies above for the security rationale.
|
||
override-dependencies = ["urllib3>=2.7.0"]
|