Skip to content

0.27.0 - 2026-08-18

Choose a tag to compare

@github-actions github-actions released this 18 Aug 11:15
bb764a2

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, and jekyll-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/ and groove.toml.example — and would have resolved to
    nothing on a site whose root is docs/. 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 is publish = false, so both
    would be false.

    Images are referenced by absolute URL rather than repository-relative path,
    because a release archive ships this README without assets/ — 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 to raw.githubusercontent.com, which is reported to serve .svg as
    text/plain so an <img> will not render it, and the screenshots are PNG in
    any case. assets/README.md records 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_public and max_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 a groove.toml beside 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 from search and graph (fields may be added, so
    ignore ones you do not recognise), MCP tool and prompt names with their schemas,
    the kb:// resource scheme, /mcp and /healthz, configuration keys and
    defaults, the default embedding model, and the names written into your
    filesystem.

    Explicitly not stable: /ui and /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 uninstall and service status take --service-name
    instead of a positional.
    install, tray-install and tray-uninstall
    already named the instance with a flag, so the same thing had two spellings —
    install --service-name work against uninstall 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
    install refuses can never have been installed, so nothing that used to work
    stops working.

  • docs/stability.md now says what it freezes, rather than leaving it to be
    inferred.

    Which flags. The promise is scoped to the groove binary 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 --format is listed in one of two
    groups, because nine of them were in neither and silence reads as a promise.
    The JSON of search, graph, doctor and validate is stable, as is
    validate --format github. Text output is not, from any subcommand; neither
    are graph --format dot and --format svg, which are drawings; neither is
    the JSON of eval and tune, whose numbers are expected to improve — eval
    already stamps its history with a metric_version for that reason.

    Which channel. The stdout/stderr split is stated as it actually is. Six
    subcommands produce a result on stdout; index, status and service write
    everything to stderr, so groove 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 --exclude is now
    --exclude-paths, matching the tool's exclude_paths; and the tool's path
    is now start, matching groove graph --start. The tool took the flag's word
    rather than the other way round, because --path beside --kb-path reads as
    the corpus, and get_document keeps path for 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_graph is
    groove graph), and rerank is a per-call boolean while --reranker picks a
    model. Neither shape is unusual — gh --label fills the REST API's labels,
    and docker --publish fills Compose's ports — 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_strategy now
    takes all_chunks and all-chunks on both sides. Copying either spelling
    from one surface to the other used to be rejected — by clap on one side and by
    unknown seed_strategy on the other. There is one table of accepted
    spellings and both parsers read it, so a strategy cannot become reachable on
    one surface only; --help still 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.md stopped calling /ui a disposable placeholder. It
    still described the file as "a disposable placeholder — a proper redesign is
    expected in Phase 3+" after that redesign had shipped.

  • /ui shows the knowledge-base path the way it was typed. Windows
    canonicalisation returns an extended-length path, so the status band read
    \\?\C:\notes where the operator had passed C:\notes. The prefix is now
    stripped for display only; /api/admin/status still 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 under docs/.
    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 into docs/configuration.md, docs/usage.md,
    docs/clients.md, docs/mcp-tools.md and docs/behavior.md (each with its
    .ja.md pair), 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-discovery now 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 no docs/, so the
    Documentation section says so and gives an absolute URL, and groove --help
    now names that URL rather than a path the reader may not have.

  • /ui is 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 with textContent.

    Routing its search through /mcp rather than a private endpoint means the
    page exercises the same surface an external client would, and puts /ui
    under Origin validation for the first time
    . With the default list it
    works; an allowed_origins that 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.

  • grooveseek is marked publish = false. The Rust API is not part of the 1.0
    promise, and cargo package cannot succeed anyway while the workspace uses
    unversioned path dependencies. cargo publish now refuses rather than relying on
    a documentation note. [package.metadata.dist] dist = true was 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 let modle = "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 serve asked IpAddr::is_loopback, which says no; and
    groove service install matched 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]:PORT no 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 --bind now 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
    Host validation and the session cap are not authentication. Same text in
    groove serve and groove service install.

  • The admin web surface is now documented as scheduled to go away.
    docs/stability.md records the intent to retire /ui during 1.x, once a
    client that speaks /mcp exists — browsing belongs there, where every tool
    and every search parameter is reachable and the surface is already stable.
    /api/admin/status stays: 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 the Host half. 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.md said 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.md froze an environment variable the binary does not read.
    GROOVE_BIN is a variable of the shipped example hook. The three the binary
    actually reads are GROOVE_CONFIG_HOME, GROOVE_TRAY_LOG, and fastembed's own
    FASTEMBED_CACHE_DIR, which is not ours to freeze. Same shape as the
    --verbose entry below, found the same way — by checking the list against the
    code rather than reading it.

  • groove search ignored rerank_by_default. The key decided whether
    serve reranked every call; the command line did not read it at all. One
    groove.toml carrying reranker = "bge-v2-m3" beside
    rerank_by_default = false therefore 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 = false next to a
    reranker, groove search no longer reranks. Naming a model on the command
    line opts a single query back in — --reranker bge-v2-m3 — and
    --reranker none opts a single query out, which is how a CLI argument has
    always related to the file. No --rerank flag was added for it:
    docs/stability.md freezes the MCP rerank parameter as
    the per-call boolean and --reranker as 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 eval keeps 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-regression picks its baseline by fingerprint equality.

  • --min-confidence-ratio accepted nan and inf. 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 as null, 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.00.0 is still how
    the check is disabled — and rejects before any model is loaded.
    [search].min_confidence_ratio in groove.toml is held to the same rule by
    the same predicate, which matters because that is the path serve reads. 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 to 0.0.

  • docs/usage.md said 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 a uri when 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 the uri rule is written down.

  • docs/stability.md described a flag that does not exist. It offered
    --verbose as the way to get more detail; groove has never had one.
    Verbosity comes from RUST_LOG, which appeared nowhere in the documentation.
    The paragraph names the real mechanism now.

Removed

  • /api/search. It accepted query and limit — 2 of the 17 parameters
    the MCP search tool takes — so /mcp was already the better endpoint for
    anything outside the process, and /ui uses /mcp now. 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, /mcp answers the same query with the whole
    parameter set and no session handshake — see the request shape in the
    /ui source (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 Origin header, which it never did.
    The MCP specification's Streamable HTTP section states that a server "MUST
    validate the Origin header 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 any Origin. Measured against a running
    v0.26.0 daemon: Origin: http://evil.example was 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
    no Origin header passes, and ordinary MCP clients, the tray and curl send
    none. What it stops is a web page open in the operator's own browser reaching
    /mcp cross-origin. It is not authentication, and groove still has none.

    It covers /mcp, and /ui searches through /mcp (see below), so this
    list decides whether the built-in page can query. /api/admin/status has no
    Origin check 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