0.27.0 - 2026-08-18
Release Notes
Added
-
The documentation is published as a site.
docs/is the GitHub Pages
publishing source, so the twenty-two reference pages and nine ADRs are
readable at https://alphabet-h.github.io/grooveseek/ without cloning
anything. Both languages are published; every page already linked to its
counterpart, andjekyll-relative-links— on by default — resolves those
links, so the language switch is the one that was already in the text.The repository root was the other possible source and was not chosen: it
would have published ninety-four Markdown files, thirty-two of them synthetic
test fixtures, plus the source tree as static files, and would have needed an
exclusion list maintained against a repository that is mostly not
documentation.Six links inside
docs/pointed outside it — at
grooveseek/examples/andgroove.toml.example— and would have resolved to
nothing on a site whose root isdocs/. They are absolute now, for the same
reason the README's images are. -
The README has a face: a mark, a screenshot of
/ui, and three badges.
ADR-0007 accepted,
knowingly, that "GrooveSeek" says nothing about what the product does and that
searching for "groove" lands in music software — and concluded that this "makes
the first line of the README load-bearing". The mark is lines of a document
with the shipped◆marking the passage a search found, in the same accent
the web interface uses; light and dark variants are selected with<picture>.The badges are CI, latest release, and the licence. There is deliberately no
crates.io or downloads badge: every crate here ispublish = false, so both
would be false.Images are referenced by absolute URL rather than repository-relative path,
because a release archive ships this README withoutassets/— the same
reason the documentation links were made absolute in the previous change.
They point at PNG renders rather than the SVG sources: an absolute URL
resolves toraw.githubusercontent.com, which is reported to serve.svgas
text/plainso an<img>will not render it, and the screenshots are PNG in
any case.assets/README.mdrecords the reasoning and how to regenerate. -
[transport.http].allowed_origins. Names the browser origins the server
accepts. Needed when a browser reaches groove through a reverse proxy, because
the browser then sends the public origin and the loopback default will not
match it. Entries carry a scheme and bracket IPv6, since they are compared as
RFC 6454(scheme, host, port)triples.Setting it replaces the default list rather than extending it, matching
allowed_hosts. Keep the loopback entries alongside your public origin if
browser-based clients also reach you over loopback.An empty list disables validation entirely and now warns at startup. Like
allowed_hosts,healthz_publicandmax_sessions, the key is ignored when
it comes from a config file groove discovered rather than one you passed with
--config— otherwise whoever can write agroove.tomlbeside the binary
could name their own origin, or blank the list, and turn the check off. -
A stability policy: docs/stability.md. It states what
1.0.0 will freeze and — more usefully — what it deliberately will not. Without
it, tagging 1.0.0 would promise that everything observable stays fixed until
2.0.0: 408 public Rust items across 24 modules, 138 command-line flags, 6 MCP
tools, 11 configuration sections, and a SQLite schema.Stable from 1.0.0: subcommand names and documented flags, exit codes, the
stdout/stderr split, the JSON fromsearchandgraph(fields may be added, so
ignore ones you do not recognise), MCP tool and prompt names with their schemas,
thekb://resource scheme,/mcpand/healthz, configuration keys and
defaults, the default embedding model, and the names written into your
filesystem.Explicitly not stable:
/uiand/api/*(loopback-only admin surface, due
to be rebuilt), all human-readable text output, the internal database schema, log
wording, and the Rust API. Reasoning: ADR-0008.
Changed
-
groove service uninstallandservice statustake--service-name
instead of a positional.install,tray-installandtray-uninstall
already named the instance with a flag, so the same thing had two spellings —
install --service-name workagainstuninstall work.
docs/stability.md freezes subcommand positionals as well
as long flags, which would have kept both forever, and a positional cannot be
taken away afterwards at all.The two also gained the name validation the other three already had. A name
installrefuses can never have been installed, so nothing that used to work
stops working. -
docs/stability.mdnow says what it freezes, rather than leaving it to be
inferred.Which flags. The promise is scoped to the
groovebinary and to flags this
documentation describes — and "documented" is now checked by a test rather
than assumed. Two flags were undocumented and would have been left unfrozen by
accident:groove validate --schema, the only way to point validation at a
schema that does not sit beside the knowledge base, and--fail-fast. Both
are written up in docs/usage.md now.Which output. Every subcommand that takes
--formatis listed in one of two
groups, because nine of them were in neither and silence reads as a promise.
The JSON ofsearch,graph,doctorandvalidateis stable, as is
validate --format github. Text output is not, from any subcommand; neither
aregraph --format dotand--format svg, which are drawings; neither is
the JSON ofevalandtune, whose numbers are expected to improve —eval
already stamps its history with ametric_versionfor that reason.Which channel. The stdout/stderr split is stated as it actually is. Six
subcommands produce a result on stdout;index,statusandservicewrite
everything to stderr, sogroove status | …receives nothing. That was true
before and the document said otherwise. -
The command line and the MCP tools now use the same noun for the same
thing, and docs/stability.md says which parts of the two
surfaces correspond. Both are frozen at 1.0.0, so this is the last release
that can move either one.Two names were one concept called two things.
groove graph --excludeis now
--exclude-paths, matching the tool'sexclude_paths; and the tool'spath
is nowstart, matchinggroove graph --start. The tool took the flag's word
rather than the other way round, because--pathbeside--kb-pathreads as
the corpus, andget_documentkeepspathfor the document it fetches.What remains different is deliberate, and is now written down instead of being
inferred: a repeatable flag is singular where the array it fills is plural
(--path-glob/path_globs,--tag-any/tags_any), tool names and
subcommand names do not correspond at all (get_connection_graphis
groove graph), andrerankis a per-call boolean while--rerankerpicks a
model. Neither shape is unusual —gh --labelfills the REST API'slabels,
anddocker --publishfills Compose'sports— so the rule is that the
mapping is predictable, not that the strings are equal.Values are held to a stricter rule, because a name that differs costs a
lookup while a value that differs fails the call outright:seed_strategynow
takesall_chunksandall-chunkson both sides. Copying either spelling
from one surface to the other used to be rejected — by clap on one side and by
unknown seed_strategyon the other. There is one table of accepted
spellings and both parsers read it, so a strategy cannot become reachable on
one surface only;--helpstill advertises the one spelling the command
line's own conventions produce.A test pins the pairing itself. Adding a parameter to either surface fails
until the table names its counterpart or records why it has none, which puts
the question in front of whoever adds it while the answer is still free. -
docs/ARCHITECTURE.mdstopped calling/uia disposable placeholder. It
still described the file as "a disposable placeholder — a proper redesign is
expected in Phase 3+" after that redesign had shipped. -
/uishows the knowledge-base path the way it was typed. Windows
canonicalisation returns an extended-length path, so the status band read
\\?\C:\noteswhere the operator had passedC:\notes. The prefix is now
stripped for display only;/api/admin/statusstill returns what it returned,
because the tray reads that field too. -
The README is an entry point again, and the reference it used to carry now
lives underdocs/. It had grown to 1,057 lines, of which 1,004 — 95% —
were configuration, CLI and client reference that a first-time reader has to
scroll past to reach "what is this and how do I install it". Those five
sections moved verbatim intodocs/configuration.md,docs/usage.md,
docs/clients.md,docs/mcp-tools.mdanddocs/behavior.md(each with its
.ja.mdpair), and the README is now 112 lines: what it is, how to install
it, a quick start, and an index of the rest.Links into the old sections change. Anchors that pointed at, say,
README.md#config-file-discoverynow live at
docs/configuration.md#config-file-discovery; the section names and their
anchors are unchanged, only the file is. Everything inside the repository
that referenced them was updated in the same commit.Two things are read outside the repository and were handled separately: a
release archive ships the binary and this README but nodocs/, so the
Documentation section says so and gives an absolute URL, andgroove --help
now names that URL rather than a path the reader may not have. -
/uiis the operator's view of their own server, and it searches through
/mcp. It shows a status band — version, documents, chunks, model, watcher,
uptime, pid, indexing progress — over a search box, replacing a placeholder
that said "MVP" and "to be redesigned" in its own markup while the project was
preparing to call itself stable. Still one file, no external requests, and
every string out of the knowledge base placed withtextContent.Routing its search through
/mcprather than a private endpoint means the
page exercises the same surface an external client would, and puts/ui
underOriginvalidation for the first time. With the default list it
works; anallowed_originsthat names only a public origin leaves the page
served but unable to query, and the server now warns about that at startup
rather than leaving a silent 403 on screen. -
grooveseekis markedpublish = false. The Rust API is not part of the 1.0
promise, andcargo packagecannot succeed anyway while the workspace uses
unversioned path dependencies.cargo publishnow refuses rather than relying on
a documentation note.[package.metadata.dist] dist = truewas added in the same
change, without which cargo-dist would silently stop shipping the main binary. -
Configuration files are declared not forward compatible. Unknown keys stay an
error, so a 1.0.x binary will refuse a configuration written for 1.1. The
alternative would letmodle = "bge-m3"index a knowledge base with the wrong
model behind a single warning on a daemon's stderr. -
The README titles now name the product (GrooveSeek) rather than the command
(groove). -
docs/stability.md now says where GrooveSeek is meant to
run. Having no authentication is a design position, not a gap awaiting work,
and saying so is what makes the rest of the policy coherent: the HTTP transport
expects to be reached from the same host, with the network boundary owned by a
container, a reverse proxy, or the application that puts a face on the knowledge
base. Non-loopback binds stay allowed — a container has to bind one or published
ports never reach it — but they mean you have taken that boundary on yourself. -
"Is this address loopback?" now has one answer instead of three. The admin
router unwraps IPv4-mapped IPv6 (::ffff:127.0.0.1) and treats it as local;
groove serveaskedIpAddr::is_loopback, which says no; and
groove service installmatched on string prefixes. So binding to a mapped
loopback address was refused as "network exposure" without--i-know, while
a peer arriving from that same address was being let into/ui. All three
now call one predicate, and--bind [::ffff:127.0.0.1]:PORTno longer
demands--i-know— it is a loopback address, and the rest of the server
already behaved as though it were. Nothing else changes: every other address
the old predicates already agreed on. -
The refusal printed for a non-loopback
--bindnow states the consequence.
It used to say groove "has no auth" and that exposure "is dangerous", which
leaves the reader to work out what is actually at stake. It now says that
anything able to reach the port can read the entire knowledge base, and that
Hostvalidation and the session cap are not authentication. Same text in
groove serveandgroove service install. -
The admin web surface is now documented as scheduled to go away.
docs/stability.mdrecords the intent to retire/uiduring 1.x, once a
client that speaks/mcpexists — browsing belongs there, where every tool
and every search parameter is reachable and the surface is already stable.
/api/admin/statusstays: it reports operational state (version, pid,
indexing progress) that does not belong in a tool surface built for language
models. Both remain unstable, so this is notice rather than a promise.
Fixed
-
The intranet-HTTP recipe never mentioned
allowed_origins. That release
adds Origin validation and turns it on by default, and the recipe it matters
most for — a reverse proxy terminating TLS in front of the server — explained
only theHosthalf. Following it as written left every browser-based client
refused with no indication why. The config template, the threat table and the
nginx step now name the key and say that a browser behind a proxy sends the
public origin. -
docs/behavior.mdsaid groove has no authentication "yet". That reads as a
promise; docs/stability.md states the opposite — no
authentication, by design, with the boundary belonging to whatever runs in
front. A page describing behaviour and a page defining the 1.0 surface must not
disagree about a security posture. -
docs/stability.mdfroze an environment variable the binary does not read.
GROOVE_BINis a variable of the shipped example hook. The three the binary
actually reads areGROOVE_CONFIG_HOME,GROOVE_TRAY_LOG, and fastembed's own
FASTEMBED_CACHE_DIR, which is not ours to freeze. Same shape as the
--verboseentry below, found the same way — by checking the list against the
code rather than reading it. -
groove searchignoredrerank_by_default. The key decided whether
servereranked every call; the command line did not read it at all. One
groove.tomlcarryingreranker = "bge-v2-m3"beside
rerank_by_default = falsetherefore reranked from the CLI and did not rerank
from the server — and three of the shipped deployment recipes are that exact
pair. The difference is not subtle: measured here on a warm cache, the same
query took 7 seconds without the cross-encoder and 72 with it.This changes behaviour. With
rerank_by_default = falsenext to a
reranker,groove searchno longer reranks. Naming a model on the command
line opts a single query back in —--reranker bge-v2-m3— and
--reranker noneopts a single query out, which is how a CLI argument has
always related to the file. No--rerankflag was added for it:
docs/stability.md freezes the MCPrerankparameter as
the per-call boolean and--rerankeras the model picker, and a--rerank
one letter away from it, taking a different type, would be frozen beside it
at 1.0.0.The decision now lives in one function both surfaces call. Each still spells
its own per-call override — a parameter on one side, naming a model on the
other — but what an override means, and what happens without one, is a
single expression. Writing that twice is how the two came apart to begin with.groove evalkeeps reading only--reranker, deliberately: its run
fingerprint records the model and not this key, so honouring it would let two
runs carry the same fingerprint while measuring different pipelines — and
--fail-on-regressionpicks its baseline by fingerprint equality. -
--min-confidence-ratioacceptednanandinf. A non-finite ratio
compares false against every score, so a value passed in order to tighten
the low-confidence check switched it off instead. The JSON echo could not
report that either: serde writes a non-finite float asnull, and the
null-stripping pass then drops the key, leaving output with no trace of the
override. The flag now requires a finite value>= 0.0—0.0is still how
the check is disabled — and rejects before any model is loaded.
[search].min_confidence_ratioingroove.tomlis held to the same rule by
the same predicate, which matters because that is the pathservereads. The
MCP parameter is unchanged: it cannot refuse a value mid-conversation, so it
substitutes — a non-finite ratio is logged and replaced by the server's own,
and a negative one is clamped to0.0. -
docs/usage.mdsaid the CLI and the MCP tool answer with the same JSON.
The wrapper is the same —results,low_confidence,filter_applied— but
the hits are not: an MCP hit also carries auriwhen the document is one the
server will hand over, and a CLI hit never does. The sentence now says which
part is shared and links to where theurirule is written down. -
docs/stability.mddescribed a flag that does not exist. It offered
--verboseas the way to get more detail;groovehas never had one.
Verbosity comes fromRUST_LOG, which appeared nowhere in the documentation.
The paragraph names the real mechanism now.
Removed
-
/api/search. It acceptedqueryandlimit— 2 of the 17 parameters
the MCPsearchtool takes — so/mcpwas already the better endpoint for
anything outside the process, and/uiuses/mcpnow. The endpoint was
declared unstable in docs/stability.md, and this removes
it before 1.0.0 rather than during it.If you were calling it directly,
/mcpanswers the same query with the whole
parameter set and no session handshake — see the request shape in the
/uisource (grooveseek/src/transport/webui_index.html), which is now the
smallest working example of an MCP client over Streamable HTTP.
Security
-
The HTTP transport now validates the
Originheader, which it never did.
The MCP specification's Streamable HTTP section states that a server "MUST
validate theOriginheader on all incoming connections to prevent DNS
rebinding attacks". rmcp implements the check but defaults it to an empty
list, which means do not validate, and groove never set it — so every release
up to and including v0.26.0 accepted anyOrigin. Measured against a running
v0.26.0 daemon:Origin: http://evil.examplewas answered normally.The default is now the loopback origins for whichever port is bound
(http://localhost:PORT,http://127.0.0.1:PORT,http://[::1]:PORT).This does not break existing clients. Per RFC 6454 a request that carries
noOriginheader passes, and ordinary MCP clients, the tray andcurlsend
none. What it stops is a web page open in the operator's own browser reaching
/mcpcross-origin. It is not authentication, and groove still has none.It covers
/mcp, and/uisearches through/mcp(see below), so this
list decides whether the built-in page can query./api/admin/statushas no
Origincheck of its own; it is restricted by requiring a loopback peer,
which is not configurable.
grooveseek 0.27.0
Download grooveseek 0.27.0
| File | Platform | Checksum |
|---|---|---|
| grooveseek-aarch64-apple-darwin.tar.xz | Apple Silicon macOS | checksum |
| grooveseek-x86_64-pc-windows-msvc.zip | x64 Windows | checksum |
| grooveseek-aarch64-unknown-linux-gnu.tar.xz | ARM64 Linux | checksum |
| grooveseek-x86_64-unknown-linux-gnu.tar.xz | x64 Linux | checksum |
groove-tray 0.27.0
Download groove-tray 0.27.0
| File | Platform | Checksum |
|---|---|---|
| groove-tray-x86_64-pc-windows-msvc.zip | x64 Windows | checksum |
groove-svc 0.27.0
Download groove-svc 0.27.0
| File | Platform | Checksum |
|---|---|---|
| groove-svc-x86_64-pc-windows-msvc.zip | x64 Windows | checksum |