Changelog
What's new in SourceVault. Everything below runs on your own hardware, with local models and no source-code egress.
2026-08-12, v1.29.0: Sessions you can revoke, and retrieval that stays put
Security
- Logging out now ends the session. Dashboard sessions were stateless self-signed tokens with no revocation, so
DELETE /api/dashboard/session only cleared the caller's cookie and a captured value stayed usable until it expired, up to 12 hours later. The only real invalidation was rotating DASHBOARD_TOKEN, which signs everyone out. Logout now records the session nonce in a persisted ledger that verification consults, pruned as entries expire and capped by dropping the ones nearest natural expiry first.
- The authentication throttle covered one door of two. The limiter guarded
POST /session while every other /api/dashboard/* route authenticated the same token through a bearer header with no limiter at all. A gate limiter now sits ahead of dashboard auth and counts only unauthenticated requests, so operator polling is never throttled.
- Self-hosted provider hosts are checked against the git-host allowlist. The instance host arrived in the request body and was only checked for being parseable, with nothing blocking RFC1918, link-local, or the cloud metadata address. The server then fetched
https://<host>/..., and a token refresh would POST a stored refresh_token to whatever host the connection carried. The clone path already enforced the allowlist; these HTTP paths now do too.
- A guessable webhook secret is called out at boot. Unlike the generated
DASHBOARD_TOKEN, SOURCEVAULT_WEBHOOK_SECRET is operator-chosen and compared against an attacker-supplied header, and a correct guess triggers a repository reindex. The throttle on that endpoint is deliberately loose at 30 requests per 5 minutes, which caps a guessing run without stopping it. Entropy is scored over the alphabet the secret actually uses, so padding a short secret to length does not earn a pass. It warns rather than refuses, because the endpoint is optional and refusing would take search, MCP, and the dashboard down on upgrade.
- Release tokens no longer sit in a checkout's git config. A credential in a remote URL is written verbatim into
.git/config for the life of the job, and for the tap clone that is a token able to repoint what every customer brew-installs, left on a self-hosted runner. The tap clone, tap push, and tag push all pass it through http.extraHeader instead.
- The shipped runtime templates run SourceVault with fewer privileges. Indexing shells out to git against repository URLs a caller can influence, and neither the systemd unit nor
compose.yaml did any of the cheap kernel-side limiting around that. deploy/systemd/sourcevault.service now sets NoNewPrivileges, a private /tmp, a read-only home with an explicit writable list, and SystemCallFilter=@system-service. All three compose services drop every capability and set no-new-privileges. Directives that could not be verified against a live deployment, a read-only container rootfs among them, ship commented out with the checks they need first.
Fixed
- Indexing another repository no longer changes the results you get in the ones you already had. The keyword half of search filtered rows to a single repository but ranked them with
bm25() statistics gathered over every indexed repository, so term weights moved as unrelated code arrived: on the express fixture, file-hit@5 fell from 0.833 to 0.800 once flask was indexed beside it, with express's own index, questions, and query untouched. Each repository now gets its own full-text table, so a five-repository install retrieves the way a one-repository install does. Re-indexing a changed file used to leave the old version's terms behind in that index as well, inflating the same statistics and taking a slot out of every result set. Existing installs rebuild from text already on disk the first time they open the database, with no re-indexing and no re-embedding.
- Citations are checked against the sources the answer was given. Nothing validated generated
[S#] markers, so a model citing [S7] when it had five sources shipped a dead citation to the dashboard and misattributed grounding to MCP clients. The benchmark's grounded metric was satisfied by a single hallucinated marker, so the one number meant to police citation discipline was blind to the most common citation failure.
- Answers no longer spend part of their context budget on text the model has already been shown. When several chunks of one file went into the same block, they were joined as they were stored, so the roughly 200 characters (
CODE_CHUNK_OVERLAP) that chunking repeats at each seam appeared twice, right where the retrieval hit is, and the copy counted against CODE_ASK_CONTEXT_CHARS and pushed out blocks holding something different. Blocks are now rebuilt from line ranges, so the overlap that exists for embedding stops there and reaches neither the prompt nor the [S#] sources shown with the answer.
- A relayed result is marked delivered only once Hermes has it. The entry was written before the delivery was awaited, so a failed delivery left it claiming success. The 500 told OpenClaw to retry, the retry was answered "already forwarded" and suppressed, and the result was dropped with an info-level line to show for it.
- MCP clients are no longer told their results passed a gate that is switched off. The server advertised Sentinel policy and DLP filtering on every read no matter what the install enforced, and Sentinel is licensed and off by default. A client acting on that could forward content believing it had been scrubbed. The instructions now say which of the two is true, and
sourcevault://policy reports live enforcement state.
- Opening a citation from an Ask that covered several repositories now loads the file. The drawer asked the server for the file under the repository name of the view you clicked in, and for a multi-repository Ask that name is the comma-joined list of every repository in the question, so the read failed and you got the retrieved snippet with an error instead of the file. A citation now carries the repository its block came from. Asks against a single repository were never affected.
- Opening a citation while another is still loading no longer shows the wrong file. Clicking a second citation before the first arrived left whichever response landed last in control, so the drawer could show one citation's title and line highlighting above another citation's contents. The pending request is now cancelled when you move to a new citation.
- Reading the audit log no longer stalls the server. Every read loaded the whole file synchronously and split it into lines before keeping the last 200 records, or 2000 for the console view, so a month of activity blocked the event loop on each call. Reads now take the tail directly. The chain verdict, which is the expensive part, is cached for 30 seconds; set
SOURCEVAULT_AUDIT_CACHE_MS=0 to turn that off.
- Licence verification is cached for 5 seconds instead of running on every request. It walked up to three trial directories and verified a signature each time. Installing a key clears the cache immediately, so an upgrade takes effect at once, and 0 disables the cache.
- The trial banner's links can be reached from the keyboard. Three prompts in the expiry banner were anchors with no
href, so they took no focus and a keyboard or screen-reader user could not follow them to the licence form. They are buttons now. The licence form itself was always reachable through the sidebar Settings button. Archived interactions and repository cards also gained explicit Move up and Move down actions, in the archive row's action area and the repository card's overflow menu, so reordering no longer needs a drag.
- A repository whose name contains a comma no longer corrupts the selection. The dashboard held the repo selection as one string meaning three different things: a name, a comma-joined list, or the "*" sentinel. Every consumer re-derived the shape by hand, and a comma in a name broke the selection and every request built from it.
- Destructive confirmations follow your theme. They rendered with default tokens, so a delete dialog popped up light over a dark interface.
- A capacity that is not a number no longer switches off the limit it sets.
TASK_HISTORY_CAPACITY=5k became NaN, every size check against it was false, and the task-relay history grew for as long as the process ran. An unreadable value now falls back to 5000.
- The Docker image no longer carries the application twice. Ownership is set as files are copied rather than by a recursive chown afterwards, which had written the whole tree into a second layer. The build context also excludes nested
node_modules directories that a root-anchored pattern was missing, including the roughly 27 MB under scripts/demo.
npm run tune no longer recommends knob settings its question set cannot support. The sweep scored nine combinations on the same questions it then picked from, crowned the highest scorer, and printed it as .env lines to paste into production, but on a 30-question set one flipped question moves file-hit by three points, so the winner was often noise. Each combination is now compared against your current settings with the exact McNemar test the nightly quality gate already used, Holm-corrected across the grid, and has to survive dropping any single question. When nothing clears that, the report says so and keeps the defaults.
- Benchmark precision no longer scores questions that have no expected files. A question matched only on answer text scored a flat zero, pulling the reported average down with a number that measured nothing. Those are unscored now, the same way file-hit already was. Zero cited files where files were expected still scores zero, because that is a real miss.
Changed
search_codebase over MCP returns a compact result by default. Every reply carried the full chunk text, and the same payload was repeated as pretty-printed JSON in the text block and again in structuredContent. Content is now opt-in with include_content: true, matching the service default, which has always been off. The MCP projection also stops carrying chunk, repoName, fileBasename, directory and extension; file, preview, the line range, language, kind, distance and matchTerms all survive. Callers that reach /api/search-codebase over HTTP, the Hermes plugin among them, are unaffected.
- The installer pins the vector store to the version compose already pinned. It started Chroma from a mutable tag, so an upstream release could change what a customer runs between two installs of the same SourceVault. Linux pulls the pinned tag and digest, macOS installs the matching Python package, and Renovate moves all of them with the compose pin so they cannot drift.
- The signed task relay is supported rather than experimental. It is deliberately single-shot, which is a design decision rather than a caveat.
docs/RELAY.md gains the signing recipe it was missing: which routes accept the current scheme and why, the optional v2 form, the outbound webhook signing that runs the other way, and a worked example to check an implementation against.
Upgrade notes
- Connecting a self-hosted provider now requires its host to be listed in
SOURCEVAULT_ALLOWED_GIT_HOSTS, the same setting git operations already honoured. Connections made before this release keep working; a new connection to a host that is not listed is refused, and the error names the setting to add it to.
- An MCP client that read
chunk from a search_codebase result should now pass include_content: true. One that read repoName, fileBasename, directory or extension has no flag to restore them: they are gone from the MCP projection, and file is what remains to derive a path from.
- The privilege reduction is in the shipped templates, so an install already running
deploy/systemd/sourcevault.service needs the new copy and a systemctl --user daemon-reload. Create the writable directories before the first start, mkdir -p ~/.sourcevault/repos and your state dir, because the read-only home will not let the app create them. On a kernel with unprivileged user namespaces switched off the unit fails with status=226/NAMESPACE; comment out PrivateTmp=, ProtectHome= and ReadWritePaths= and keep the rest. An install that mirrors repos over SSH through an agent socket under /tmp also has to drop PrivateTmp=, since the private /tmp hides that socket. For compose, recreate the containers to pick up the new security_opt and cap_drop settings.
- If
CODE_RRF_BASE_WEIGHT or CODE_FETCH_MULTIPLIER is set in your environment because an earlier npm run tune printed it, re-run the tuner. The old report handed you the top scorer on that question set whether or not the set could tell it apart from the defaults, and the new one says outright when to keep your values and when to drop back to 60 and 8.
- If
SOURCEVAULT_WEBHOOK_SECRET is set to something short or low-entropy, this release logs a warning at boot. Nothing stops working; generate a longer random value when convenient. Installs that do not use webhook reindexing are unaffected.
2026-08-10, v1.28.0: A faster dashboard, and signed requests that expire
Fixed
- The dashboard no longer stutters while you type. Every keystroke in the ask box rewrote the entire cached view (status, repositories, history, archive, and every open result tab) to browser storage, on the same thread drawing the screen. It now saves only when something worth saving actually changed, and never more than once per half second. During an index run this was happening continuously in the background as well.
- Search got faster on large repositories. A query mentioning anything code-shaped pulled every chunk of the repository into memory and scored all of them, which on a hundred-thousand-chunk repository meant hundreds of megabytes of work per search. It now asks the index for the candidates it actually needs. Results are unchanged: the ranking runs over the same rows it would have found by reading everything.
- Ordinary questions no longer take the slow path. "How does the sendFile function work?" counted as a code query because it contains a parenthesis and the word *function*, so it paid for exact-match scanning it could not use. That test now looks for code shape rather than code vocabulary: a call, an arrow, a declaration.
- The repository list stopped being the slowest thing the dashboard does. It ran up to six git commands per repository one after another, counted indexed chunks by fetching every chunk's id, and worked out each repository's file types by fetching every chunk's metadata. Counting is now a count, file types are recorded when the index is built, and repositories are inspected several at a time.
- Changing the embedding model no longer corrupts an index in silence. Switching models left the stored vectors in the old model's space and every new search in the new one, and an incremental reindex would mix both into a single index with no way to tell afterwards. SourceVault now records which model built an index, rebuilds it when the model changes, and refuses to search across a mismatch instead of returning quietly wrong results.
- Git history indexing can no longer wedge. If an indexing run was interrupted partway through writing commits, every later run retried the same range and failed on the commits already stored, so history stayed stuck until a full rebuild. Repeat writes are now harmless.
- A delegated task is no longer re-run when only the reply fails. If OpenClaw finished the work but the completion could not be delivered to Hermes, SourceVault forgot the task had run, so Hermes' retry executed all of it again, once per attempt, for as long as the outage lasted.
- Provider sign-ins survive a crash or a coincidence. Connections, sources, settings, and repository order were each written by overwriting the file in place with no lock, so an interrupted write could leave unreadable JSON that read back as *no connections at all*, and two writes at once could lose one of them, including a freshly refreshed GitLab token, which ends that connection permanently. These files are now written atomically and changed one writer at a time, and an unreadable one is reported rather than silently treated as empty.
- In lite mode, the dashboard stopped reporting a server error for a model server it is designed to run without. The models list now comes back empty and says why.
Security
- Machine API authentication could be silently disabled. On installs that deliver secrets through systemd credentials (the hardening path the deployment guide recommends), the code-search, read-file, and history-search endpoints read their signing secret before those credentials were loaded, so they came up with no secret at all. On a loopback bind that means unsigned requests for repository contents were accepted, while the startup log reported authentication as enabled. Installs that pass the secret through the environment or a
.env file were never affected.
- Signed requests can no longer be replayed. A signature covered only the request body, so a captured request stayed valid forever: anyone able to observe traffic on a plaintext deployment could repeat a read-file request indefinitely and keep receiving the current contents of that file. Signatures now cover a timestamp and a one-time value, are rejected outside a five-minute window, and cannot be replayed inside it.
- The machine API now requires the new signature form and refuses the old one. See the upgrade notes.
- The nightly search-quality check can no longer pass while search is broken. It compared only the questions that returned an answer, so a run where every question failed compared nothing, found no regression, and reported success. Errors, and a comparison that covers too little of the baseline, now fail it.
2026-08-10, v1.27.0: Sign in to GitLab without pasting a token
Added
- Sign in to GitLab by approving SourceVault at your GitLab instance instead of pasting a personal access token, the same way GitHub sign-in works. Works with gitlab.com and self-hosted instances; needs GitLab 17.9+ (17.3 behind a feature flag). Unlike GitHub (whose OAuth scopes grant write alongside read), GitLab's ask for read access and nothing else, so this gives up no privilege compared with a token. (#239)
- GitLab tokens expire after about two hours; SourceVault refreshes them in the background as part of the git operation that needs them, so a connection that worked this morning still works this afternoon with nothing to re-approve. (#239)
- Repositories now show which store holds their index, and anything still on the old shared store is tagged reindex pending. The move to per-repo stores in v1.25.0 was invisible until now: you were told to schedule a reindex without being told which repositories needed one. (#238)
Fixed
- A dashboard exposed over HTTPS could be advertised at a URL nobody can open: binding to all interfaces (
HOST=0.0.0.0) put that literal address into the startup link. It now advertises localhost, and warns when TLS is on and SOURCEVAULT_URL is unset — the case where your certificate names something else. (#241)
Security
- Updated a dashboard build dependency (
nanoid) past a high-severity advisory, GHSA-2v37-7h3g-55p8. It is a build tool, not something SourceVault runs or exposes, so no deployment was at risk. (#243)
2026-08-07, v1.26.0: Sign in to GitHub without pasting a token
Added
- You can now sign in to GitHub by approving SourceVault at
github.com/login/device instead of creating and pasting a personal access token. The dashboard shows a short code, you enter it at GitHub, and the sign-in dialog closes itself once GitHub confirms. Nothing is relayed through a SourceVault server (your browser talks to GitHub, and your own machine exchanges the approval for a token), so this changes how you sign in without changing where anything is stored or who can see it. (#235)
- Settings → Source control now shows how each connection was signed in, so it is clear which logins you can revoke at the provider and which are a token you hold. (#235)
2026-08-07, v1.25.1: Fixes a v1.25.0 reindex that emptied a repository's index
Fixed
- Reindexing a repository after upgrading to v1.25.0 could leave it with no index at all. v1.25.0 moved each repository into its own store, but an incremental reindex decides what work to do from a record of what it indexed last time — and that record described the *previous* store. Every file looked unchanged, so nothing was written to the new store, and the repository then reported zero indexed chunks. Moving a repository to its own store is now always a full index; later reindexes are incremental as before. (#233)
2026-08-07, v1.25.0: Large repositories stop breaking search
Fixed
- Search against a large repository no longer fails outright. Every repository's chunks lived in one store, and a search narrowed to a repository with a metadata filter; past roughly 121,000 chunks that filter exceeded a limit inside the store and the query returned an error instead of results. It did not get slow; it stopped working, and because every repository shares that store today, one large monorepo was enough to break every search against it. Each repository now has its own store, so no filter is needed and the limit is never reached. Deleting a repository is faster for the same reason. (#231)
- An identifier-shaped query no longer downloads the whole repository to answer. The exact-match half of a search fetched every chunk from the store on each query: invisible on a small repository, and hundreds of megabytes per question on a large one. It now reads the local index that already holds the same rows. (#230)
2026-08-07, v1.24.0: Locking keeps your place, and an edited repository stops nagging
Changed
- Locking the dashboard no longer discards what you were working on. Unlocking puts you back where you were. Locking used to clear the source drawer's snippet conversations, which read like a privacy measure but was not one: the search results beside them kept their code in the browser's saved state either way, so the same source remained and the only reliable effect was losing your place. Locking ends the session and leaves the browser's stored state in place. If you need it gone, clear the browser's site data — and say so, because a built-in control for it is a reasonable thing to want. (#226)
Fixed
- A repository with uncommitted changes no longer reports itself out of date forever. It was marked stale for having a dirty working tree, which reindexing could never clear, because indexing does not commit anything. The dashboard offered a remedy that could not reach the state. Uncommitted files are indexed like any others, so a repository indexed after its edits is current and now says so. It reports out of date when files have actually changed since the last index, and reindexing clears it. (#227)
- Errors from a repository action stay on the Repositories page. A message such as "Too many reindex requests" used to appear above every view, including pages with nothing to reindex. (#228)
- Choosing a repository in the sidebar now shows that repository, opened, with its details. It previously switched to the Repositories page and listed all of them, leaving you to find the one you had just clicked. (#228)
2026-08-07, v1.23.0: SourceVault can serve its own HTTPS
Added
- SourceVault can now terminate TLS itself, without a reverse proxy in front. Point
SOURCEVAULT_TLS_CERT and SOURCEVAULT_TLS_KEY at a certificate and key and it serves HTTPS directly. This is for two situations a proxy handles badly: a machine on an internal network whose certificates come from a corporate authority, and an air-gapped install, where it adds nothing whatsoever to the list of outbound connections. No authority is contacted and no revocation is checked. For anything facing the internet a reverse proxy remains the better answer, and the existing recipes still apply. (#224)
- Certificates can be renewed without a restart: send the process
SIGHUP and it re-reads them from the same paths, without dropping connections. If the new files are unusable the previous certificate keeps serving and the log says why. (#224)
- Optional companions to the above:
SOURCEVAULT_HTTP_REDIRECT_PORT runs a small plaintext listener that redirects to the HTTPS port, so anyone who types the bare address arrives somewhere instead of watching the browser hang; and SOURCEVAULT_HSTS sends HSTS on a real hostname. HSTS is refused on local addresses no matter how it is set: it applies to a hostname and ignores the port, so sending it from a local instance would force HTTPS on every other development server sharing that name in the same browser. (#224)
Security
- The dashboard no longer marks its cookies
Secure on the strength of an X-Forwarded-Proto header alone. The header was believed whether or not SOURCEVAULT_TRUST_PROXY said a proxy could be trusted, which is the setting that exists to answer exactly that question, and is off by default because anyone can send the header. Exposure was limited: a forged header affected only the sender's own session. (#223)
2026-08-07, v1.22.0: Deselecting a repository stops searching it
Fixed
- Removing a repository from the search bar no longer leaves it selected. The dashboard reapplied a default repository on every background refresh, and could not tell "cleared" from "not chosen yet", so a repository you had just removed quietly came back within seconds, and questions kept being answered from it. (#221)
- Removing a repository from the search bar now clears the results it retrieved. They used to stay on screen after the repository was gone from the search bar, and a question grounded in those results still answered from them. (#221)
- Deleting a repository now closes the results still showing its code. Every citation in them pointed at a file that no longer existed, and because results are remembered per repository and query, they also survived deleting and re-adding a repository of the same name: stale code shown beside a freshly built index. (#221)
2026-08-07, v1.21.0: Settings becomes a page
Changed
- Settings now opens as a full page. Seven unrelated groups (appearance, features, licensing, connected sources, storage backend, version, and token rotation) shared one scrolling column inside a fixed dialog, so reaching any of them meant scrolling past the others. Categories are tabs now: one group at a time, each short enough to read without scrolling. On a phone the categories move to the top and each setting stacks its label above its control. (#219)
Fixed
- The licence is described once instead of once per paid feature. Four rows each repeated who the licence belonged to, which said the same thing four times and read like a list of receipts. Plan & licence now names the plan, the licence holder, and how many sources the plan allows, in one place; each feature says only whether it is available. (#219)
- Prompts offering to take you somewhere to enter a licence key now open the licence section directly, rather than leaving you to find it. (#219)
2026-08-07, v1.20.0: Three more themes, and a drawer that stays where you put it
Added
- Terracotta now has a dark variant, carrying the warm palette onto low-light surfaces with brown-leaning near-blacks rather than the grey of the default dark theme. (#215, #217)
- A Tropical Forest theme, in light and dark. Neutral slate structure and a forest-green brand, with status colours designed as part of the theme: distinct green, amber, red, and teal for passing checks, pending review, urgent violations, and live data. It is built for sessions spent watching policy and audit state, where the reason to look at the screen is usually that something needs attention; the structure stays quiet so those colours carry the signal. (#215, #217)
Fixed
- The navigation drawer no longer reopens collapsed on a wide screen. Narrowing the window collapsed the drawer and recorded that as a deliberate preference, so a single visit in a small window left it collapsed on every later visit at any size. Resizing is treated as what it is, a reaction to the window, and only collapsing it yourself is remembered. Because a preference stored before this fix cannot be distinguished from one the window imposed, it is cleared once and the drawer opens until you choose otherwise. (#217)
Changed
- Repository status dots take their colours from the active theme. They were fixed values no theme could speak for; every existing theme keeps exactly the colours it had. (#215)
- The expanded drawer's footer is centred, with its action icons spaced evenly across the width instead of bunched against the left edge. (#217)
2026-08-07, v1.19.0: Choose how the dashboard looks
Added
- You can now choose the dashboard theme. Settings offers Match system (the default, which follows the operating system as before) alongside Light, Dark, and a new Terracotta palette. (#212)
- Terracotta is a warm, minimalist alternative to the default: a terracotta accent on cream, bordered in soft taupe, in place of the indigo-on-white that most developer tools settle for. The accent is a deeper terracotta than the palette's surfaces because it has to carry button text at the contrast ratio accessibility guidelines ask for; the softer shades that read better as a background do not clear it. (#212)
Security
- Updated js-yaml to 4.3.1, which carries the fix for CVE-2026-59870, a quadratic slowdown when resolving a crafted
!!omap value. It reaches installations through the OpenAPI specification generator rather than anything that reads untrusted input, so exposure was limited, but the advisory is rated high and the update costs nothing. (#213)
Fixed
- A dashboard on a machine set to dark mode, with no theme chosen, painted its first frame in light colours before correcting itself. The startup path only recognised an explicitly saved dark preference, so it disagreed with the rest of the app about what "no choice yet" meant. (#212)
2026-08-06, v1.18.0: The dashboard notices every index
Added
- The Repositories view now learns about indexing within five seconds, no matter what started it. Indexing triggered by a webhook push, resumed after a restart, begun in another browser tab, or already running before a reload previously reached the header only through the repository list, which refreshes once every forty-five seconds — long enough for a short index to begin and finish entirely unseen. A dedicated endpoint now reports the running jobs and nothing else, cheap enough to ask often. (#210)
Fixed
- The header could claim the corpus was up to date while an index was running. It described only the work the open tab had started itself, so an index begun anywhere else left it reporting a settled chunk count and "up to date" beside a repository card that was visibly still working. (e143dde)
- Indexing several repositories at once reported a misleading percentage. A repository whose file count was still being counted contributed nothing at all, so a second repository added mid-run stayed invisible and the figure tracked only the first, then fell sharply the moment the new work was counted. The percentage is now withheld until every running job has been measured, and the caption names the number of repositories so a pooled figure is not mistaken for one repository's progress. (e143dde)
- Adding a repository held the dialog open for the entire first index. The clone itself finishes in about a second, but the dialog stayed until the indexing that followed had completed, leaving the app unusable for minutes on a large repository. The dialog now closes as soon as the clone lands and indexing continues in the background, tracked on the repository card. (#206)
2026-08-06, v1.17.0: Reindexing only pays for what changed
Changed
- Incremental reindexing no longer reads a file to discover it has not changed. Size and modification time are recorded and compared first, so an unchanged file costs a stat instead of a full read and hash. On a repository carrying large committed artefacts this is the difference between minutes and moments: a reindex with nothing to do measured 375 seconds before and finishes instantly after. The first pass following the upgrade still reads everything once to record those details. (#204)
- Files larger than 2 MB are now excluded from the index. Generated bundles, vendored blobs, and committed fixtures cannot produce useful search results, and reading them on every pass was pure cost: one repository spent every reindex re-reading 34 MB of benchmark fixtures that failed to embed each time. The count is reported at the end of a run, and
SOURCEVAULT_INDEX_MAX_FILE_BYTES changes or lifts the limit. (#204)
Fixed
- A repository whose upstream history was rewritten (a force-push) could sit forever on a commit the remote no longer contains, reporting itself freshly indexed against code nobody could fetch. Sync now realigns the local mirror onto the remote branch, and a repository stranded that way reads as stale with an explanation instead of ready. (#202)
2026-08-05, v1.16.1: Secret redaction covers compound key names
Fixed
- Sentinel's secret scrubbing missed credentials stored under compound names. A value assigned to
secret: was redacted, but the same value under secretAccessKey:, secret_access_key =, or AWS_SECRET_ACCESS_KEY= was not, so an AWS secret access key could reach an agent in the clear while the access-key ID beside it was correctly redacted. The rule now matches any name containing a secret-shaped word. Ordinary identifiers and low-entropy values are unaffected. (#198)
2026-08-05, v1.16.0: Indexing that can't spin forever
Added
- A stop button on every repository card while an index runs: canceling stops the indexer immediately and records "Canceled by operator", so you no longer wait out a run you didn't mean to start. (#196)
- A stall watchdog behind every index job: if the indexer reports no progress for ten minutes (tunable via
SOURCEVAULT_INDEX_STALL_MS), it is stopped and the job reports exactly that, with what to do next, instead of showing an eternal spinner. "Slow," "stuck," and "silently dead" are now three different, visible things. (#196)
Fixed
- An index interrupted by a restart could leave a phantom "running" job that nothing was actually running, spinning in the dashboard forever. Interrupted jobs now resume cleanly (flagged as resumed), and any record that cannot be resumed is swept instead of surviving as a ghost. (#196)
2026-08-05, v1.15.0: Any git host, and sign-in that meets you at the door
Added
- Self-hosted GitLab: the GitLab sign-in now takes an optional instance host. Leave it blank for gitlab.com, or point it at your own installation (same API, same token scopes). Credentials always route to the instance you connected, never to gitlab.com by accident. (#191)
- Custom remotes: a new provider for any other git server reachable over HTTPS (Azure DevOps, Gerrit, cgit, an internal mirror). Enter the host, username, and the password or token the server expects for git, and private repositories clone and stay in sync. There is no repository autocomplete for custom remotes (no common API exists); paste clone URLs, and the first clone verifies the credentials. (#191)
- Settings has a new Source control section: every provider with its connection status, and sign in / disconnect right there, so you no longer have to hunt for sign-in inside the Add Repository dialog. (#191)
Changed
- A fresh install now leads with what to do: with no repositories yet, the Ask view opens with clone / add-local buttons instead of a search box over nothing, and the Add Repository dialog defaults to whichever source you are signed into. (#191)
2026-08-05, v1.14.0: Self-hosted Forgejo and Gitea join the provider list
Added
- Forgejo is now a first-class source-control provider alongside GitHub, GitLab, and Bitbucket; and because it speaks the same v1 API, Gitea works identically. Pick Forgejo in the Add Repository dialog, enter your instance's host and an access token (Settings → Applications on your instance), and private repositories autocomplete, clone, and stay in sync through the same credential helper as the hosted platforms. Pasting a clone URL from your connected instance selects the provider automatically. (#185)
Fixed
- An expired provider login (for example a revoked GitHub token) no longer logs you out of the dashboard: opening the clone dialog against a dead connection used to force a trip through the login gate on every attempt. Upstream credential failures now say what they are (check the named provider's token) and leave your dashboard session alone. The same applies to clone attempts against repositories that need authentication. (#184)
- The login gate now says why you are seeing it: if your 12-hour session expired it says so (instead of presenting as a fresh install), and hitting the login rate limit reports "too many attempts" rather than a wrong-token error. (#181)
2026-08-05, v1.13.2: Dashboard stops failing silently on vanished history
Fixed
- Archiving, deleting, pinning, or rating a history item whose server record no longer exists (a cleared history, or a delete from another window) failed with a silent 404: the dashboard kept showing the item (usually a result tab restored from the browser's saved snapshot) and nothing explained why the click did nothing. The dashboard now closes any tab still showing the vanished conversation and says so in a dismissible banner; other action failures are named instead of being dropped. Deleting an interaction also closes its open tab. (#179)
- Collapsed sidebar: the bottom controls (model, health, settings, lock, collapse) now line up in one centered column with the menu icons above, and the collapsed rail no longer shows a scrollbar that pushed its icons off-center. (#179)
2026-08-05, v1.13.1: Fresh benchmark numbers and self-announcing releases
Added
- Releases now announce themselves: after a release tags, the pipeline posts the changelog entry to the community Discord's
#announcements and regenerates sourcevault.ai/changelog from this file, so what you are reading is also the announcement. (This entry is the pipeline's first end-to-end run.) (#177)
Changed
- BENCHMARK.md carries a fresh five-corpus results table (2026-08-04, qwen3.5:9b on Apple M4 Pro/24 GB): express 0.900 file-hit / 1.000 answer correctness, flask 0.960 / 1.000, ripgrep 0.800 / 0.920, zod 0.550 / 0.900; every corpus at or above its search-only baseline on the compact model. The cross-encoder reranker's benefit is now measured: citation precision +0.04 (95% CI [0.01, 0.071], significant); its file-hit effect stays within noise. The kubernetes column is marked blocked on two filed scale issues (#174, #175) rather than populated or removed. (#176)
2026-08-04, v1.13.0: Lite mode runs without Ollama or a GPU
Added
- Lite mode:
SOURCEVAULT_LITE=1 runs SourceVault with no Ollama and no ChromaDB (no GPU, no 24 GB floor). Indexing builds the keyword (FTS5) index, symbol graph, and repo map; search runs the lexical + literal legs through the same fusion and ranking pipeline (including the local CPU cross-encoder); file reads, the dashboard, MCP tools/resources, Sentinel, and the audit chain work unchanged. Ask and semantic history search decline with a clear message instead of degrading silently, and npm run doctor offers lite mode when the services are missing. Unset the flag and reindex to upgrade the same install to the full stack. See README "Lite mode". (#170)
- The nightly retrieval gate now covers four corpora as a fail-fast-off matrix: express, zod, flask, and ripgrep (JavaScript/TypeScript/Python/Rust). Each corpus arms itself independently from committed embedding fixtures and baselines. kubernetes deliberately stays a manual benchmark (its fixture would be hundreds of MB of committed JSON); the decision is recorded in BENCHMARK.md. A babysitter script (
scripts/ops/check-retrieval-gate.js) reports per-corpus results and the gate's consecutive-green-day streak. (#169, #172)
- An agent benchmark harness (
npm run bench:agent): agent task success and token spend with SourceVault MCP versus raw file reading, over the same verified question sets as the ask benchmark, with paired statistics. Built for harness phase 2; deliberately not yet run, and BENCHMARK.md says exactly what may not be published until it is. (#172)
- Team fulfillment automation (
scripts/release/fulfill-team.js): creates and sends the one-off Team invoice and, only once it is paid, issues the Gateway-bundle key to the billing email and stores it as the recovery metadata, with refusals against unpaid invoices and double-issuance. Vendor-side only; no customer-facing change. (#171)
2026-08-04, v1.12.0: The SourceVault Gateway
Added
- The SourceVault Gateway: the compliance features (Sentinel policy + DLP, the hash-chained audit log, AI-change provenance) are now one named tier, granted by the single
gateway license flag. The flag expands to the three member features at verification time, so keys issued earlier with the features spelled out verify unchanged. The Gateway is the Team and Enterprise differentiator; see the new docs/GATEWAY.md for the umbrella guide, including the honest-use boundary. (#167)
- A Gateway view in the dashboard, the operator console over the records the gateway already writes: agent (MCP) activity by surface and repository, Sentinel denials and redactions with their rules, and the audit-chain verdict translated to plain language (what
truncated, file_missing, and head_signature_invalid each mean). Backed by GET /api/dashboard/console, a pure aggregation of the month's audit log; viewing it records nothing, and with audit_log off the view says so. (#167)
- MCP resources and prompts. Resources:
sourcevault://repos, sourcevault://watches (metadata only, so watch baselines cannot bypass answer sanitization), sourcevault://policy, and a sourcevault://repos/{repo}/files/{+path} template served through the same Sentinel policy/DLP gate as read_repo_file. Prompts: four retrieval-pattern prompts (ground_in_repo, locate_implementation, pre_merge_provenance_gate, repo_orientation). Every resource read lands in the audit log. (#166)
Changed
- The MCP server now speaks the 2026-07-28 MCP specification as a dual-era server: modern clients work statelessly (
server/discover, per-request _meta versioning, resultType, cache metadata on list results) while legacy clients (2024-11-05 through 2025-11-25) keep the initialize handshake exactly as before. Both are served concurrently, so existing clients are unaffected. Tools gained titles, read-only annotations, and structuredContent; serverInfo reports the real package version. (#166)
- The task relay is promoted out of experimental as the Gateway's agent-to-agent surface. It remains deliberately single-shot (no queue, no retry, no orchestration), now documented as intended scope: orchestration belongs to the agents on either side, governance to the Gateway. (#167)
2026-08-03, v1.11.2: Same-day ip-address advisories and pin updates
Fixed
- Three high-severity advisories against
ip-address published later the same day as v1.11.1 (GHSA-mwp4-54f8-5fhr, GHSA-4xrf-jv44-h6hh, GHSA-22jq-vg5j-6vgg: SSRF/trust-boundary bypasses via octet decoding, CIDR-suffix classification, and IPv4-mapped IPv6 misclassification) are cleared via a patch-level lockfile bump. (#164)
Changed
- Routine pin updates, the first delivered by Renovate: the Docker base image digest (
node:24-bookworm-slim) and the ollama container tag (v0.32.5). (#160, #161)
2026-08-03, v1.11.1: Security bumps and the Forgejo-native pipeline
Fixed
- Two high-severity advisories published today are cleared via patch-level lockfile bumps:
brace-expansion (GHSA-rgw5-rvv9-x895, backend tree) and fast-uri (GHSA-7p8r-x3mc-p8w7, both trees), with a moderate postcss fix riding along in the dashboard tree. (#154, #159)
Changed
- CI/CD is now Forgejo-native end to end: the Semgrep job checks out with plain git (the pinned scanner image carries no Node for actions), the release pipeline publishes to the public GitHub tap while source, issues, and CI stay on Forgejo, the release-drift tracker is hardened against Forgejo's silently-ignored label filter, and Renovate replaces Dependabot for dependency and pin updates. (#153, #155, #156, #158)
2026-08-03, v1.11.0: The trial clock survives a wiped state dir
Changed
- The unlicensed trial's start date is now recorded in redundant, self-healing markers instead of a single
trial.json in the state dir. On a default install the clock is written to the state dir plus the user's XDG config and data directories (~/.config/sourcevault, ~/.local/share/sourcevault); the earliest surviving copy wins, deleted or hand-edited copies are re-seeded from it, and future-dated markers are ignored. Deleting the state dir or reinstalling no longer restarts the trial. Enforcement stays offline honest-use gating (no phone-home), so deleting every marker remains a documented reset and a license key is the legitimate path. (#149)
Upgrade notes
- Server deployments that set
SOURCEVAULT_STATE_DIR are unaffected: the marker stays under that single root and no user-level files are written. To spread redundant markers across additional persisted paths, set SOURCEVAULT_TRIAL_DIRS (path-delimiter separated); see deploy/server/sourcevault.env.example.
2026-07-31, v1.10.1: Dependency updates and release housekeeping
Fixed
- Dependency updates clear a new high-severity advisory in the dashboard's build chain (
brace-expansion, GHSA-mh99-v99m-4gvg) via an override to the patched version, alongside the week's routine backend and dashboard bumps. (#143, #144, #145)
- The release-drift tracking issue now closes at tag time instead of waiting for the next day's scheduled check, and the release process doc records the versioning policy (v1 stays; the minor number is the release number). (#141, #142)
2026-07-24, v1.10.0: The dashboard can tap you on the shoulder
Added
- Desktop notifications, off by default and delivered entirely by your browser: a watch flags drift, an index run of a minute or longer finishes or fails, or health turns unhealthy. Nothing routes through a push service: notifications fire while the dashboard is open (a tab or the installed app) and nothing leaves your machine. Enable them in Settings under Appearance. (#139)
- The app icon now carries a badge counting watches that drifted since you last looked; opening the Watches view clears it. (#139)
Fixed
- Watches now refresh on the dashboard's regular poll. Previously they refreshed only on page load or manual action, so a drift produced by a background reindex stayed invisible until a reload. (#139)
2026-07-24, v1.9.0: Choose your history backend from the dashboard
Added
- The git-history backend is now a dashboard setting (Settings -> Advanced) instead of an environment-variable-only switch. Choosing Qdrant verifies the backend before the change is saved, downloading and starting the managed engine on the spot, so a problem shows up in the settings dialog rather than during a later index run. A confirmation explains that each repository rebuilds its history index on its next reindex, backend changes are recorded in the settings audit trail, and installs configured via
CODE_HISTORY_BACKEND see the control disabled with a note that the environment is in charge. (#136)
2026-07-23, v1.8.0: History search reaches the machine API and the Hermes plugin
Added
- A new signed machine-API endpoint,
POST /api/history-search, answers natural-language questions about a repository's indexed commit history and returns matching commits with their metadata (hash, date, author, subject, provenance tags). It carries the same protections as the other machine endpoints: HMAC signing, rate limiting, and the Sentinel redaction pass; commit messages get the same secret scrubbing as code. (#129)
- The Hermes plugin (v1.3) gains a
/code-history slash command and matching LLM tools, so agents can ask "when did this change, and why?" against the local history index. Requires SourceVault v1.8.0; against an older install the command explains the version requirement. (#129)
2026-07-23, v1.7.0: Opt-in full git history
Added
- A "Full git history" toggle in Settings -> Features (free, off by default). Enabled, SourceVault indexes every commit in a repository instead of the latest 500. The first index of a large repository takes a while and shows progress; after that, each index run only adds commits that are new since the last one. Turning the setting off returns to the capped behavior on the next index. (#127)
- The git-history index can now run on Qdrant instead of ChromaDB (
CODE_HISTORY_BACKEND=qdrant). SourceVault manages the Qdrant server itself: a version-pinned binary is downloaded once, verified against a checksum, and run locally with its storage inside the SourceVault state directory; nothing to install or operate. Benchmarks on an 82,000-commit history showed filtered queries dropping from ~150ms to under 2ms on this backend. ChromaDB remains the default; existing installs are unaffected. (#126)
Fixed
- Deleting a repository now also removes its indexed git history, which previously remained in the history index forever. (#126)
2026-07-23, v1.6.0: Keyword search stops scanning the whole repo
Changed
- The keyword leg of hybrid search now runs against a SQLite FTS5 index instead of fetching and scoring every chunk of the repository on each query. Results are ranked by BM25 with prefix matching, latency no longer grows with repository size, and existing indexes migrate automatically the first time each repository is searched (no reindex needed). The old behavior remains available via
CODE_LEXICAL_BACKEND=scan. (#121)
- Indexed commits now carry a numeric timestamp, enabling time-window filtering of git-history search (the previous string-only date field could not be range-filtered). Each repository's history picks this up on its next index. (#122)
Added
- A vector-store benchmark suite (
npm run bench:history and companions) that measures indexing throughput, filtered-query latency, and memory at git-history scale, used to evaluate ChromaDB against candidate successor stores on real corpora. Developer tooling; no runtime impact. (#122, #123, #124)
2026-07-23, v1.5.1: Dependency security updates and clearer docs
Fixed
- Vector collections now declare cosine distance explicitly instead of inheriting ChromaDB's L2 default. Search results are unchanged (embeddings are unit-normalized, so both metrics rank identically), and existing indexes keep working without a reindex; the setting takes effect for newly created collections. (#116)
- Dependency updates clear all
npm audit findings in the backend and dashboard, including high-severity advisories in js-yaml, fast-uri, body-parser, adm-zip, and sharp. The adm-zip and sharp fixes are applied through npm overrides until @huggingface/transformers updates its own pins. (#117, #114, #119, #113)
Changed
- The README and all documentation were rewritten for plainer, more direct prose. Content, commands, and configuration references are unchanged; the storage-sizing figures in the implementation doc were corrected (embeddings are 768-dimensional, about 3KB each). (#118, #116)
2026-07-08, v1.5.0: Source provenance recorded at add time
Changed
- A source's type is now recorded when you add it, instead of being inferred from disk every time. When you clone a repository or add a local folder or file, SourceVault writes down what it is; the dashboard reads that record to decide whether a source belongs under Repositories or Local. Previously the type was re-derived from filesystem shape on each listing, which could not reliably tell a clone from a local folder that happened to contain a
.git (from your own git init). Classification is now authoritative and no longer depends on git state, so a source can't change sections as its git state changes. Existing sources are migrated automatically the first time they're listed, and a folder placed directly under the sources directory out of band is still recognised and recorded. No action needed. (#108)
2026-07-08, v1.4.2: Local sources index and classify correctly
Fixed
- "Sync & reindex" no longer fails on a dirty or upstream-less git source. The git pull that fetches the latest before reindexing was mandatory, so a dirty working tree, a diverged branch, an offline host, or a repo with no tracking upstream (any local
git init folder) failed the whole job and nothing was indexed. The pull is now best-effort: if it can't fast-forward, SourceVault logs a note and indexes the current on-disk working tree instead. Plain "Reindex" was never affected. (#107)
- A local folder that is a git repo is now filed under Local instead of Repositories. Classification checked for a
.git before anything else, so a local folder you added in place and had run git init in was mislabeled as a cloned repository. It's now identified by how SourceVault stores it (a local source is symlinked in place; a clone is a real directory), so it lands in the Local section while still showing its branch and commit. (#107)
2026-07-08, v1.4.1: Dashboard health fix for uncommitted local sources
Fixed
- A local folder that is a git repo with no commits yet no longer breaks the dashboard. Adding such a folder (a freshly
git init'd directory with an unborn HEAD) made the repositories listing fail with a 500, which flashed the sidebar's system-health tag red with no explanation. The listing now tolerates an uncommitted git source, showing it with no commit, and a single malformed source can no longer take down the whole list. Indexing was never affected: local folders and files are indexed from disk and never required git or a commit. (#105)
2026-07-08, v1.4.0: Local folders and files
Added
- Index local folders and files, not just cloned repos. Point SourceVault at any folder or file already on your machine and it's indexed in place through a link, so a reindex always reflects the live contents and removing it here never touches your originals. Add them from the sidebar's new Local section or the Repositories view; a native file/folder picker (macOS, Windows, Linux) fills in the path so you don't have to type it. Git clones and local sources sit in separate sidebar sections so the two don't collide. (#103)
Changed
- Plan limits are now measured in sources. A git repository and a local folder each use one slot of your plan's cap, while all loose individual files you add share a single pooled slot, so adding a handful of reference files never burns a repository each. The dashboard's plan meter and the limit message now read "sources" to match. (#103)
Upgrade notes
- Local folders now count toward your plan's source cap: a folder uses one slot, the same as a repository, and loose files added individually share a single pooled slot. Your existing indexed repositories are unaffected; if you add a local source while already at your cap, the dashboard asks you to remove a source or upgrade rather than failing silently.
2026-07-07, v1.3.5: Version at a glance
Added
- The sidebar shows what you're running. A muted version number sits beside the Support and Discord links in the sidebar footer, reporting the server's actual version; after a self-applying upgrade you can watch it tick over. Settings → About keeps the fuller details. (#96)
2026-07-07, v1.3.4: Upgrades apply themselves
Fixed
- Upgrading no longer takes repeated reloads to get the new interface. The dashboard now refreshes itself exactly once when a new version's service worker takes over, so
brew upgrade → open dashboard → current interface, with zero manual reloads. Tabs that were already open when the server upgraded show a "SourceVault was updated — Reload now" banner within a minute instead of silently running the old interface. (#94)
Upgrade notes
- One last time: this upgrade is installed *under* the previous service worker, so the first load may still show the old interface; reload once. Every upgrade after this one applies itself.
2026-07-07, v1.3.3: The dashboard becomes a workspace
Changed
- A new sidebar layout puts asking first. Navigation (Ask, Watches, Runbook, History, Archive) lives in a collapsible left sidebar with an always-visible repository list (status dots show ready/stale/indexing at a glance) and the model, service health, and settings tucked into its footer. The Ask composer is now the first thing you see, centered at reading width, with example prompts when you're starting fresh. Repositories are a full view of their own (with add, reindex-stale, and live indexing progress in the header) instead of a collapsed table at the bottom of the page. On phones the sidebar becomes a slide-out menu. Everything you had is still here: the four status cards' jobs moved into the sidebar and the Repositories view; nothing was removed. (#92)
Fixed
- The trial-expired notice linked a retired domain, and the renew-updates notice linked a pricing anchor that no longer exists; both land on the right pages now. (#92)
- Icon-only controls in the new sidebar carry proper accessible names for screen readers, and the service-health indicator respects reduced-motion preferences.
Upgrade notes
- The dashboard is an installable app (PWA), so your browser may show the previous layout on first load after upgrading; reload once or twice and the new interface activates.
2026-07-07, v1.3.2: Support surfaces, in the product
Added
- Help is one click from the dashboard. The dashboard footer now links the support surfaces: the Discord community, the support routing page, and the public issue tracker. (#85)
- Discord support server, as code. The community server's structure ships as reviewable config with a staff runbook (docs/DISCORD_SUPPORT.md), self-serve license-recovery pointers across the support surfaces, and corrected secrets guidance:
npm run doctor output is safe to share as-is; .env files, settings.json, compose/systemd configs, and logs are what need redaction. (#82–#84, #88)
Changed
- Settings now reflects plan gating accurately. The Audit log row is gated to the Team plan with compliance add-on pointers where they apply, and enterprise keys are clarified as always including Multi-repo Ask. (#86)
- Docs. DASHBOARD.md now describes the always-on auth model shipped in v1.3.0: the bearer token is required on every dashboard route, loopback included, with no tokenless mode.
2026-07-07, v1.3.1: Self-serve Starter → Pro upgrade
Added
- Upgrade to Pro, right from Settings. A Starter install now shows "Upgrade to Pro — pay only the difference" on the locked Multi-repo Ask setting, linking the self-serve upgrade page. The upgrade costs the list-price difference, and the new Pro key arrives by email with a fresh 12-month updates window; paste it over the old key and Multi-repo Ask unlocks immediately. The fulfillment contract gains
license_kind=upgrade (fulfill-stripe.js, LICENSING.md), and the billing policy is now self-serve-first with the manual runbook as the backup path. (#80)
Fixed
- License activation updates plan limits immediately. Activating a key refreshed the license state but left the repository cap (and updates window) showing trial-era values until the dashboard was reloaded or locked and unlocked. Enforcement was never affected: the server always honored the new plan; the display now does too. (#79)
2026-07-06, v1.3.0: Sentinel, enforced dashboard auth, async task relay
Added
- SourceVault Sentinel: a local enforcement layer between the code index and anything that reads from it. An access policy gate decides which files may be read at all; a gitleaks-style DLP pass redacts secret material from everything that leaves the engine; every enforcement lands in a tamper-evident, hash-chained decision log. Ships with dashboard toggles and documentation. (#55)
- AI-change provenance. Commits are classified for AI authorship and security relevance (path rules plus a diff-content pass, full-body trailer scan, and revert handling), with signed in-toto attestations, a drift correlator that ties index drift back to the commits that caused it, and dashboard/MCP surfaces. (#59, #71)
- Audit chain anchoring. The decision log now carries a signed monthly head anchor, detects tail-truncation and file deletion (not just edits), and fails closed on lock timeout instead of falling back to an unlocked write. (#63)
- Retrieval regression gate. A retrieval-only benchmark with recorded embedding fixtures (deterministic, no model needed), Wilson confidence intervals and paired McNemar A/B stats, multi-repo question sets, and a nightly CI gate armed with a committed baseline. (#50–#54, #68)
- Stripe fulfillment. Checkout-to-license fulfillment scripts with payment-link auditing, a documented billing policy, and unit tests over the whole money path. (#42, #45, #60)
Security
- Dashboard auth is now enforced by default; there is no tokenless mode. Boot provisions
DASHBOARD_TOKEN when nothing supplies one (persisted to ~/.config/sourcevault/dashboard_token, 0600, value never logged) and every fail-open branch is removed; a truly tokenless boot fails closed with 503 dashboard_locked, loopback included. Previously any local process could drive the mutating control plane on a tokenless install. (#74)
- DLP scanning gaps closed on the signed search API. Search previews are now scanned against the full chunk content (a trimmed preview window could cut the assignment prefix a rule needs), same-file chunks are stitched before scanning so secrets straddling chunk boundaries redact in both halves, and modern OpenAI key formats (
sk-proj-/sk-svcacct-) are detected without the legacy infix. (#61)
- Full-project review remediations and ops hardening. Fail-closed Sentinel enforcement paths, safer audit locking, pinned chroma/ollama images (tag+digest), Bitbucket webhook dedupe via pushed head commit, and documented loopback trade-offs. (#62, #69)
Fixed
POST /task answers its 202 immediately instead of blocking up to two upstream timeouts (~120s) while clients gave up and retried; POST /result is idempotent, so a deployment where OpenClaw also calls back no longer double-delivers to Hermes. (#70)
- Token rotation reaches every config source. Rotation now persists to the compose env file (
~/.config/sourcevault/server.env) and the brew launchd env file (via SOURCEVAULT_ENV_FILE, exported by the updated formula wrapper); previously a Settings-page rotation on those installs silently reverted on the next service restart. Boot warns on credential-file drift. (#75, #76)
- Dashboard file reads honor the requested cap. Full-file reads were silently clamped to 50KB; the dashboard's trusted cap now applies and byte counts are accurate. Drawer ask-conversations are session-scoped. (#72)
- Pre-push hook survives linked worktrees. Hook-exported
GIT_DIR no longer redirects temp-repo test git commands at the project repo (which could re-init it bare). (#64–#65, #67)
- Chunk-dedup accounting.
reusedChunks no longer overcounts; verified end-to-end against a real indexer run. (#50)
Changed
- The live compose env file moved out of the repo tree to
~/.config/sourcevault/server.env (override with SOURCEVAULT_ENV_FILE); the old in-tree path stays git-ignored and blocked by the pre-commit hook. (#66)
- Docs. The task relay is documented as experimental with its own guide; the parked roadmap, billing policy, and Starter → Pro upgrade path are written down. (#73, #43, #47)
- Supply chain. actions/checkout v7, Node 24 CI actions, Dependabot backend bumps. (#35, #38–#39)
Upgrade notes
- Dashboard login is now always required. Installs with
DASHBOARD_TOKEN already configured see no change. Tokenless installs get a token auto-provisioned at first boot — read it with cat ~/.config/sourcevault/dashboard_token and paste it at the unlock screen; the browser session keeps logins infrequent (DASHBOARD_SESSION_MAX_AGE_SEC, default 12h).
- Compose deployments: move the env file out of the repo tree to
~/.config/sourcevault/server.env before the next docker compose up; compose fails loudly until then (see DEPLOY_SERVER.md).
- Homebrew installs: this upgrade also refreshes the service wrapper so in-app token rotation persists to the launchd env file; restart the service (
brew services restart sourcevault) after upgrading.
2026-07-03, v1.2.0: production hardening — sanitized errors, rate-limited machine API, Vite 8, non-root Docker
Security
- Server errors no longer leak internals. 5xx responses previously echoed raw error messages (which can carry absolute filesystem paths and upstream response bodies); they now answer a generic detail while the specifics are logged server-side. A global error handler also replaces the HTML stack trace Express emits outside
NODE_ENV=production, closing the leak for bare npm start runs. (#32)
- The signed machine API and task relay are rate-limited.
/api/search-codebase + /api/read-file (default 300/min, SOURCEVAULT_MACHINE_API_RATE_LIMIT_*) and /task + /result (60/5min, SOURCEVAULT_RELAY_RATE_LIMIT_*), mounted ahead of signature checks so brute-forcing is throttled too. Previously only dashboard and webhook routes had limits. (#32)
- Crash guards.
uncaughtException exits cleanly for service-manager respawn; unhandledRejection logs and continues, so a background relay or index-resume failure can't take the server down. (#32)
- Hardened surfaces. Swagger UI (
/api-docs) gets its own CSP; cross-origin-opener-policy/cross-origin-resource-policy join the baseline headers; boot warns when a secret still holds a template placeholder value. (#32)
- Secure installation. The Docker image runs as the unprivileged
node user with a digest-pinned base; compose binds 127.0.0.1:9000 by default; CI actions are pinned to commit SHAs and Semgrep to a version; the Linux installer no longer pipes remote scripts straight into a root shell (download → printed sha256 → confirm → execute). (#32)
Fixed
OLLAMA_HOST now actually reaches ask/embed calls. The Ollama client was configured via an instance field the library ignores, so non-default hosts silently fell back to 127.0.0.1:11434. The client is now constructed with the configured host. (#32)
Changed
- Dependencies current, zero audit findings. Dashboard build moves to Vite 8 (Rolldown) with
@vitejs/plugin-react 6; ollama 0.6. Both package trees now npm audit clean. (#32)
- ESLint gates both trees. Flat configs for the backend and dashboard run in CI and the pre-push hook; the Zustand store is typed end-to-end (
set/get checked against the real store shape); duplicated fetch/ poller/error-factory code consolidated into shared helpers. (#32)
- Docs. README now answers "when is Docker required?" per install path (macOS: never; Linux installer: ChromaDB container only; server: Compose). (#33) License signing-key generation/rotation is documented with a
scripts/ops/license-keygen.js helper. (#32)
Upgrade notes
- Docker Compose deployments only: the container now runs as the
node user, so named volumes created by an older (root) image need a one-time ownership fix before the new image starts: docker compose run --rm --user root sourcevault chown -R node:node /data, then docker compose up -d. Fresh deployments and all native (macOS/Linux installer) installs are unaffected. See DEPLOY_SERVER.md "Updating".
- The task relay's
OPENCLAW_API_KEY and HERMES_WEBHOOK_SECRET are now documented in .env.example; set them only if you use the Hermes ↔ OpenClaw broker routes.
2026-07-02, v1.1.6: sourcevault app Dock launcher
Added
sourcevault app [url] [--out <dir>] builds a SourceVault.app Dock launcher in ~/Applications that opens the dashboard in the default browser: the packaged equivalent of npm run dashboard:app, for Homebrew installs that don't have npm scripts. The embedded URL honors the install's configured HOST/PORT. (#27)
sourcevault --help prints CLI usage. (#27)
2026-07-02, v1.1.5: install the dashboard as an app + self-healing upgrades
Added
- The dashboard installs as a desktop app. Served from
http://localhost:9000/dashboard/ (a secure context, so no "Not Secure" label), the dashboard's PWA lights up native install: Chrome/Edge show the address-bar install icon and a new Settings → About → "Install app" button; Safari installs via File → Add to Dock. Installed, it runs in its own window with a Dock icon and no browser chrome. (#24)
- Browsers landing on
/ go to the app. localhost:9000 and the dashboard.sourcevault.ai alias now redirect HTML clients to the dashboard; scripts and monitors requesting JSON keep the health payload. (#24)
Fixed
- Upgrades no longer strand a running service on deleted files. Package upgrades remove the old install directory while the service keeps running from it (symptom: dashboard 500s on a missing
index.html). The server now watches its own install path and exits for respawn when it vanishes; launchd (keep_alive) and systemd (Restart=on-failure) bring up the new version automatically. The Homebrew formula also restarts the service at upgrade time. (#23, homebrew-tap#1)
Changed
- The advertised dashboard URL is
http://localhost:9000/dashboard/ (secure context, installable) instead of the branded host, which browsers flag as "Not Secure" on plain HTTP. dashboard.sourcevault.ai remains a typeable alias that redirects into the app. (#24)
2026-07-02, v1.1.4: branded dashboard URL
Added
- The dashboard has a branded URL:
http://dashboard.sourcevault.ai:9000/dashboard. The name carries public A/AAAA records pointing at loopback, so it reaches the local install on every machine with zero setup. The server advertises it (startup dashboard_available log, macOS launcher) only after verifying it resolves to loopback locally; offline machines, DNS-rebinding-protected routers, and non-loopback HOST binds fall back to the bound address automatically. (#21)
npm run dashboard:app builds a Dock-able macOS launcher that opens the dashboard in the default browser.
Fixed
- The launcher opens
/dashboard (the SPA) instead of the server root (health JSON). (#21)
2026-07-02, v1.1.3: enforce the advertised trial repo cap
Fixed
- Trial installs are now capped at 1 repository, as advertised. The install caveats and site have always said "7-day trial with one indexed repository", but the cap was wired to an env var no installer set, so an unlicensed trial could index unlimited repos.
getRepoLimit() now defaults unlicensed installs to 1 (licensed keys without max_repos, i.e. enterprise, remain unlimited; SOURCEVAULT_DEFAULT_MAX_REPOS still overrides).
2026-07-02, v1.1.2: version everywhere support needs it
Added
sourcevault --version (and -v) prints the version (plus the release date on release builds) without booting the server.
/health includes version, so "what version are you running?" is one curl away on any install.
- Dashboard Settings shows an About row with the server version.
2026-07-02, Auto-release on version bump + browser smoke tests
Pipeline
- Merging a version bump now IS the release. The release workflow also runs on pushes to
main: when package.json's version has no tag yet, it builds, publishes to the tap, bumps the formula, and pushes the vX.Y.Z tag itself (tag-last, so a failed build leaves nothing behind and simply retries). Merges that don't bump the version no-op. Manual tag pushes still work and remain the path for --suffixed prerelease tags, which never repoint the customer formula.
- Real-browser E2E smoke in CI. A Playwright job boots the server with auth enabled, logs into the dashboard through the actual UI in Chromium, and asserts a mutating request passes the CSRF double-submit check while a raw header-less request is still refused. This is the test tier the v1.1.1 CSRF-cookie-path bug slipped past: unit tests attach cookies and headers manually, so only a real browser sees cookie-path visibility. Verified to fail against the v1.1.0 code and pass against the fix.
2026-07-02, v1.1.1: fix dashboard CSRF cookie path
Fixed
- Mutating dashboard requests 403'd in the browser (
csrf_token_mismatch) whenever DASHBOARD_TOKEN was set (e.g. saving an SCM token, adding a repo). The CSRF cookie was scoped to Path=/api/dashboard, but document.cookie only exposes cookies whose path matches the page's own path, and the SPA is served from /dashboard/, so the client could never read the token to echo it as the x-csrf-token header. The cookie is now Path=/ (double-submit security lives in the token value; the cookie is JS-readable by design). The HttpOnly session cookie stays scoped to /api/dashboard. Server-level tests attached cookie + header manually, which is why this only surfaced in a real browser; a regression test now pins the cookie path.
2026-07-02, 7-day trial for unlicensed installs
Licensing
- Perpetual licenses with a 12-month updates window. Self-serve keys now carry
updates_until: the license works forever on every build released inside its window, and newer builds require a discounted renewal; the installed version never stops working. Enforcement is fully offline (release tarballs embed their release date; dev/git/Docker builds don't enforce). Customers whose window ended see renewal guidance and a dedicated dashboard banner, never trial messaging. Keys without the field (existing/enterprise, --updates-forever) cover updates forever.
- Trial period. Unlicensed installs now run a 7-day trial of the core product (search, read, ask, repo onboarding); paid upgrade features stay locked throughout the trial. After expiry the core surfaces return
403 trial_expired on every product surface (dashboard, signed API, MCP, Hermes) until a license key is entered. The clock starts on first use (trial.json in the state dir); window length is SOURCEVAULT_TRIAL_DAYS (default 7); any valid license bypasses the trial. The dashboard shows a days-remaining banner during the trial and a license prompt after expiry, and stays reachable so the key can always be entered.
2026-07-02, One-command macOS install via Homebrew
Platform
brew install sourcevault-ai/tap/sourcevault. macOS installs are now a single Homebrew command via the new public tap (sourcevault-ai/homebrew-tap): it pulls Node 24 (pinned node@24), Ollama (homebrew-core), and ChromaDB (companion formula in the tap, private Python virtualenv), generates config + secrets to $(brew --prefix)/etc/sourcevault/sourcevault.env once (0600, survives upgrades), and registers a brew services launchd service; no more hand-rolled LaunchAgents on the brew path. scripts/setup/install.sh remains the Linux/WSL2 path and a macOS alternative (still the way to get per-repo scheduled reindex agents).
- CLI entry point. New
bin/sourcevault (package.json bin field) so package managers expose a sourcevault executable; sourcevault.js's main block is now an exported main().
Release engineering
- Versioned releases for Homebrew. New
.github/workflows/release.yml: pushing a v* tag builds the dashboard and publishes a GitHub Release with sourcevault-vX.Y.Z.tar.gz (source + prebuilt dashboard/dist) and its sha256; that tarball is what the formula consumes (scripts/release/package-brew-tarball.sh).
- Fully one-command releases. The release workflow also bumps the formula's
url/sha256 in the tap automatically, so shipping is just: bump package.json version on main, git tag vX.Y.Z && git push origin vX.Y.Z. Prerelease tags (any version containing -) publish an asset but skip the formula bump, so customers are never repointed at a test build.
- Release-gate fix. The customer-bundle forbidden-path gate was silently disabled on Linux/CI:
tar | grep -q under set -o pipefail turned a MATCH into a pipeline failure via SIGPIPE. The listing is now read once and files are matched exactly, so .env can no longer false-positive on the intentionally shipped .env.example either.
- Hardcoded
/home/larry fallback paths replaced with home-relative equivalents.
2026-07-02, Security hardening, observability, and deploy polish
Security
- Rate limiting on the control plane and webhook. Dashboard login (10/5min), ask/ask-source (20/5min), reindex (5/10min), and the git webhook (30/5min) are now rate-limited via
express-rate-limit with per-route, env-overridable thresholds (*_RATE_LIMIT_MAX / *_RATE_LIMIT_WINDOW_MS), standard draft-6 RateLimit-* headers, and a Retry-After on 429s. New SOURCEVAULT_TRUST_PROXY (hop count) makes req.ip the real client behind a reverse proxy so limits apply per client instead of one shared bucket.
- CSRF double-submit token for the dashboard. Defense-in-depth on top of the
SameSite=Strict session cookie: login issues a non-HttpOnly sourcevault_dashboard_csrf cookie the SPA echoes back as x-csrf-token on mutating requests; the server compares them with a timing-safe check. Applies only to cookie-authenticated requests: bearer-token clients (scripts, plugins) are unaffected.
- Webhook replay dedup. Provider retries (GitHub redeliveries, GitLab retries) no longer trigger duplicate reindex jobs: delivery IDs (
X-GitHub-Delivery, webhook-id/Idempotency-Key, X-Gitlab-Event-UUID) are tracked in a bounded cache and repeats get 200 {ignored: true}.
- Search-query length bound (
CODE_SEARCH_QUERY_MAX_CHARS, default 2000) so a pathological query can't drive unbounded embedding cost, plus shared input-validation helpers (services/util/validate.js).
- Baseline security headers app-wide (
nosniff, X-Frame-Options: DENY, referrer-policy), X-Powered-By disabled, and the shared HMAC verification extracted into one middleware (services/security/hmac-guard.js) instead of four per-route copies.
- Container hardening.
sourcevault/chroma get CPU/memory/pids limits in compose.yaml so a runaway index job can't OOM the host, and the optional with-ollama profile binds 11434 to loopback only (Ollama's API is unauthenticated; never expose it directly).
Observability
- Structured logging via a dependency-free logger (
services/util/logger.js): readable text by default, one-line JSON with SOURCEVAULT_LOG_FORMAT=json for aggregation. Every 5xx now logs with stack context, and previously swallowed errors in the ask pipeline (phase callbacks, thread resolution, symbol-graph enrichment, follow-up rewrites) are logged instead of silently discarded.
- Auth events in the audit log. Failed logins, successful logins, HMAC signature failures (per endpoint), webhook auth failures, and rejected settings/license changes are now audited alongside the existing events, via a shared
recordAuthFailure helper that feeds both the audit log and the structured logs.
- Startup dependency probe. After the server starts listening it probes ChromaDB and Ollama in the background (with backoff) and warns if either is unreachable — observability only, never blocks or fails boot, since the
with-ollama quickstart intentionally pulls models after up -d. Opt out with SOURCEVAULT_SKIP_STARTUP_PROBE=1.
Deployment & PWA
- Offline fallback page (
offline.html, theme-aware) precached by the service worker: navigations outside the SPA shell fallback get a branded offline page instead of the browser error screen (the cached app shell continues to serve normal dashboard navigation offline, as before).
- Expanded
docs/DEPLOY_SERVER.md: nginx and Traefik reverse-proxy examples alongside Caddy, an explicit X-Forwarded-Proto requirement (the session cookie's Secure flag depends on it), a CSP-behind-proxy note, and the SOURCEVAULT_TRUST_PROXY guidance above.
- Config constants centralized: read-file byte cap, search result cap, preview length, and prior-answer replay length moved into
CODE_MEMORY_CONFIG as env-overridable values.
2026-06-20, Plan limits + macOS support
Platform
- macOS (launchd) is now a supported install target.
scripts/setup/install.sh auto-detects the platform: systemd on Linux/WSL2, launchd on macOS (Homebrew for Node/Ollama, native chroma run instead of Docker, dashboard-only). See docs/INSTALL_PLAYBOOK.md. Native Windows remains unsupported (use WSL2 or the Docker Compose deploy).
Licensing
- Per-plan repository cap. (#3, #4) Licenses can carry a signed
max_repos limit; cloneCodeRepo rejects adds past the cap with repo_limit_reached (403). Resolution: license limits.max_repos → SOURCEVAULT_DEFAULT_MAX_REPOS → unlimited. Settings shows "X / N repos used".
2026-06-19, Docker Compose deployment + installable PWA dashboard
Deployment
- Docker Compose server deployment. (#72) A team can now run one SourceVault stack and connect from Windows/macOS/Linux/WSL2 via the browser, instead of replicating the heavy Ollama + ChromaDB + Node stack on every laptop. Adds a multi-stage
Dockerfile (Node 24; builds the dashboard, prunes dev deps, fetch-based HEALTHCHECK) and compose.yaml (sourcevault + chroma, with an optional ollama profile for all-in-one demo boxes). Chroma is network-internal with a bash /dev/tcp healthcheck; OLLAMA_HOST points at a shared GPU/host box (host.docker.internal works on Docker Desktop/Colima and, via the host-gateway mapping, on Linux). State, repos, and vectors persist on named volumes. New deploy/server/sourcevault.env.example (real env git-ignored) and docs/DEPLOY_SERVER.md covering prerequisites (incl. macOS/Colima), quickstart, reverse-proxy/TLS, backups, and troubleshooting. Verified end-to-end: /health?deep=1 reports chroma + ollama reachable, and /api/dashboard/* fails closed (401, not 503) once the three secrets are set.
Dashboard
- Installable PWA. (#72) The dashboard is now a Progressive Web App via
vite-plugin-pwa: "Install" gives it an app icon and a standalone window on Windows/macOS/Linux with no per-OS packaging or code-signing. The manifest is scoped to /dashboard/ with a maskable icon set, and the service worker precaches only the app shell and excludes /api/* so the cookie-authed, dynamic API responses are never served stale. Installability needs a secure context — localhost for local testing, otherwise HTTPS via the reverse proxy.
2026-06-19, Webhook-driven reindexing and chunk-level dedup
Indexing
- Git-webhook trigger: push to reindex. (#68) A push to an indexed repo's branch now runs a sync + incremental reindex automatically, via
POST /api/webhooks/git, the same background job as the dashboard's "sync and reindex". Authenticated solely by SOURCEVAULT_WEBHOOK_SECRET (not the dashboard token) across GitHub HMAC (X-Hub-Signature-256), GitLab (X-Gitlab-Token), and a generic X-SourceVault-Token; it fails closed: 503 when the secret is unset, 401 on a bad/missing signature. Pings, tag pushes, and non-indexed-branch pushes are acknowledged and ignored; an unknown repo returns 404. See the README "Git webhooks" section to configure it.
- Chunk-level content-hash dedup: re-embed only what changed. (#69) File-level incremental indexing already skipped unchanged files, but a changed file re-embedded all of its chunks. Each chunk now carries an
embedHash (a hash of its embed input), so an incremental reindex reuses the existing embeddings for a changed file's unchanged chunks: a small edit to a large file only re-embeds the chunks that actually changed (reported as reusedChunks). Embedding is the expensive step, so this is a direct win on edit-heavy reindexing.
2026-06-18, Ask survives a browser refresh
Dashboard
- Ask survives a browser refresh, with a live phase + ETA. (#55) An Ask ran synchronously inside the HTTP request, so refreshing the browser aborted the fetch and dropped the in-flight answer. Ask now runs as a background job — the same pattern indexing got in #35 / #38:
POST /api/dashboard/ask returns 202 + a job handle, the dashboard polls to completion, and on reload it re-attaches to the still-running job by id. While it runs, a banner shows the phase (retrieving → generating → finalizing) and a rough ETA estimated from the median duration of recent asks for that repo/model. Note: POST /api/dashboard/ask now always returns 202 and surfaces input errors asynchronously via the job (status:"error") instead of a synchronous 400.
2026-06-18, Indexing at scale, retrieval quality, and live progress
A focused push to make large-repo indexing reliable and fast, surface what
indexing is doing in the dashboard, and close a retrieval-recall bug. Shipped
across PRs #35–#48
(roadmap tracked in issue #44).
Retrieval quality
- Literal-plan recall fix: benchmark file-find 0.600 → 0.833. (#48) A natural-language question that merely *named* a method (e.g. "how does
res.sendFile send a file?") was hijacked into a literal-only search that returned test files / call sites and skipped semantic recall of the file that actually defines the symbol. Now only pure file-listing intent (extensionOnly) short-circuits; everything else folds literal hits into the hybrid RRF fusion. Search-only retrieval went hit@5 0.333 → 0.833, recall@50 0.467 → 0.967, MRR 0.284 → 0.633.
- Opt-in TEI embedding backend + Matryoshka dimension truncation. (#46)
CODE_EMBED_URL routes embeddings through a Hugging Face Text Embeddings Inference server; CODE_EMBED_DIMS truncates and re-normalizes vectors. Off by default; throws on failure so it never silently mixes vector spaces.
- Opt-in TEI reranker backend + RAG roadmap. (#43)
CODE_RERANKER_URL swaps the local ONNX cross-encoder for a TEI /rerank server. Off by default. Added docs/RAG_ROADMAP.md.
- TEI
/rerank truncation fix. (#45) Requests now send truncate: true; code chunks were 413-ing without it.
- Reranker A/B recorded. (#47)
bge-reranker-base and 8192-ctx bge-reranker-v2-m3 were both A/B'd via TEI and both lost; the local ms-marco-MiniLM cross-encoder stays the default.
Indexing reliability & performance
- Background index jobs: large repos no longer time out. (#35) Reindexing runs as a fire-and-forget job instead of synchronously inside the HTTP request (which SIGTERM'd at the 5-minute mark). Reindex routes return
202 + a job handle.
- Chunk sanitization + per-file resilience. (#40)
sanitizeForChroma() strips lone UTF-16 surrogates that ChromaDB's strict Rust JSON parser rejects (the real reason a 14k-file C++ repo aborted at ~83%), and a per-file try/catch lets one bad file skip instead of failing the whole run.
- Concurrent embed/upsert pool (~2×). (#41) A bounded pool (
CODE_INDEX_CONCURRENCY, default 4) parallelizes embedding/upsert. Measured on a full index: 27m50s → 14m15s, identical output.
- Vendored/build-dir filtering +
.sourcevaultignore. (#42) Expanded default ignoreDirs plus a per-repo .sourcevaultignore (gitignore-subset). On vendored-heavy repos this cuts ~90% of files (14k → ~2k), the single biggest indexing-time win.
Dashboard
- Richer index status + ETA. (#39) Replaces the bare spinner with scanning/progress/percentage and an estimated time remaining, driven by a file count.
- Resume index progress after reload. (#38) The dashboard re-attaches to a running index job after a page refresh.
- Per-repo index spinners. (#37) Concurrent indexes no longer steal each other's spinner; progress is tracked per repo.
- Restore vault-mark logo in the topbar. (#36)