Legal database search tool with Files-and-Sources per-message picker
nwhitehouse added a `legal_search` tool fanning out across four open U.S. legal databases, hard-gated behind a per-message source picker in the chat input. When no sources are selected, the tool isn't in the schema at all.
backend/src/lib/legalSearch.ts (270 LOC, new) fans out to CourtListener, GovInfo, Federal Register, and eCFR via Promise.allSettled with 12s per-source timeouts. Sources with missing API keys return zero results with a logged warning rather than failing the whole call. formatLegalResultsForModel() produces markdown links so the model can cite sources inline.
The hard-gate pattern is worth noting: LEGAL_TOOLS is a separate array from the always-on TOOLS. It's only appended to the active tool schema when sources.legal is non-empty. Inside runToolCalls, the model's sources argument is clamped to the user's actual selection - intersection of what the model requested and what the user enabled. If the model tries to widen scope, it's silently constrained. A system-prompt line reinforces the same constraint.
The UI rename is AddDocButton.tsx → FilesAndSourcesButton.tsx, which grows the button into a sectioned popover: existing upload/browse actions, plus a new "U.S. Legal Sources" section with four checkboxes defaulting to off. The architecture is explicitly designed for additional sections to drop in without rework. MikeMessage.sources is per-message transient state, reset after submit.
Two of the four sources require API keys: CourtListener (COURTLISTENER_API_TOKEN) and GovInfo (GOVINFO_API_KEY). Both are free. Federal Register and eCFR are open. All four were smoke-tested against live endpoints.
Spotted something wrong? Or know the PR text has fresher detail than the writeup above?