11 KiB
🔒 Security
- Raw exception variables (
e,exc, etc.) are no longer interpolated directly intologger.exception()messages across all LLM provider, embedding provider, and web search engine paths; each catch site now scrubs the message throughsanitize_error_message()/redact_secrets()before logging, preventing error text from leaking API keys, credentials, or internal URL structure into production logs. Scrubbed exception log lines now include the exception class name so exceptions that stringify to an empty message still produce useful diagnostics. A newcheck-sensitive-loggingpre-commit hook enforces this rule going forward, rejecting direct exception-variable interpolation in the secure-logging directories and flaggingexc_info=Trueonwarning/error/criticalcalls. (#4888) NewsAPIException.to_dict()now redacts credential shapes from the fields it serializes to the client through the two@errorhandler(NewsAPIException)handlers: the human-readablemessagegoes through the centralsanitize_error_for_client(), and string leaves ofdetailsgo throughsanitize_error_message(). This is a defence-in-depth backstop at the response boundary — the primary defence stays at thenews/api.pyraise sites (curated generic messages, #4843) — so a future raise site, subclass, or external caller that lets a credential-bearing string intomessageordetails(e.g. the caller-supplied searchqueryforwarded indetails) has it redacted before it ships. Redaction is credential-shape based (Bearer/Authorization,?api_key=-style params, known token prefixes,http(s)URL userinfo), so legitimate text is unchanged;error_code/status_codeare left untouched. It does not attempt to catch DSN userinfo (postgresql://user:pass@), SQL, or filesystem paths — those remain the responsibility of the raise-site discipline. (#4931)- The central credential sanitizer (
sanitize_error_message/sanitize_error_for_client) now redacts URL userinfo credentials in any scheme (case-insensitive), not justhttp(s). Raw URL-form database connection errors — SQLAlchemydialect+driverDSNs (postgresql+psycopg2://user:pass@host,mysql+pymysql://…,mongodb+srv://…), plain DB schemes, and password-only DSNs (redis://:pass@host) — previously leaked their password through any surfaced exception message; the password is now replaced with[REDACTED]. Credential-less DSNs with a port (postgresql://host:5432/db) and all existinghttp(s)behavior are unchanged. Key=value DSNs (e.g. pyodbcServer=...;Pwd=...) have no://and remain out of scope for a userinfo regex. This benefits every consumer of the central sanitizer (download service, fetch tool, agent strategies, search engines, and the news error-response backstop). (#4933) WebAPIException.to_dict()now redacts credential shapes from the fields it serializes to the client (messageviasanitize_error_for_client(),detailsstring leaves viasanitize_error_message()), mirroring theNewsAPIExceptionbackstop. This is a defence-in-depth measure at the response boundary so that a future raise site, subclass, or external caller that lets a credential-bearing string intomessageordetailshas it redacted before it ships. Legitimate text is unchanged, andstatus/error_code/status_codeare left untouched. (#4937)- The egress guardrail now enforces a two-axis (data-sensitivity × destination-exposure) admissibility rule at the start of a research run (ADR-0007): the run is refused when a sensitive source (a private collection/library/store) would reach an exposing sink (a public search engine or a cloud LLM/embeddings provider), in addition to the existing scope checks. Override by setting Egress Scope to Unprotected, marking the collection public, or trusting the destination.
✨ New Features
- New Zotero integration: import a Zotero library or collection into your document library, with optional background auto-sync. Add your Zotero API key, library type/ID and (optionally) a collection key under Settings → Zotero, then use the new Library → Zotero page to test the connection, list collections, and Sync now. Imported papers are stored, text-extracted and RAG-indexed like any other library document, so they become searchable during research via the collection search engine — no separate search engine to configure. Sync is incremental (new, changed and removed items are reconciled), and items without an attached PDF are imported as metadata/abstract text by default (toggle off to import PDFs only). Enable Auto-Sync to refresh in the background on a configurable interval. (#4723)
- Zotero integration polish from first real-world testing: standalone PDF attachments (PDFs added without a parent item) now import as documents; the library ID is auto-detected from the API key (usernames resolve too); manual syncs re-examine previously skipped items; imports appear in the main Library view; a live progress bar tracks syncs; the config page autosaves with essentials-first layout and runs the connection test on saving a key; friendlier defaults (text-only PDF storage, integration enabled once a key is set) and actionable error messages. (#4960)
- Add a per-category entry counter next to each filter button in the Research Logs panel (All, Milestones, Info, Warning, Errors). Counts reflect entries currently rendered in the DOM and update on insert, prune, and batch load, so users can see at a glance which categories still have entries when the global DOM cap is hit.
- Add an Unprotected egress scope — an explicit opt-out that disables the egress-scope restrictions for a run (the hard SSRF and cloud-metadata blocks still apply), surfaced with a light-red panel and a non-dismissible "protection disabled" banner. The rarely-used Both scope is retired: it is removed from the selector, and existing saved
bothvalues are migrated to Adaptive (a residual value is also coerced to Adaptive at read time). If you relied onbothto run a private collection with a cloud model, mark that collection public or choose Unprotected. - Add per-destination trust to the egress model (ADR-0007):
policy.trusted_inference_providersandpolicy.trusted_search_engineslet you mark specific off-machine LLM/embeddings providers or search engines as trusted, so the two-axis classification treats them as contained (e.g. a zero-retention enterprise Anthropic endpoint, or a self-hosted Elasticsearch/Paperless on a public hostname). A banner surfaces active trust entries.
🐛 Bug Fixes
-
Registration no longer leaves a bricked account behind: if creating the encrypted user database fails after the auth row is committed, both the orphaned auth row and any partial on-disk database files (including the per-database salt) are now cleaned up, so the username is genuinely reusable on retry. (#4934)
-
The settings write API now rejects malformed keys (a trailing dot like
local_search_chunk_size., a leading dot, an empty..segment, or blank/whitespace) with HTTP 400 instead of silently persisting them. Such rows madeget_setting()return a{"": value}wrapper dict that rendered as[object Object]on settings pages (#4840). The manager (set_setting,create_or_update_setting) also refuses to create malformed keys, andimport_settingsskips them so a corrupted export can't reintroduce them. Complements the read-side fix in #4852. (#4935) -
Registration now recovers from an orphaned per-database salt file (left with no matching database by a prior interrupted registration), so a username that was previously stuck as un-registerable can be registered again. (#4942)
-
Both settings prefix-read paths —
SettingsManager.get_setting(DB) andget_setting_from_snapshot(the snapshot path used by research threads) — now filter out malformed rows (e.g. a legacyfoo..orfoo.key), so a stray malformed row can no longer turn a leaf read into a{".": value}wrapper dict that renders as[object Object]. Completes the read side of #4840 beyond #4852's exact single-trailing-dot exclusion; existing malformed rows remain harmless and are not deleted. (#4946) -
Fix the 'Info' filter button in the Research Logs panel being a silent no-op. Clicking it left the panel showing every entry (warnings, errors, milestones) because the visibility helper returned true for all log types when the active filter was 'info'. The filter now narrows to info entries only, matching the button's label.
-
Fixed a 30–50 second visible lag between clicking Stop and a focused-iteration research actually stopping.
FocusedIterationStrategy.analyze_topicnow callsself.check_termination()at the start of every iteration, after the parallel search (before optional verification searches), and just before the final synthesis LLM call, so a cancellation request is detected within the current iteration rather than only at the next iteration's progress emit. -
Fixes a subtle data-loss bug in the Research Logs panel: identical warning, error, and milestone entries were being collapsed into a single
(N×)counter, the same way repetitive info entries like "Status: OK" already were. Collapsing diagnostic entries like that strips the recency signal — after the second identical error you can no longer tell when the last failure happened, and repeated retries look like a single stuck event instead of forward progress.The content-dedup scan now applies only to
infoentries (where collapsing repetitive noise is useful). Warning / error / milestone entries always render. The id-based dedup earlier in the pipeline still catches exact retransmits with the same id, so the bypass is strictly about content-similar-but-distinct-events getting the same timestamp.
📝 Other Changes
- Fixed broken copy-paste examples in the README and docs (benchmark CLI
module path,
rate_limiting reset --engine, nonexistentopenclawsearch engine) plus two dead external links, and added a pre-commit check that keeps README links, anchors, and examples in sync with the codebase. (#4950)