Elyra
Elyra The coding agent eTerm The terminal that knows where each command ends Starf An activity monitor for Apple silicon that never invents a number Litr A small, native web browser for macOS Notr A notebook for macOS e The native code editor Elyra Grove Native local development environment Askr The real server for Laravel & PHP Elyra Framework Rust + Svelte 5 framework for desktop apps Elyra Conductor Local project conductor Refr Local-first PDF workspace for macOS Elyra SQL Server MySQL-compatible SQL server in Rust Elyra Félagi Agents as teammates on one board Elyra SQL Client Native desktop SQL workbench Elyra SQL Anywhere Replication-ready SQL engine Elyra Sjá SEO & GEO workspace for macOS Elyra DataGrid Server-driven data grid for Laravel
Internals
Architecture
Release notes
Changelog
Elyra

Changelog

All notable changes to Elyra Grove are documented in this file.

The format is based on Keep a Changelog, and this project adheres to Semantic Versioning.

Unreleased

Security

  • ElyraSQL is pinned to 1.12.0 (was 1.11.4). It fixes two ways around column grants: a user granted some columns of a table could read the others through UNION or copy them out with INSERT ... SELECT, and a replica did not enforce grants or schema changes made on its primary after it started. Grove runs ElyraSQL without accounts on loopback, so Grove itself was not exposed; a project that created accounts in its ElyraSQL was. The daemon fetches 1.12.0 beside the data and switches to it, as for 1.11.4; the database file is not touched.

Changed

  • ElyraSQL 1.12.0 behaves more like MySQL in two ways a project can see. AVG over an integer column returns DECIMAL rather than DOUBLE, so code that decodes it strictly as a float (sqlx f64) must accept a decimal. And an account made with CREATE USER starts with no privileges, as in MySQL, instead of reading every table: grant it what it needs. It is also much faster: point queries use less than half the CPU, and LOAD DATA is 2.4x quicker.

1.10.1 — 2026-09-26

A security update for ElyraSQL, and the macOS downloads that 1.10.0 and earlier left unnotarized: the standalone CLI, which Gatekeeper killed when it came from a browser, and the disk image the app ships in.

Upgrade notes

  • ElyraSQL moves to 1.11.4 on its own. When the daemon starts, it sees the 1.11.3 build beside your data, fetches 1.11.4 in the background, and starts it if it was running before. Your database file is not touched. The old build stays on disk until you remove services/elyrasql/elyrasql-1.11.3-*.
  • Nothing else needs doing. The app updates itself as before.

Fixed

  • A service whose pinned version moves is upgraded instead of dropped. Service builds unpack to a directory named for their version, so moving ElyraSQL's pin from 1.11.3 to 1.11.4 would have left it looking uninstalled. Autostart would have skipped it silently, and grove service start would have said "not installed", with the old build and the data still in place. At startup the daemon now looks for an older build of each service that is not installed.

    • A patch release of what you had: it installs the pinned build in the background and starts it the way it ran before. Autostart and on-demand settings are kept.
    • A minor or major move: it does nothing and logs the command to run. MySQL and PostgreSQL data directories do not always carry across such a move.

    Verified by installing ElyraSQL 1.11.3 with the released 1.10.0 in a scratch home, then starting this daemon on it. It fetched 1.11.4 and started it from the new directory on the same port, and the same .edb (same inode and size) and a marker file in data/ were untouched.

  • The standalone macOS CLI is signed and notarized. The grove-<version>-aarch64-apple-darwin.tar.gz download held ad hoc signed grove and grove-tunnel binaries. Fetched with a browser, they are quarantined, and Gatekeeper killed them on first run: 1.10.0's grove --version exited 137 that way. The release now signs both with the Developer ID and the hardened runtime and notarizes them. Before publishing, it runs each binary quarantined, the way a browser download would, and stops if Gatekeeper refuses. The CLI inside Grove.app was already signed and notarized.

  • The .dmg itself is notarized and stapled. Tauri notarizes the app inside it but not the disk image, which was Developer ID signed and unnotarized, so spctl rejected it. The release now notarizes and staples the DMG and checks it with spctl before uploading.

  • The release workflow can be dry-run. workflow_dispatch on any branch runs the same build, signing and notarization, publishes nothing, and attaches the results to the run.

Security

  • ElyraSQL is pinned to 1.11.4 (was 1.11.3), for RUSTSEC-2026-0285 in rustls: its TLS 1.3 handshake accepted handshake messages across encryption-level boundaries. ElyraSQL terminates client TLS with rustls, and every release through 1.11.3 shipped an affected version. The archive unpacks to a versioned directory beside the data directory, so the new build leaves an existing database alone. The daemon fetches it itself; see the upgrade notes.

1.10.0 — 2026-09-25

Six things Grove can do because every request and every database goes through it. Another branch can run beside yours, with its own site and its own copy of your database. A coding agent can get the same thing as a sandbox over MCP. grove bisect finds the commit that broke a recorded request. A replay can start from the same data every time. grove routes notices when a route gets slower. And an idle on-demand database starts as soon as its site is looked up, before PHP asks for it.

Upgrade notes

  • Nothing needs doing. The daemon and CLI talk the same protocol as 1.9.0 plus new requests, so an older GUI or CLI keeps working against a new daemon.
  • Grove now keeps route timings on disk. It writes $GROVE_HOME/routes.json (owner-only) every 30 seconds while anything changed. The file holds each route's method, its path with ids folded to placeholders, and recent durations, with no query strings, headers or bodies. grove routes --reset empties it.
  • grove try and grove bisect put worktrees in ~/.grove/try/. Try databases are named <db>__gt_<hash>. Both commands take away what they made, grove try --done when you are finished and grove bisect at the end of every run.
  • The sandbox tools exist only with grove mcp --allow-write. A read-only MCP server gains one tool, grove_routes.

Added

  • grove routes: which route got slower. Grove times every request that PHP or an upstream answers and keeps the times per route. A route is the method plus the path, with ids, UUIDs, ULIDs and long hashes folded to placeholders, so /orders/17 and /orders/18 count as one route. When a change makes a route slower, the daemon logs it and grove routes shows it, with a request id to pass to grove explain:

    $ grove routes
    SITE             ROUTE                                          SEEN  TYPICAL   RECENT
    shop             GET /orders/{id}                                 26     21ms    222ms  slower 10.6× for 0s, e.g. request #66
    shop             GET /                                            20     21ms     21ms
    

    Typical is the median of the route's last 50 requests, and recent is the median of its last five. Because both are medians, a single slow request (a database starting on demand, or OPcache recompiling a file) moves nothing. A route is flagged when recent is at least twice typical and at least 50 ms more. It is judged only after it has ten requests behind it.

    While a route is flagged, its baseline takes no new samples, so a regression does not quietly become the new normal. The flag clears when the route is fast again. grove routes <site> --reset --route '<route>' accepts the new speed instead.

    Only answers below 400 count, because an error's speed says nothing about the route. Static files do not count either. The timings are saved to routes.json every 30 seconds and at shutdown, so they survive a restart. The MCP tool grove_routes gives an agent the same numbers, so it can check whether its change slowed a route.

    A slowdown that creeps in a few percent at a time is absorbed into the baseline and never flagged. This catches a route that jumps, like an N+1 query or a dropped index.

    Verified live with a route that went from 21 ms to 222 ms: it was flagged on the third slow request. The baseline stayed at 21 ms through 50 more slow requests, the flag cleared once the route was fast again, and the baselines survived a restart.

  • grove replay <id> --same-data: replay against the same data every time. A request that writes finds what it wrote last time when you replay it: the order exists, the email is taken, the webhook was already applied. With --same-data the first replay snapshots the site's database and every later one puts it back before it sends, so you can change the code and try the same request again from the same starting point as often as you need.

    $ grove replay 1 --same-data
    baseline taken of MySQL `shop`; later --same-data replays start from it
    replayed → 200 in 1ms (see it in `grove requests`)
    $ grove replay 1 --same-data
    MySQL `shop` put back to the baseline first
    replayed → 200 in 1ms (see it in `grove requests`)
    

    MySQL on Grove's own server uses the ordinary snapshot store (it shows in grove db list), and SQLite gets a copy of the file with its -wal and -shm. grove replay <id> --forget drops the baseline. The starting point is the data as it is at that first --same-data replay, not as it was when the request was first made. A failed request that rolled back left nothing behind, so for a failure the two are the same. If the original request did write something, undo that before the first --same-data. Baselines are keyed by request id and held in memory like the request log, so a daemon restart forgets both and deletes the files at startup. Verified live on MySQL and SQLite with a POST that inserts a row: three --same-data replays left one row more than the baseline, not three.

  • grove bisect: which commit broke this request? Give it a commit where a request worked and one of the requests Grove recorded (grove requests). It checks each commit out beside your checkout at <site>--bisect.test, gives it a fresh copy of your database migrated to that commit, runs composer install only when composer.lock changed, replays the request with its method, path, headers and body, and lets git bisect narrow it down. Your checkout never moves.

    $ grove bisect --good 3f2a9c0 --request 1
    COMMIT     STATUS       SUBJECT
    59f8d721a3 500    bad   c8: notes
    aba631d973 200    good  c4: more notes
    fcb901b823 500    bad   c6: rename total to amount in the report
    b540fc73ef 200    good  c5: report sums invoice totals
    
    first bad commit: fcb901b823 c6: rename total to amount in the report
    

    A commit is good when the replay answers below 500, or exactly --expect-status when given. One whose migrations or dependencies do not install is skipped. It first checks that the request really fails at the bad end and says so if it does not. Everything it set up is taken down at the end, including when a step fails. Each step waits three seconds for OPcache to notice the new files, because OPcache counts its two-second revalidation window in whole seconds. At 2.1 s the previous commit's compiled code answered, and a first version blamed a commit that only touched a text file. Verified on an eight-commit history with a real bug in the sixth: found in three of three runs, 13 s each.

  • Sandboxes for coding agents. grove mcp --allow-write offers grove_sandbox_open and grove_sandbox_close, plus grove_sandbox_list without the flag. An agent gets a complete running copy of a site to work in: a new branch from the site's current commit in its own worktree, its own copy of the database, migrated, at its own HTTPS URL. Your checkout and your database are never involved. Every existing tool takes the sandbox's site name, so the agent can run its migrations through Grove and query what the running app did against its own data. Closing removes the site, the database copy and the worktree and keeps the branch with the agent's commits. It refuses, naming the files, while anything is uncommitted. Built on grove try, which gained --new for the same thing by hand. Verified over a real MCP session: opened in 0.8 s, a migration and a query in the sandbox, the branch and its commit still there after closing, and the user's database and checkout unchanged.

  • grove try <branch>: another branch running beside yours. Reviewing a colleague's branch no longer means stashing, checking out and migrating your own database into their schema. A try is a second checkout, and your checkout and your database are never touched:

    $ grove try feature/invoices
    ✓ feature/invoices is running at https://myapp--feature-invoices.test
      code:     ~/.grove/try/myapp/feature-invoices
      database: mysql myapp__gt_f38b5863
      done:     grove try --done feature/invoices
    

    It makes a git worktree, fetching the branch from origin if it is not local. It copies your .env into it with the site's hostname moved everywhere it appears (APP_URL, SESSION_DOMAIN, SANCTUM_STATEFUL_DOMAINS, subdomains) and DB_DATABASE pointed at the try's own copy of your database. That copy is a new schema on Grove's MySQL, or a copy of the SQLite file. vendor/, node_modules/ and public/build/ are cloned from your checkout (copy-on-write on APFS, so seconds and no disk). Then composer install catches up with the branch's lock file, the branch's migrations run, and the try is linked on the same PHP, secured if your site is. The name uses a double hyphen and no dot, because every subdomain of a site routes to that site. A try whose setup fails part-way is undone completely. --done removes the site, the try's database and the worktree, and refuses while the worktree holds uncommitted work, naming the files, unless --force. --list shows what is running. The try's database is refused, not shared, for anything but MySQL on Grove's own server or SQLite.

  • An idle on-demand database starts when its site is looked up. Two earlier signals than PHP's first connection now start it. One is the DNS lookup of the site's name: a browser resolves myapp.test before it connects, and often while the address is still being typed. The other is the request itself. Either maps the site to the bundled services its .env uses, matching loopback and Grove's own ports and counting Redis only when cache, sessions or queues use it, and starts the idle ones in the background through the same gate a connection takes. Neither DNS nor the request waits for it. Measured on a page that spends 40 ms before its first query, about what Laravel does on the sites this was built against:

    first request after MySQL went idle
    before 354 ms
    the request alone as the signal 302 ms
    a lookup 0.4 s ahead 43 ms, the same as with MySQL already up

    How much lead a lookup gives depends on the browser. Typing the address can give plenty, and a plain reload gives a few milliseconds, which is then worth the same ~50 ms as the request. Grove's answers have a five-minute TTL, shorter than the default ten-minute idle period, so the lookup reaches Grove in exactly the case where the database has stopped.

1.9.0 — 2026-09-23

Two things Grove can do because it runs your databases, not just points at them. Each git branch can have its own database, swapped in when you check the branch out. A database can also run only while something is connected: an idle MySQL that sat at 500–700 MB costs nothing until the next query, and that query waits a third of a second. Around those, the root code path that 1.8.0 made unnecessary is gone, and three places that reported success for something that had not happened now report what did.

Upgrade notes

  • The daemon now refuses to run as root. 1.8.0 moved it to your user; 1.9.0 deletes the machinery that let a root daemon run safely, so a root daemon would start PHP-FPM and your databases as root out of a directory you can write. If you updated to 1.8.0 and ran sudo grove install afterwards, nothing changes. If you did not — including coming straight from 1.7.x — the daemon stops at startup with:

    Error: the Grove daemon will not run as root.
    … Run `sudo grove install` to rewrite the service so it starts as you.
    

    Run sudo grove install once and it starts. Nothing else needs doing: your sites, databases and certificates are where they were.

  • On-demand and branch databases are opt-in. Nothing about an existing service or database changes until you run grove service on-demand <key> on or grove db branches on.

Added

  • Databases that run only while something is connected. grove service on-demand mysql on hands the port to the daemon. The server does not run until the first connection arrives, starts behind that connection, and stops cleanly once nothing has been connected for the idle period (default 10 minutes, --idle 30m). Measured here, an idle MySQL that sat at 517 MB went to nothing. The first connection after it stopped was answered in 0.35 s including the server start, and every later query ran as before. Twenty clients hitting an idle server at the same moment got twenty answers from one start. The mode survives a daemon restart. Autostart leaves on-demand servers alone and the port is held from the moment the daemon is up. Works for MySQL, PostgreSQL, ElyraSQL and Redis. grove service list shows idle for a stopped on-demand server and the mode in a new column.

    Every client goes through the daemon, which is what makes the idle decision sound: its count of open connections is the count of clients. For the same reason the server's unix socket moves to a private name in this mode and none is advertised. A client on the socket would go uncounted and be cut off when the server went idle. None of the 90 projects on the machine this was built on connect that way. Grove's own snapshots, restores and branch-database switches go through the same port, so they start the server when they need it. An idle server is stopped with SIGTERM and fifteen seconds to finish, not SIGKILL, so MySQL shuts down clean and does not replay its redo log on the next start. grove service on-demand mysql off gives the port back and runs the server all the time again.

  • A database per git branch. grove db branches on in a project, and checking out a branch swaps in that branch's database: a feature branch's migrations no longer land in main's tables, and going back to main brings its data back exactly as it was left. The first checkout of a new branch starts it from the data you were on. The database keeps its name throughout and only its contents move, so the app, php artisan in a terminal, a test run and a database client all see the checked-out branch without being told. Pointing the app at a different database per branch would only have reached the requests Grove proxies.

    On MySQL a swap is a single RENAME TABLE moving every table between the live schema and the branch's parked one, atomically; measured at 43–46 ms for a 70 MB schema, the same as for an empty one. The first visit copies, and took 831 ms for 300 000 rows. On SQLite a swap is two renames of the database and its -wal/-shm. During a switch the site answers 503 with Retry-After and its grove dev processes restart, so a queue worker comes back on the new branch's code and data. A detached HEAD — a rebase, a bisect — moves nothing, and a switch waits until a checkout has settled for a second.

    A switch writes its intent before anything moves, so one interrupted by a crash is finished or undone when the daemon next starts. MySQL on Grove's own server and SQLite are supported; a remote MySQL, PostgreSQL and ElyraSQL are refused with a message, as is a MySQL database with views, triggers, stored routines or events, and two sites (worktrees) following one database. grove db branches shows what is live and parked, and flags copies whose git branch is gone; off keeps the copies and on picks them up again; drop is the only thing that deletes one.

Removed

  • The privilege-dropping machinery, and the daemon's ability to run as root. 1.8.0 moved the daemon to the login user; this deletes what that made unnecessary. grove_core::privdrop is gone, and with it the pre_exec block that called setgroups/setgid/setuid between fork and exec — the most delicate unsafe in the workspace — along with its twenty-odd call sites across PHP-FPM, the runtime probes, the scaffolding tools and every bundled service. Two hand-rolled copies of geteuid went too, as did --allow-to-run-as-root and the user/listen.owner pool directives that only ever meant anything to a root master. Net 433 lines.

    What survives is the half the privileged commands still need, now called grove_core::ownership: sudo grove install creates files as root and has to hand them to the user who will read them.

  • crates/grove-core/tests/privdrop_root.rs and crates/grove-runtime/tests/probe_root.rs, which proved a drop that no longer happens. ca_ownership_root.rs stays: which user owns the CA key after a root install is still only observable as root.

Changed

  • Dependencies: rustls 0.23.45 with rustls-webpki 0.103.15, the verifier the CA name-constraint tests run against, which passed on the new version. hickory-dns 0.26.3 fixes regressions from 0.26.2 in DNSSEC validation and delegation. Also vite 8.3.0 in the app's frontend and clap 4.6.7.

  • The daemon refuses to start as root, naming sudo grove install as the fix, and exits non-zero so the service manager does not treat it as a successful start. It has no use for privilege — launchd and systemd hand it the ports — and what it would do with privilege is exec PHP-FPM and your databases out of a directory you can write, which is exactly the escalation the deleted machinery existed to prevent. Not starting is recoverable in one command; running your sites as root is not. The only way to reach it is a unit written before 1.8.0.

  • The IPC socket is no longer a privilege boundary, and says so. It still refuses other local users, by mode and by peer credentials, because another account has no business restarting your daemon or dumping your databases. What it no longer guards is a root-privileged command surface, because there is not one.

Fixed

  • grove service start said "started" for a server that had already died. A successful spawn is a process that exists, not a server that runs: mysqld with no data directory, Postgres with a stale lock file or a port someone else holds each exit within a second, after Grove had reported success. Start now waits until the port accepts a connection, up to 30 seconds, and a server that exits before that fails the command with its own error lines from the log:

    ✗ MySQL exited while starting (exit status: 1):
      [ERROR] [MY-013276] Failed to set datadir to '…/services/mysql/data/' (OS errno: 2 - No such file or directory)
      [ERROR] [MY-010119] Aborting
    

    A port that is already taken is refused before anything starts, naming what holds it. Without that the readiness check would connect to the other server and call this one up. The fixed 1.5 s sleep before a snapshot or restore went too, since start now returns when the server is ready. That was 0.35 s for MySQL here.

  • Two snapshots in the same second overwrote each other. Ids are to-the-second timestamps, and the file name is built from the id, so the second dump replaced the first on disk while the index kept both entries, pointing at the same file under two notes. A sandboxed migration snapshots before it runs, so two in a row was enough. A taken id now gets a -2, -3 suffix, reserved across concurrent requests.

  • Restoring a MySQL snapshot left tables that were created after it. A mysqldump --databases file recreates the tables it holds and says nothing about any others, so a restore after a migration that created invoices still had invoices. That is the exact case the sandboxed-migration tool snapshots for. Each database in the dump is now dropped and recreated on the way in, so it comes back exactly as it was. MySQL's own system schemas are never dropped, databases the dump does not contain are left alone, and snapshots taken before this change get the same treatment because it happens at restore time. Restoring an id that does not exist now says so, instead of unknown service "snapshot …".

1.8.0 — 2026-09-14

The daemon stops being root. Binding a port below 1024 needs privilege; serving on one does not, so launchd and systemd now bind 53, 80 and 443 while they are root and hand the listening descriptors to a process that never had privilege at all. What is left that genuinely needs root is one-off and visible: writing the unit, /etc/resolver, the system trust store.

Everything else here follows from that, or from re-checking what 1.5.0 claimed: grove doctor re-runs those invariants on every start, and the central one — that a leaked Grove CA key cannot mint a certificate for anything outside your TLD — stops being a doc comment and becomes a test through the same verifier browsers use.

Upgrade notes

  • sudo grove install once, to move the daemon off root. Nothing happens until you run it: an existing install keeps its old unit and its root daemon, and everything works as before. Running it rewrites the unit so launchd or systemd binds the ports and starts the daemon as you, and hands you the Grove home in the same step — on a machine in daily use that is thousands of files, including config.toml, every PHP build and the CA key, so it is one chown -R rather than a surprise later. If you skip that step and the daemon ends up as you with a tree root owns, it says so at startup and grove doctor repeats it on the grove-home line.
  • The CA private key becomes yours (0600, owned by the run user) instead of root's. The daemon signs leaf certificates and the daemon is no longer root, so a key it cannot read is HTTPS that does not work. What that costs is in docs/ARCHITECTURE.md under Trust boundaries, in short: code running as you can now read it, bounded by the CA's name constraint to the configured TLD and by those names resolving to loopback — while the root-privileged IPC surface that same code could already reach disappears entirely.
  • Rolling back is safe. A root daemon reads a user-owned Grove home without trouble, so installing an older Grove and running its sudo grove install puts everything back.

Added

  • The daemon runs as you. The unit carries UserName on launchd and User= on systemd, so from its first instruction the process that serves your sites is the login user. It works because the service manager already binds the privileged ports and hands the descriptors over; there is nothing left that needs privilege. What still needs root is one-off and visible: writing the unit, /etc/resolver, the system trust store.

    Consequences worth naming. grove restart no longer asks launchd to kickstart a system job, which it could not do unprivileged; it exits and KeepAlive brings it back, and because launchd keeps holding the listening sockets the ports are never released, so the restart race that could leave an unprivileged daemon serving nothing is gone. The privilege-dropping machinery every spawn went through turns itself off when it finds it is not root, so php-fpm, PostgreSQL, MySQL and Redis inherit your identity instead of being dropped to it. A machine where grove install cannot work out who to serve still runs the daemon as root, exactly as before.

  • The service manager binds the privileged ports, and hands them over. Binding 53, 80 and 443 needs root; serving on them does not. grove install now writes a Sockets dictionary into the launchd plist and a companion grove.socket unit for systemd, so launchd or systemd binds those ports while it is root and passes the listening descriptors to the daemon. The daemon asks for each port before binding it and serves on whatever it is given — the same accept loops, from a descriptor it did not create. This is the groundwork for a daemon that does not run as root at all; it does not yet change who the daemon runs as.

    Nothing requires the handover. Grove updates itself without rewriting the unit, so a daemon that insisted on delivered sockets would break every existing install the day it shipped: a port nobody handed over is bound by the daemon exactly as before, and the two DNS halves are adopted independently. Running sudo grove install is what switches a machine over. grove doctor says which sockets arrived, on the privileges line, and no longer warns about an unprivileged daemon that was handed :80.

  • grove doctor re-checks the 1.5.0 invariants on every run. Four new lines: ipc-socket (the daemon's socket must not be world-accessible), grove-home (the tree root reads binaries out of must not be world-writable; warns when root owns it), site-certs (how many leaves, soonest expiry, expired ones noted as reissued on next request), and trust-store — is the CA on disk what the system trusts, and is it the only Grove CA trusted. The last catches what grove ca rotate without sudo used to leave behind: an old, unconstrained CA the machine still believes, able to sign any hostname. It names the stale entry's own removal command (security delete-certificate -Z <sha1> on macOS, the anchor path on Linux). Believes, not merely stores: a rotate strips the old CA's trust settings and leaves the certificate in the keychain, so the check asks the system's trust evaluator rather than searching the keychain, and an inert leftover raises nothing. root-ca-scope likewise reads the NameConstraints extension off the certificate instead of trusting ca-meta.json beside it, so a note left over from an earlier CA cannot vouch for one that constrains nothing. Runtime-hash verification is not among them yet: there is no recorded digest to compare against until runtimes are content-addressed.

  • Security invariants as tests. 1.5.0's central claim — a leaked Grove CA key cannot mint a certificate for anything outside the configured TLD that a machine trusting the CA will accept — lived in two doc comments. It is now a regression test through rustls + webpki, the same verifier and rules browsers apply, in three parts: a Grove-signed leaf for google.com is refused; the same leaf from a CA without the constraint is accepted (so the refusal is the extension's doing, not an expired date or a bad chain); and the certificates Grove actually issues are accepted, wildcard subdomains included. A fourth test pins that the constraint is a DNS suffix (myapp.localtest is outside .test). And the request-path sanitizer — the one function between an attacker's URL and document_root.join(…) — gets property tests: for any string, the result is relative, only ordinary components, stays under the root, and sanitizing twice changes nothing; a model-based test checks the stack semantics and the dotfile rule over thousands of traversal-shaped inputs.

Changed

  • ElyraSQL is pinned to 1.11.3 (was 1.11.2). Upstream fixes two things a Grove site can hit: SET @var inside a stored procedure now reaches the session instead of vanishing when CALL returns, and the expression-depth limit follows the calling thread's stack, so a deeply nested expression is refused cleanly rather than overflowing a small stack and aborting the process. It also adds UTC_TIMESTAMP()/CONVERT_TZ(), freezes NOW() to one instant per statement, and makes || honour PIPES_AS_CONCAT. The archive unpacks to a versioned directory beside the data directory, so grove service install elyrasql fetches the new build and the existing database is untouched.

1.7.1 — 2026-09-07

Fixes for what happens after an update: the daemon kept running the old version, and the tools for restarting it could make things worse.

Upgrade notes

  • Restart the daemon once after this update. 1.7.0's updater relaunches the app but not the daemon, so right after installing 1.7.1 the app will show The daemon is running v1.7.0 with a Restart daemon button — click it, or run grove restart. From 1.7.1 on, updates restart the daemon themselves.

Fixed

  • After the app updated itself, the daemon kept running the old version — silently. The updater relaunched the app but never restarted the daemon (a separate root process under launchd/systemd), and the app showed the old daemon's catalog with no hint why: 1.7.0's ElyraSQL was missing from Services until a manual restart. The app now restarts the daemon as part of installing an update, and shows a banner with a Restart daemon button whenever the daemon's version differs from its own — the check the CLI already made on every command.
  • grove restart under the system service could leave an unprivileged daemon behind. It stopped the daemon and spawned a new one from the shell, racing launchd's KeepAlive; whichever won, the other could not bind, and since 1.6.0 the supervisor's instance then refused to start over the shell's. With a service unit installed, restart now asks the daemon to have its supervisor re-exec it (the app's Restart button's path) and waits for it to go down and come back; start refuses to spawn beside an installed service and names the command that starts it.
  • Linux: grove restart left the daemon down. The unit said Restart=on-failure, and a deliberate restart is a clean exit. Now Restart=always.

1.7.0 — 2026-09-07

One addition: a fourth bundled database. Everything else from 1.6.0 stands.

Added

  • ElyraSQL as a bundled database. grove service install elyrasql downloads ElyraSQL — a MySQL-compatible SQL server in one static binary with the whole database in one file — verifies its published SHA-256, and runs it on 127.0.0.1:3307 beside MySQL. Your app uses the MySQL driver (DB_CONNECTION=mysql, DB_DATABASE=elyra); grove env prints the block. grove db snapshot --engine elyrasql takes a hot, consistent copy of the database file via BACKUP TO, and grove db restore puts it back. Grove tells an ElyraSQL site from a MySQL one by the port it connects to, so the agent-safe migration sandbox and grove bundle snapshot the right server. The convert tool takes it as a source or target. macOS (Apple silicon) and Linux; upstream publishes no Intel macOS build. Requires ElyraSQL 1.11.2: adding it surfaced that sqlx's MySQL driver — which every Rust client and Grove's convert tool sit on — could not connect to 1.11.1 at all, because its per-connection SET sql_mode=(SELECT CONCAT(@@sql_mode, …)), time_zone=… was refused; 1.11.2 accepts it.

1.6.0 — 2026-09-02

Everything from a robustness review of 1.5.0, in five rounds: one real bug in the request path, one upgrade hazard in 1.5.0's own instructions, a daemon that learns to say when it is not actually serving, a proxy that behaves the way browsers expect, a CLI that reports what happened rather than what it meant to do, and a Linux integration that exists.

Upgrade notes

  • grove doctor exits non-zero when anything fails, and a missing resolver or a port held by another server is a failure. A script gating on its exit code will start failing where it should have.
  • Secured sites redirect http:// to https:// (301 for GET/HEAD, 308 otherwise). A curl in a script or a webhook target that reached a grove secured site over plaintext should use the HTTPS URL.
  • grove uninstall needs sudo and leaves GROVE_HOME (config, PHP builds, databases) and the PATH shims in place unless you pass --purge.
  • grove init exits 1 when a step fails — a PHP download, the CA. The elevation notice is advice and does not fail the command.
  • Linux: re-run sudo grove install. The unit moved from a --user unit to /etc/systemd/system/grove.service. If an old user unit is present, install says so and gives the command to disable it.
  • After upgrading the binary, grove restart. The CLI now warns on every command when the daemon is a different version; the daemon answers a request it cannot parse with its version and the same advice.

Added

  • HTTP/2 on both listeners. ALPN offered only http/1.1, so browsers fell back to six connections per origin and loaded Vite modules in batches.
  • WebSocket passthrough for proxy sites. The listeners accepted upgrades all along, but nothing pumped the bytes: the browser got a 101 and a dead socket, so Vite HMR on a proxy site, Next/Nuxt dev servers and Reverb over wss://myapp.test reconnect-looped. Upgrades get a dedicated upstream connection and a bidirectional copy until either side closes.
  • http:// → https:// redirect for secured sites. site.secure was set by grove secure and never read on the request path; a typed hostname served plaintext, PHP saw HTTPS="", and Laravel built http:// asset URLs — mixed content against the HTTPS Vite server. The Location carries the configured HTTPS port.
  • Error pages that name the fix. Every Grove-generated error was a bare text/plain line — a Vite server that wasn't running showed the browser "client error (Connect): tcp connect error … (os error 61)". A refused upstream now names the URL and offers grove dev start <site>; an unknown host offers grove link; a missing PHP offers grove php install <v>.
  • Static files: Range requests (206), gzip, and the MIME types that were missing. Safari refused to play <video> without 206; a 2 MB dev bundle was a 2 MB transfer; sitemaps, PDFs, fonts beyond woff and source maps went out as application/octet-stream.
  • Dev-sized PHP defaults via the pool config: 512M uploads, post size and memory, 300s scripts, and a 600s request_terminate_timeout so a wedged worker is recycled. PHP's compiled-in 8M/128M/30s applied before, since Grove writes no php.ini. Set with php_value, so an app's own ini_set() wins.
  • grove status and grove doctor report what actually bound. Each of dns/http/https/mail is recorded at bind time and shown with the OS error and, where lsof/ss will say, the process holding the port: ○ http Address already in use — held by httpd (pid 412). Previously DNS was hardcoded as running and a failed bind was one line in daemon.log.
  • grove doctor works without the daemon. The config, CA and resolver checks run locally when the socket does not answer, followed by an explicit daemon failure, instead of the old "not running" and nothing else. It also reports state files that were set aside as unparsable.
  • A resolver check that asks the OS to resolve a name under the TLD and expects loopback back — the question a user actually has, and the one a VPN client rewriting DNS order breaks without touching /etc/resolver.
  • grove reload re-reads config.toml and rebuilds the site list without a restart. A grove link/secure that would overwrite a hand edit the daemon has not read refuses and points here; before, the edit was silently lost.
  • grove uninstall --purge also removes GROVE_HOME and the PATH shims. Without it, uninstall lists what it left in place and where.
  • A version check on every command. The CLI pings the daemon first and warns when the two versions differ, naming both and grove restart.
  • Orphans from a killed daemon are reaped at boot. php-fpm masters and bundled databases left running after a SIGKILL are found by pid file, checked by name, and stopped before anything new is spawned. Previously a restart spawned duplicates on the same socket, or failed the port bind and reported the service as "not running" while the orphan kept serving.
  • One daemon at a time. A second grove daemon refuses to start over a live one instead of unlinking its socket and overwriting its pidfile.
  • grove start parses config.toml first and shows the error with its line, instead of "daemon did not come up in time" — which under launchd's KeepAlive was a silent restart loop. When the daemon does fail to start, the last lines of its log are shown inline.
  • Linux, for real (beta). sudo grove install writes a system unit (root, children dropped to you) instead of a --user unit that could not bind 53/80/443; DNS is routed through systemd-resolved via a grove0 dummy link that the unit recreates on boot — the previous code referenced a link nothing created; the CA goes into the distro's store (Debian or p11-kit layouts) and into Chrome's and Firefox's NSS databases, which is where browsers on Linux actually look. The README badge says macOS | Linux (beta) and no longer claims Windows.
  • CI builds the GUI against the real grove-pro when the deploy key is available, with the release's dependency guard. 1.5.0's release broke on a dependency only that crate declared differently; nothing before release had compiled it.
  • Release pre-flight. The release job creates the GitHub Release before building anything — with RELEASE_TOKEN when set, else the Actions token — and fails immediately with the exact fix if neither may, instead of after a 14-minute build.
  • Daily PHP builds that build only what changed. The php-build workflow asks php.net for each minor's latest patch and skips minors already published, so a PHP security release lands in a day, not a month.
  • IPC protocol compatibility tests — the tag shape, unknown commands as named errors, unknown fields tolerated, older status shapes still parsing.

Changed

  • State files are written atomically (config.toml, php-builds.json, service and snapshot indexes): temp file, fsync, rename. A crash mid-write used to leave a truncated file.
  • A state file that does not parse is moved aside as <name>.corrupt-<ts> and logged, and Grove continues with defaults. Before, it was read as empty and then saved over — every Grove-installed PHP build forgotten, every database port back to default, silently.
  • Shutdown stops php-fpm pools and databases explicitly and gives in-flight requests a moment, instead of leaving children to runtime teardown and aborting listeners mid-request.
  • grove stop only signals a pid that is alive and is a Grove process. A stale pidfile from before a reboot is removed, not sent SIGTERM.
  • The IPC and mail-catcher accept loops back off on a transient error instead of exiting the daemon or spinning; a too-long GROVE_HOME fails with the path and the limit rather than a bare "path must be shorter than SUN_LEN".

Fixed

  • A php-fpm master that died stayed dead. The respawn worked, then the dropped old pool removed the socket file the new master had just bound — the same path. With a live child in the map nothing respawned again, and every request for that PHP version answered 502 until the daemon restarted. Two reviews had called the respawn path correct; both checked that a dead child is detected, not what happened to the one being replaced.
  • grove ca rotate without sudo broke HTTPS. It untrusted nothing (warn only), deleted the old CA, minted a new one, then failed to trust it: old CA still in the keychain, untrusted one on disk. It now refuses before touching anything. This is the command 1.5.0's upgrade notes tell everyone to run.
  • HTTP/2 requests routed as host "". h2 carries the host as :authority and has no Host header; the handler read only the header. Caught by the end-to-end smoke test the same day h2 was enabled, before release.
  • grove uninstall without sudo printed "removed" having removed nothing. Every step was let _ =. It now refuses without elevation, stops the daemon first, reports each step, and exits non-zero if any failed.
  • grove init exited 0 when the PHP download or CA generation failed. Under sudo, the config.toml it writes is now chowned to the invoking user, so a later plain grove init or grove import no longer fails on a root-owned file.
  • grove path show said Grove's toolchain was on PATH when Homebrew's php still won. It checked membership, not order; it now names the php that actually resolves first and how to move Grove ahead of it.
  • The SPA fallback served index.html as 200 text/html for any missing path, so a stale hashed asset produced "expected a JavaScript MIME type" instead of a 404 naming the file. It applies only to extension-less paths from clients that accept HTML.
  • The 2 GiB request-body limit was enforced only on chunked bodies; a declared Content-Length streamed through unchecked.
  • A hand edit to config.toml was silently overwritten by the next grove link; Request::Reload rebuilt from memory and never saw it.
  • A daemon that could not parse a request closed the connection, which the CLI reported as "connection closed before a full message was received".
  • Five user-visible messages whose line continuations had been flattened into runs of spaces (store). Restart Grove).
  • docs/INSTALL.md showed grove init and grove doctor output that was never what they printed — including ✓ resolver and ✓ dns doctor lines that did not exist. Both examples now come from real runs.

Security

  • Bumped h2 (RUSTSEC-2026-0258, unbounded empty DATA frames) before enabling HTTP/2, and plist/quick-xml (RUSTSEC-2026-0194/-0195, GUI only).

1.5.0 — 2026-08-25

This release closes every finding from a security review of the daemon, the proxy, the tunnel and the secret-sync client. Most of it needs nothing from you; these five do.

Upgrade notes

  • Re-run sudo grove install. It records the numeric run user in the service unit, which is what lets the daemon authorise its IPC socket and drop privileges precisely. Existing installs keep working without it, on a weaker signal.
  • sudo grove ca rotate, once. A certificate cannot gain a name constraint after the fact, so an existing root CA stays able to sign any hostname until it is replaced. grove doctor warns until you do. Every site's certificate is re-issued on next use.
  • grove-tunnel now requires --token. An empty token used to disable authentication, so the simplest way to start a server was also the open one. Pass --allow-anonymous to keep that behaviour deliberately.
  • grove secret share is now required to add a teammate. The client no longer accepts a recipient list the backend changed on its own, so a new member is refused until someone records the change locally.
  • grove request <id> --as curl now emits Authorization: [redacted]. The snippet will not reproduce an authenticated request until you put your own credential back. grove replay is unaffected and still works.

If you run the tunnel server, note that its control channel is still unencrypted — see the new Security section in TUNNEL.md.

Added

  • cpx is part of the bundled toolchain. grove path install now creates a cpx shim alongside php, composer, node, npm, npx and laravel, so the Composer Package Executor — npx, but for Composer packages — is simply on your PATH:

    $ cpx laravel/pint
    $ cpx friendsofphp/php-cs-fixer fix ./src
    $ cpx phpstan analyse
    

    Nothing to composer global require: Grove fetches the self-contained cpx PHAR on first use and runs it on the PHP the current directory resolves to (grove isolate / grove use). cpx 2's ad-hoc PHP commands come with it — cpx exec -r '…' and cpx tinker boot your Laravel app (config, facades, .env, $app), and cpx tinker hands off to the project's own php artisan tinker when it has one.

    The PHAR lives at ~/.grove/cpx.phar rather than under the root-owned $GROVE_HOME, so cpx self-update can replace it in place. cpx needs PHP 8.3+; a directory pinned to something older gets an explanation instead of a parse error out of the PHAR.

  • grove php ext — an extension audit. Grove's bundled PHP comes from prebuilt static archives with a fixed extension set, and until now nothing told you what was in the one you got. grove php ext diffs a build's real php -m against the extensions the ecosystem expects, and says what each gap costs:

    $ grove php ext
    php@8.5 (common) — 49 modules, 1 required missing, 5 recommended missing
    
      Missing (required):
        ✗ mysqli         WordPress — its only MySQL driver
    
      Missing (recommended):
        ✗ intl           Laravel Number/dates, Filament, Nova
        ✗ sodium         modern crypto (sodium_*), passkeys
        ✗ readline       history/editing in tinker and cpx tinker
        ✗ apcu           in-process cache (Laravel apc store)
        ✗ xsl            XSLT transforms (ext-xsl)
    

    The same gap now shows up in three other places you'd want it: grove php install prints required-tier misses right after installing, grove php list carries a one-line summary per build, grove doctor has a php-extensions check, and the GUI's PHP panel has an Extensions section.

  • Grove builds its own PHP. The prebuilt static-PHP archives Grove used to download are not supersets of each other, and each is missing something that matters: upstream common has pdo_sqlite/pdo_pgsql but no intl, mysqli, sodium, readline, apcu or xsl; upstream bulk has those six but drops both PDO drivers. Laravel's default database is SQLite, Grove bundles PostgreSQL, WordPress speaks only mysqli, and a large slice of Packagist requires ext-intl — so no choice between the two was the right one.

    .github/workflows/php-build.yml now builds the union with static-php-cli, for macOS and Linux on both architectures, and publishes it to a rolling php-runtimes release. grove php install fetches that by default (--variant grove, [general].php_variant = "grove").

    The extension list is authored once, in grove_runtime::extensions::BUILD_SET, and grove php craft prints the static-php-cli config generated from it — so the binary that audits a build is the same one that specified it, and CI can't drift from the audit.

    Until a Grove build exists for a given version, grove php install falls back to upstream common, names the extensions that costs you, and labels the build with the set it actually got rather than the one requested. Asking for --variant common or --variant bulk explicitly never falls back: silently trading one extension hole for the other would be worse than an error.

    GROVE_PHP_MIRROR still points the downloader at a different host for all three variants, for a team mirror or an air-gapped cache.

Changed

  • The default PHP version is now 8.5 (was 8.4) — [general].default_php, grove init --php, and the versions the docs use in examples.
  • grove php install now fetches the matching CLI binary along with php-fpm, and records which variant a build came from. grove php ext, grove php list and the PATH shims all need the CLI, and auditing a build whose php -m came from a different archive than the one serving requests would have been worse than not auditing at all. Switching a version's variant replaces both binaries, so they can never disagree.
  • Redis is fetched from download.redis.io, not GitHub's git-archive of the tag. GitHub generates those tarballs on demand and does not promise their bytes stay stable, so the old URL was unverifiable by construction.
  • e-db is pinned to a revision in [workspace.dependencies] rather than tracking whatever kwhorne/e last pushed. An unpinned git dependency let a commit there land in Grove's lockfile with no review — which is how keyring, and with it a libdbus-1-dev build requirement on Linux, nearly arrived.

Security

  • The daemon's IPC socket authenticates its callers. It was 0777 with no check at all, and its requests are not advisory: PhpInstall and ServiceInstall make root download and execute a binary, DbDumpFile makes root write a file to a caller-chosen path. Any local process could issue them — a compromised npm/composer postinstall hook, or one of the PHP apps Grove serves. The socket is now 0660, owned by the user Grove serves, and every connection is checked with SO_PEERCRED/LOCAL_PEERCRED before the request is read.
  • Root no longer executes anything out of $GROVE_HOME. That directory is the user's own home, so its contents — and the php-builds.json that names the php-fpm binary — are user-writable. Rewriting one JSON file made root exec an arbitrary path at the next request. The PHP-FPM master, Redis, the Redis build, the runtime version probes, and grove new's Composer and Laravel installer runs all drop to the invoking user now. PostgreSQL and MySQL already did, because they refuse to run as root at all.
  • Root no longer writes secrets through symlinks, or world-readable. A symlink at certs/grove-ca.key had root write and chmod the target. The opposite failure was the Vite dev-server keys, which got no chmod at all and so were simply left world-readable. Every sensitive write now sets its mode in open(2) and refuses a symlinked destination. The MySQL migration dump also moves out of world-writable /tmp.
  • The root CA is constrained to the TLD Grove serves, so it can no longer sign google.com or a bank — it is in the system trust store, and it had no name constraints. Its private key is also root-owned now: nothing unprivileged needs it, and at user ownership a compromised postinstall hook could have walked off with a machine-trusted signing key.
  • Certificates are only issued for names Grove actually serves. The SNI resolver minted a leaf for whatever hostname was asked for, with no check against the site registry — and the HTTPS listener binds 0.0.0.0, so anyone on the network who could reach it could obtain a valid, machine-trusted certificate for any name. The certificate cache is also bounded now, and only a site's own certificate is persisted.
  • Downloads are verified against the publisher's own SHA-256 before they are written or executed: Grove's PHP builds and cpx via the GitHub release digest, Node via SHASUMS256.txt, Composer via its .sha256, PostgreSQL via the asset's .sha256, Redis via redis/redis-hashes. Upstream common/bulk publish no checksum at all and MySQL publishes none usable; both say so per download rather than implying otherwise.
  • The request log redacts credentials. Grove is the proxy, so it sees every Authorization header, session cookie and login password — and handed them to the GUI, the curl/.http/Pest snippets, and the MCP tools, which exist to send a request to an AI assistant. grove replay uses a separate in-process path and still sends the real credentials.
  • The tunnel server closes its open defaults. A token is required; the apex domain is no longer claimable as a subdomain; tokens and Basic auth compare in constant time; both HTTP servers have a header-read deadline; and X-Forwarded-For is overwritten with the real peer instead of passed through.
  • Secret sync no longer lets the backend choose who can read a project. The client records the recipient list locally and refuses to encrypt to one the server changed on its own. Payloads carry a version inside the encryption, so replaying an old one — reinstating a rotated secret, or a revoked member's access — is caught.
  • The license key is 0600 rather than 0644. It doubles as the bearer token for the Teams backend, so every local user could read it.
  • The LaunchDaemon plist escapes the values it interpolates. $GROVE_HOME is user-controlled under sudo -E, and a value containing </string> could have injected keys into a root service.

Fixed

  • Leaf certificates now actually renew. issue_leaf gives them 397 days and its comment promised the daemon renewed them, but the cached pair was returned unconditionally — so 397 days after a site was first served over HTTPS, its certificate expired and every request to it failed TLS. They are replaced within 30 days of expiry, judged from the certificate's own notAfter.
  • An incomplete root CA is no longer silently replaced. load_or_create regenerated whenever either file was missing, minting a new signing identity while the trust store still held the old certificate — every site failing TLS with nothing pointing at the cause. It now refuses and says how to start over.
  • grove ca trust no longer reads the CA private key, which it never needed; an unprivileged run gets the clear "needs elevation" instead of a permissions error.
  • The license verifier's hex decoder no longer panics on non-ASCII input, and the clock helper no longer treats an unreadable clock as "not expired".

1.4.2 — 2026-08-08

Upgrade immediately if you are on 1.4.0 or 1.4.1. Those releases could not serve a single request: every connection was reset, on every site.

Fixed

  • Every request was reset (ERR_CONNECTION_RESET) on 1.4.0 and 1.4.1. The header-read timeout added in 1.4.0 was configured without giving hyper a timer. hyper does not fall back to a default and does not complain at setup: it panics the first time a connection reaches the timeout code, which is every connection — "timeout header_read_timeout set, but no timer set". The HTTP and HTTPS listeners now install TokioTimer.

    Two changes in 1.4.0 combined to hide it. Panics no longer abort the process, which is right — but it meant the daemon stayed up and kept reporting itself healthy, with grove status and the GUI both showing Running, while not a single site rendered. Under the previous panic = "abort" the first request would have killed the daemon outright and the fault would have been obvious.

  • A request now goes through a connection in the test suite. Every layer had tests of its own — DNS answers, TLS chaining, static revalidation, FastCGI framing — and none of them put a request through a connection built the way the listeners build it, which is why a misconfigured builder shipped. That test now exists, and it fails with the exact panic above when the timer is removed.

1.4.1 — 2026-08-08

Saying the right minimum. 1.4.0's dependency updates quietly moved the lowest Rust version Grove can be built with from 1.80 to 1.94 — sqlx 0.9 requires it — but the manifest, the README badge and CONTRIBUTING all still promised 1.80. Anyone who followed the documentation got a wall of errors from a transitive dependency instead of a sentence naming the cause. Nothing about the shipped binaries changes; this is about being able to build them.

Fixed

  • The declared minimum Rust version is now true. rust-version said 1.80 while the real floor had moved to 1.94, so cargo build on the documented version failed inside time-core with "the package requires the Cargo feature called edition2024" — which names neither Grove nor the dependency that raised the bar. With the correct value, cargo says "rustc 1.80 is not supported by the following package: grove-core requires rustc 1.94" before it compiles anything. The README badge and CONTRIBUTING's build instructions said 1.80 too, and now say 1.94.
  • CI checks the minimum instead of assuming it. Every job used stable, which is always new enough, so a dependency bump could raise the real floor without anything failing — which is exactly how 1.4.0 shipped a manifest that was wrong. A new MSRV job builds the workspace with the declared version, so the claim and the code cannot drift apart again.

Dependencies

  • The npm group, without the TypeScript 7 jump. svelte 5.56.8, vite 8.2.0, @sveltejs/vite-plugin-svelte 7.2.0, svelte-check 4.7.4, @fontsource/jetbrains-mono 5.3.0 and @tauri-apps/plugin-dialog 2.7.2. TypeScript stays on 6: version 7 is the Go port, which needs both 7 and 6 installed plus a --tsgo flag, and svelte-check does not drive it yet. Dependabot is now told to skip TypeScript majors, so one package that cannot move no longer fails the whole grouped update every week.
  • base64 0.23.1, thiserror 2.0.20, toml 1.1.3, and pnpm/action-setup 4 → 6.0.10 in the workflows.

1.4.0 — 2026-08-08

The same work, once. 1.3.x stopped Grove holding whole bodies in memory; this release stops it redoing things. A proxied request built a new HTTP client — and a client is the connection pool, so every request to a Vite dev server paid a fresh TCP handshake. A static asset was re-read and re-sent on every reload, because responses carried nothing a browser could revalidate against. DNS answers were marked uncacheable, so the system resolver asked again for every single connection. And the local CA minted a brand-new certificate each time it was loaded from disk.

The other half is about staying up. The release profile used panic = "abort", which turned any single failed request into a dead daemon — no DNS, no TLS, no sites. The accept loop answered failure with a bare continue, which under EMFILE is a busy loop that never recovers. Nothing bounded a TLS handshake or the wait for request headers, so a peer that connected and went quiet kept a task and a file descriptor for as long as it liked.

Also: the dependency tree is current again, including four crates whose major bumps needed real migration rather than a version bump.

Fixed

  • Reloading the local CA no longer mints a new certificate. Loading it from disk parsed the PEM into params and called self_signed, creating a fresh certificate — new serial, new validity window — on every daemon start and every CLI call. Leaf certificates still chained, since the name and key matched, but cert_pem() reported a certificate that was neither the file on disk nor the one the OS trust store had been told to trust.
  • A panic no longer takes the whole daemon down. The release profile built with panic = "abort", so one unwrap on a poisoned mutex anywhere — in a single request, for a single site — aborted the process and with it DNS, TLS and every other site. Panics now unwind, which keeps the failure inside the tokio task that caused it.
  • The accept loop no longer spins a core when it runs out of file descriptors. accept failing was answered with a bare continue; under EMFILE that fails again immediately and forever, so the listener burned 100% of a core and never recovered. Failures now back off exponentially (5 ms to 1 s), which also gives descriptors time to be released.
  • Half-open connections are no longer held forever. Neither the TLS handshake nor the wait for request headers had a deadline, so a peer that connected and went quiet — a crashed browser, a port scanner — kept a task and a descriptor indefinitely. Handshakes now time out after 10 s and headers after 30 s.
  • Blocking filesystem calls off the async path. Path::is_file/is_dir/ exists were called on every request to a PHP or static site straight from the request task; on a slow volume (a network share, a Docker bind mount) that stalls a runtime worker and every other request scheduled on it. They are now async stats.
  • Starting a PHP-FPM pool no longer stalls unrelated requests. Pool lookup forks php-fpm and then polls for its socket for up to a second, synchronously. It now runs on the blocking pool.

Changed

  • Proxied requests reuse connections. A new hyper client was constructed per request, and a client is the connection pool — so every proxied request paid a fresh TCP handshake and a Vite dev server saw one connection per asset instead of a few kept alive. One shared pooled client now serves the proxy driver and replay.
  • Static files revalidate instead of re-transferring. Responses carry an ETag (from size + mtime, so no extra read) and answer If-None-Match with 304, so reloading a dev site with unchanged assets no longer re-reads and re-sends every byte. Cache-Control: no-cache keeps an edit from ever serving stale.
  • Static files over 256 KiB stream from disk. A large asset — a video, a sourcemap — was read into memory in full, per concurrent request, before the first byte reached the browser.
  • DNS answers are cacheable. Records were served with TTL 0, which forbids caching, so the system resolver re-queried Grove for every connection — with macOS's mDNSResponder in that path on every first byte. The answer is always loopback and never changes, so it is now TTL 300.
  • The release build is optimised for speed rather than size. opt-level = "z" optimised the one thing that does not matter for a daemon on the request path. Now opt-level = 3 with fat LTO, which costs about 3 MB of binary.

Dependencies

  • The cargo group's 25 updates, including the breaking ones. Four crates needed code changes rather than a version bump: rcgen 0.13→0.14 (Issuer replaces passing a certificate and key to signed_by), sqlx 0.8→0.9 (non-literal queries now require an explicit AssertSqlSafe, with the audit written down where the conversion happens), hickory-dns 0.24→0.26 (Server, zone_handler, Metadata/HeaderCounts, request_info()), and age 0.10→0.12 (borrowed recipients, and is_scrypt() in place of the Decryptor enum). Also thiserror 1→2, ed25519-dalek 2→3, rand 0.8→0.10, toml 0.8→1.1, directories 5→6, base64 0.22→0.23.
    • Requests to the DNS resolver carrying anything other than exactly one question are now refused rather than guessed at.
    • The age upgrade is pinned by a fixture encrypted with the previous version, so a format regression cannot silently orphan secrets already on disk.

Upgrading

Nothing to do. Existing certificates, encrypted secrets and config.toml are read as before; the CA on disk is now used as-is rather than re-minted, and age files written by earlier versions are covered by a regression test.

1.3.2 — 2026-07-31

The other direction. 1.3.1 stopped buffering responses; this stops buffering uploads. A 400 MB upload used to cost Grove 1.2 GB of RSS — three copies of the body: one to collect it, one cloned for the request that gets forwarded, one for the timeline. The same upload now costs about 3 MB.

Fixed

  • Request bodies stream instead of being buffered whole. The body was collected at the top of dispatch, before Grove had even resolved which site the request belonged to, so every upload was held in memory in triplicate.
    • The proxy driver (Vite, Node) now forwards the body as it arrives, whatever its transfer encoding. CGI's CONTENT_LENGTH requirement does not apply to an HTTP upstream, so there was nothing to decide here.
    • The FastCGI path branches on the body's exact size. When the length is known — essentially every browser form post and API upload — the body is streamed into STDIN records with CONTENT_LENGTH set from it.
    • Chunked requests declare no length, but CGI must be told one before the body is sent. Grove keeps such a body in memory up to 1 MiB and spills to a private spool file beyond that, measuring it as it goes. Refusing them with 411 would have made Grove the reason a valid request fails; nginx and Apache spool, so Grove spools too. Spool files are 0600 inside a 0700 directory and are removed when the body is dropped — tied to Drop, so neither an error nor a client disconnect can leave upload contents on disk.
    • Bodies small enough for the timeline to store in full are still collected, so grove replay and the curl / .http / Pest export are byte-for-byte unchanged for what is almost every request. Only uploads larger than the timeline's own 1 MiB cap take the streaming path, where a truncated capture is already what the timeline would have kept.
  • A request body is now bounded. Because CGI forces Grove to measure an undeclared body before it can forward anything, an unbounded chunked upload was a disk-filling vector. Bodies beyond 2 GiB are refused with 413.
  • FastCGI reads and writes concurrently. Writing the whole body before reading any response would deadlock on a large upload that PHP rejects early: Grove blocking on STDIN while PHP blocks on its response, each waiting for the other's socket buffer to drain. Harmless when bodies were capped by memory; a real hazard now that they are not.
  • The curl / .http / Pest export says when a body was truncated. The timeline keeps at most 1 MiB of a request body, and the generator ignored the flag that recorded it — so an export of a large upload looked complete and quietly sent a partial body, which then surfaced as a puzzling response from the app rather than as Grove's own limit. Every format now warns first, and a truncated JSON body is reported as truncated instead of as invalid JSON, which had blamed the application for Grove's capture limit.
  • A truncated capture is recorded as truncated. The timeline inferred truncation from the stored body being longer than the cap, which stopped being true once bodies were captured as they streamed: the capture ends at exactly the cap, indistinguishable from a body that happened to fit. Record now carries the fact explicitly.

1.3.1 — 2026-07-31

Streaming, and two disclosures found while testing it. Grove's proxy buffered every response whole, so a Server-Sent Events endpoint delivered nothing until PHP closed the request. Fixing that meant looking closely at the request path, which turned up a .php file being served as source and a .env being served at all. If you use grove share, both were reachable from outside.

Fixed

  • Responses stream instead of being buffered whole. The FastCGI client accumulated every STDOUT record and returned only on END_REQUEST, and Full<Bytes> could not express a stream in any case. Grove now returns the headers as soon as PHP flushes them and forwards each FastCGI record as an HTTP chunk, so SSE arrives live and a large download is no longer held in memory — a 2 GB download used to mean 2 GB of RSS. The proxy driver passes the upstream body through for the same reason, so Vite HMR and Node SSE endpoints behave too. This is deliberately not conditional on text/event-stream: the problem was general.
  • .php files are executed, not served as source. With a PHP driver, any existing .php file in the document root was handed back as text — so /index.php disclosed the front controller on every PHP site. It also meant WordPress could not work at all, since wp-login.php and wp-admin/*.php must execute. Such requests now go to PHP-FPM with SCRIPT_FILENAME pointing at the file, matching what nginx and Apache do. Matched case-insensitively, because a case-insensitive filesystem resolves /INDEX.PHP to the same file.
  • Dot-prefixed paths are refused. A plain PHP project's document root is the project root, so /.env returned APP_KEY in full. Any path with a dot-prefixed component is now a 404, never served and never executed. .well-known/ is exempt, so ACME HTTP-01 challenges keep working.

Notes

  • duration_ms in the request timeline now measures time to headers rather than total time, for streaming responses. A 16-second SSE stream used to log 16 s and now logs a few milliseconds. That is closer to time-to-first-byte than to duration; the Requests panel labels it as duration and will be revisited.
  • Request bodies are still buffered whole, so a large upload still costs memory. Fixing that needs a decision about CONTENT_LENGTH for chunked requests and about what the request timeline promises when a body is too large to capture, so it is deliberately not in a patch release.

1.3.0 — 2026-07-30

The application decides. Laravel 13.16 moved dev-process configuration out of composer.json and into the application itself, via DevCommands. Grove used to guess — a Vite server and a queue worker, hardcoded — which meant it was blind to Reverb, Horizon or stripe listen, and assumed npm even in a bun project. Now Grove asks: it reads artisan dev:list and supervises exactly what the app declares, minus the processes Grove already is. The app owns the list; Grove owns the supervision — no open terminal, per-process logs, autostart at boot.

Added

  • grove dev now runs the processes your app declares. On Laravel 13.16+, Grove reads php artisan dev:list --json --except-vendor and supervises that list instead of guessing, so userland processes registered through DevCommands — Reverb, Horizon, stripe listen — are started alongside Vite and the queue worker, each with its own dev-<site>-<name>.log. server and logs are skipped: Grove already serves the site over FPM, and grove logs already tails the app log. Vendor-registered processes are excluded so a Composer package can't start processes inside the daemon. Non-Laravel sites and older Laravel versions keep the previous behaviour (Vite + queue worker). Because Grove reuses Laravel's NodePackageManager detection by way of the declared command, pnpm, yarn and bun projects now work without special casing. Run grove dev instead of php artisan dev, not alongside it.
  • grove path install puts the grove CLI itself on your PATH, next to the php / composer / node / npm / npx / laravel shims. Previously a user who installed the macOS app and ran grove path install still had no grove command, because the binary only existed inside the .app bundle. grove path show --json reports this as cli_installed.
  • grove dev start warns about a competing php artisan dev. Both supervise the same processes, so running both silently doubles them. The check cannot tell which project the other process belongs to, so it is reported as a warning alongside the started processes rather than as an error.

Changed

  • grove dev start / grove dev stop take an optional site argument, defaulting to the site in the current directory (resolved from grove.toml's name, else the directory name) — matching grove link, grove secure and grove up.

Fixed

  • grove dev stop no longer orphans processes. Dev processes were killed directly, but npm run dev spawns Vite as a grandchild, which survived and kept holding port 5173 — breaking the next grove dev start. Each dev process now runs in its own process group and is stopped group-wide (SIGTERM, then SIGKILL).

Notes

  • Intel macOS is no longer shipped. Notarization on GitHub's macos-13 runner routinely hangs, so the release workflow builds Apple Silicon and Linux only. Intel Macs can still build from source with cargo build --release. This took effect after 1.2.1; 1.3.0 is the first release to state it.
  • grove dev replaces php artisan dev rather than complementing it. Running both doubles every process; grove dev start now warns when it sees a competing php artisan dev.
  • On Laravel 13.16+, grove dev start boots the application once to read dev:list, which adds a moment of startup latency. If the app cannot boot, Grove falls back to the previous heuristic rather than failing.

1.2.1 — 2026-07-18

The causal chain, closed loop. 1.1 gave sandboxed writes automatic rollback and gave requests a causal chain — but the two didn't meet: a rollback that can't tell you what a change touched is just a nicer undo button. 1.2.1 links them. Every sandboxed write now reports its own blast radius, so an agent (or you) can see exactly what a migration ran before deciding to keep it.

Added

  • Attributed blast radius for sandboxed writes. grove_migrate_sandboxed and grove_sql_sandboxed now return a chain alongside the schema diff — the SQL the operation actually ran and any mail it sent, correlated to the operation's time window. Grove enables SQL capture for the duration automatically (MySQL) and restores your previous setting afterwards, so an agent (or you) can inspect exactly what a migration touched before deciding to keep it. Backed by a new ChainForWindow IPC command that generalizes the request causal chain to any time window.

Fixed

  • Auto-updater 403s. The release now rewrites latest.json to use the public browser_download_url for each artifact instead of the rate-limited api.github.com asset endpoint, so in-place updates no longer fail intermittently with 403 Forbidden.

Internal

  • grove-services now declares the time crate's parsing feature explicitly, so the crate builds and tests standalone (previously masked by workspace feature unification).

1.1.0 — 2026-07-18

The AI release. Grove 1.0 opened its local environment to AI clients over MCP, read-only. 1.1 turns grove mcp into a full AI debugging companion: safe, sandboxed writes with automatic rollback, a per-request causal chain that ties each request to the SQL it ran and the mail it sent, and one-click "explain this error" bundles that gather the request, its side effects, and the stacktrace for your assistant. The core stays free and open source.

Added — agent-safe MCP write tools (opt-in)

  • grove mcp --allow-write. The MCP server is read-only by default; write tools appear only with this flag (and are refused otherwise). Every write is recorded to an audit log at $GROVE_HOME/logs/mcp-writes.log.
  • grove_migrate_sandboxed. Runs php artisan <command> (default migrate --force) inside an automatic snapshot sandbox: Grove snapshots the database first, runs the migration, reports the schema diff, and automatically rolls back on failure. Pass roll_back: true for a pure dry run; on success the snapshot id is returned for manual rollback.
  • grove_sql_sandboxed. Runs a single write statement (INSERT/UPDATE/DELETE/DDL) through the same snapshot → run → schema-diff → auto-rollback flow, returning rows_affected. Read-only statements are refused — use grove_db_query for those.
  • Both write tools cover bundled MySQL/PostgreSQL (daemon snapshot) and SQLite (snapshotted by copying the .sqlite file).

Added — request causal chain

  • grove_request_chain MCP tool (and RequestChain IPC command) correlate a captured request with the side effects Grove observed inside its time window, plus derived metrics (duration, query count, side-effect counts). Grove sits in front of every request and captures mail centrally, so it does this with zero app instrumentation.
  • grove sql-capture on|off|status. Turns on MySQL's general query log (written to a Grove-owned file) so each request's chain includes the SQL it issued, correlated by time window — because Grove owns the database service.
  • Desktop app. The Requests panel expands each request to show its causal chain (SQL + mail + metrics), with a toolbar toggle for SQL capture.

Added — "explain this error"

  • grove explain <id> and the grove_explain MCP tool curate a debugging bundle for one request — the request (headers + body), its causal chain (SQL + mail + metrics), and the matching error-log entries with stacktraces — gathered in one place and structured for an AI assistant. Logs are chased only for failures, and the absence of a stacktrace is handled gracefully. In the desktop app, an ✨ Explain button on each request copies the bundle to the clipboard. See docs/MCP.md.

1.0.0 — 2026-07-11

Grove 1.0. A native, zero-dependency local dev environment for macOS: *.test sites with trusted HTTPS, bundled multi-version PHP/Node, databases, mail, tunnels, a request timeline with replay, a webhook hub, a database client, reproducible environment bundles, and end-to-end encrypted team secret sync — with the entire core free and open source.

Added

  • AI tools (MCP server). grove mcp runs a Model Context Protocol server that exposes your local environment — sites, request timeline, webhooks, logs, and database schema/queries — to AI clients like Claude and Cursor. Read-only and local-only; point your client at grove mcp and ask it about what's actually running. See docs/MCP.md.

0.13.1 — 2026-07-11

Changed

  • The in-app logo (header, About dialog, and splash screen) now matches the new lime app icon.

0.13.0 — 2026-07-11

Added

  • Local webhook hub. Any request to /__grove/hooks/<bucket> on a site is captured and acknowledged with 200 — a local webhook.site. Expose it with grove share <site>, point Stripe/GitHub at it, then inspect each delivery (headers + payload) and re-deliver it to your app while you fix the handler. New Webhooks panel in the app; grove hooks on the CLI.
  • Turn a request into a test. From any captured request or webhook, copy it as a curl command, a .http file, or a Pest feature test (grove request <id> --as pest, or the buttons in the app) — turn a failing request into a regression test in one click.

Changed

  • Fresh lime app icon.

0.12.0 — 2026-07-11

Added

  • Request inspection & replay. The request timeline now lets you expand any request to see its headers and body, and replay it with one click (or grove replay <id>) — a framework-agnostic way to re-run a failed request while you fix the code. Works for any site, any framework, zero setup.
  • Reproducible environment bundles. grove bundle export packages a project's grove.toml, .env, and database into one shareable file; grove bundle import unpacks it, brings the environment up, and loads the database — reproducible dev environments without Docker. Great for onboarding.
  • SQL syntax highlighting in the database client's query editor.

0.11.1 — 2026-07-11

Changed

  • Internal packaging change for how Grove Pro features are built. No user-facing changes — the free core and Pro features behave exactly as in 0.11.0.

0.11.0 — 2026-07-08

Added

  • Built-in database client. A new Database panel browses and queries your sites' databases — auto-connected from each project's .env, with no connection details to enter. Browsing tables and running SELECT queries is free; Grove Pro adds inline row editing, a schema inspector (columns, indexes, foreign keys), and a production-safety guard. See the Pro & Teams guide.

0.10.0 — 2026-07-08

Added

  • Team secret sync (Grove Teams). Share a project's .env with your team securely — encrypted end-to-end, so it never gets pasted into a chat window. Manage access with grove secret. Requires a Grove Teams license; see the Pro & Teams guide.

0.9.0 — 2026-07-07

Added

  • License activation for Grove Pro / Teams. Activate a purchased license key and Grove verifies it offline (Ed25519, grove-license) against a baked-in public key — no network call needed, so entitlements keep working without a connection.
    • grove license activate <key> / grove license status / grove license deactivate.
    • A License section in the desktop app's Settings (activate, status, remove).
    • Entitlement gates (require_pro / require_teams) that Pro/Teams features check; the free, open-source core is never gated.

0.8.1 — 2026-07-07

Fixed

  • grove path install no longer fails with a permission error. The shims now live under ~/.grove/bin (a user-owned directory) instead of under the root-owned $GROVE_HOME, so the command works when Grove runs as a root LaunchDaemon. Add ~/.grove/bin to your PATH (the command prints the line).

0.8.0 — 2026-07-07

Added

  • grove.toml + grove up — reproducible project environments. Commit a small grove.toml describing what a project needs (PHP/Node versions, bundled services, HTTPS, dev processes); a teammate then goes from git clone to a running, identical setup with a single command:
    • grove up links the project, pins its PHP/Node, ensures its services are installed + running, and (optionally) starts its dev processes.
    • grove up --write scaffolds a friendly, commented starter grove.toml.
    • grove up [path] targets a directory other than the cwd; --no-dev skips dev processes. It orchestrates the same daemon operations you'd run by hand, in one step.

0.7.0 — 2026-07-07

Added

  • Request timeline. Grove sits in front of every *.test site, so it now records a live, framework-agnostic timeline of the requests it proxies — method, path, status, and duration — with zero configuration and no per-app instrumentation.
    • New Requests panel in the desktop app: a live-updating table with status colour-coding, slow-request highlighting, a per-site filter, and shown/avg-ms/error-rate stats.
    • grove requests [site] [--limit N] on the CLI (--json supported). Captured at the proxy layer into a bounded in-memory ring buffer (last 500), so it costs nothing at rest and never grows unbounded.

0.6.0 — 2026-07-07

Added

  • grove path — the bundled toolchain on your PATH. Installs shims for php, composer, node, npm, npx and laravel that resolve to whatever version each project pins (via grove isolate / grove node use), falling back to the defaults — zero-config per-directory version switching, so you can finally drop Herd/Valet entirely. Runtimes are provisioned by the (root) daemon so the shims only ever read them.
    • grove path install / grove path show / grove path uninstall.
  • grove db — database time-travel. Point-in-time snapshots of Grove's bundled MySQL / PostgreSQL so you can experiment (or migrate) without fear:
    • grove db snapshot [--engine mysql|postgres] [--db NAME] [--note TEXT]
    • grove db list, grove db restore <id>, grove db rm <id>. Snapshots are plain SQL dumps under $GROVE_HOME/snapshots/ with a JSON index.

0.5.2 — 2026-07-06

Fixed

  • Dev processes are no longer orphaned when the daemon restarts. On graceful shutdown (including launchctl kickstart) Grove now kills the per-site Vite / queue children, so restarting the daemon doesn't leave stray vite servers squatting ports (which caused public/hot to point at a stale server).

0.5.1 — 2026-07-06

Added

  • Vite over HTTPS, automatically. When grove dev starts the Vite server for an HTTPS site, Grove issues a CA-trusted leaf certificate for the host and passes it via the standard VITE_DEV_SERVER_CERT / VITE_DEV_SERVER_KEY env vars that laravel-vite-plugin reads. Vite then serves HTTPS with a trusted cert — no mixed-content, HMR just works — with no Herd/Valet directories involved. (Use the standard laravel-vite-plugin in vite.config.js; a custom hard-coded Herd cert path won't pick this up.)

0.5.0 — 2026-07-06

Added

  • Per-site dev processes — Grove runs and supervises a site's long-running dev tasks so you don't have to: the Vite dev server (npm run dev, HMR) and, for a non-sync queue, a queue worker — each with the site's own Node/PHP, run as your user, output streamed to the Logs panel. Because Grove already serves the app, there's no artisan serve to run. Toggle it with the ⚡ button per site in the GUI, or grove dev start|stop|list <site> — a Grove-aware replacement for composer run dev.

0.4.2 — 2026-07-06

Fixed

  • Proxy sites now hit the right virtual host. The reverse proxy set Host to the upstream authority (and forwards the public host as X-Forwarded-Host
    • X-Forwarded-Proto), so name-based vhosts — e.g. an nginx container with server_name inside2.local, or an OrbStack domain — match instead of falling through to a default server block. Previously a Docker site could show the bare nginx welcome page instead of the app.

0.4.1 — 2026-07-06

Added

  • Compose auto-detection. Running docker compose projects are now served as <project>.test even without labels — Grove picks the web container (by service name / published web port) and proxies to it. Explicit dev.orbstack.domains / grove.host labels still take precedence.
  • Start / stop / restart containers from the GUI. Docker sites in the Sites table gain ▶ / ⏹ / ↻ controls; stopped containers show as stopped with a Start button, and a stopped site serves a friendly “start it” page.

0.4.0 — 2026-07-06

Added

  • Docker / OrbStack integration. Grove now auto-discovers running containers and serves them as <name>.test with its trusted local HTTPS — right next to native sites, in the same dashboard. A container is picked up when it carries a dev.orbstack.domains label (Grove reuses OrbStack's own routing) or an explicit grove.host label; Grove terminates TLS and reverse-proxies to it. Containers appear/disappear live (polled), show a 🐳 badge in the Sites table, and — because they're first-class sites now — grove share can tunnel them publicly too. Toggle with [general].docker in config.toml.

0.3.1 — 2026-07-03

Added

  • Community starter kits when creating a site: pick Custom in the New Site dialog (or grove new <name> --kind vendor/package) to scaffold any community kit — e.g. a Svelte kit — via laravel new --using=<repo>.

0.3.0 — 2026-07-03

Changed

  • New sites now scaffold with the official laravel new installer (latest Laravel) and a starter-kit picker — None, Livewire, React (Inertia) or Vue (Inertia) — replacing composer create-project. The GUI's “Create a new site” dialog gained a Starter kit selector; on the CLI use grove new <name> --kind livewire|react|vue. Grove installs the Laravel installer and a Node runtime on demand (for the asset build) against its bundled PHP/Composer/Node, and hands the finished project to your user.

0.2.9 — 2026-07-01

Added

  • Xdebug panel in the GUI (Tools → Xdebug step-debugging): a live on/off toggle, the DBGp port, and per-PHP-build availability with a one-click Install debug build for versions that lack Xdebug.

  • Xdebug step-debugging (grove debug on|off|status|env, and the GUI toggle). Grove loads Xdebug into its FPM pools on demand via per-pool -d INI overrides — the global php.ini is never touched, and pools respawn instantly when toggled. Xdebug runs in start_with_request=trigger mode, so it stays dormant (near-zero overhead) until a request opts in with the XDEBUG_TRIGGER cookie/param; grove debug env prints the matching env for debugging CLI processes (eval "$(grove debug env)"). Grove speaks the runtime half: your editor's DAP client listens on DBGp port 9003 and Xdebug connects out to it.

    Step-debugging requires a PHP that has Xdebug — a grove php register-ed dynamic PHP with Xdebug built in, or a loadable xdebug.so in its extension_dir. Grove's own fully-static builds can't load Xdebug (static PHP can't dlopen, and static-php-cli can't compile it in), so those report as unavailable in grove debug status / the GUI panel.

0.2.8 — 2026-07-01

Added

  • Convert database in the Tools panel: copy a whole database between MySQL, PostgreSQL and SQLite — tables, columns (mapped by category), primary keys and all rows. Ideal for turning a MySQL database into a portable SQLite file and back. Values transfer as text (blobs as bytes), so dates, decimals, JSON and UUIDs survive across dialects. Views, stored routines, triggers and foreign keys are not copied.

0.2.7 — 2026-07-01

Added

  • “Restart daemon” in the Tools panel — restarts Grove's background service with one click (no password), so the running daemon picks up a freshly updated app. The root LaunchDaemon re-execs itself via launchctl kickstart.

0.2.6 — 2026-07-01

Added

  • Tools panel in the GUI, starting with “Migrate MySQL from Herd”: copy all databases from another MySQL server (e.g. Laravel Herd) into Grove's MySQL via a safe logical dump & restore using Grove's own client tools. The source databases are left untouched.

0.2.5 — 2026-07-01

Fixed

  • MySQL and PostgreSQL now start when Grove runs as a root service. Both refuse to run as root (mysqld/postgres), which broke “Start” under the macOS LaunchDaemon (the service flickered green → idle). Grove now runs bundled databases as the invoking user — like PHP-FPM — dropping privileges before exec, owning their data directories to match (chown), and placing their unix sockets inside the user-owned data dir. Existing installs are repaired automatically on the next start.

0.2.4 — 2026-06-30

Fixed

  • Tunnelled sites now render assets correctly (Vite, CSS, JS). The tunnel no longer rewrites the Host header to the local site name — it preserves the public host so the app builds correct public asset URLs, and routes locally via a new X-Grove-Site header instead. It also sets X-Forwarded-Proto, and Grove's proxy maps it to FastCGI HTTPS=on, so apps generate https:// URLs (no mixed-content blocking) without needing TrustProxies configured.

    Update both the macOS app and the grove-tunnel server on your host to 0.2.4 — the server is what preserves the public host.

0.2.3 — 2026-06-30

Added

  • The public tunnel URL now shows inline in the Sites row (a 🌍 chip you can click to copy) while a site is shared — not just in the transient toast. The Tunnels panel continues to list every active tunnel.
  • A turnkey deploy/tunnel/setup.sh for standing up your own tunnel server in one command.

0.2.2 — 2026-06-30

Added

  • Zero-config tunnels. Grove now defaults to the public tunnel server grove.elyracode.com, so grove share <site> works out of the box and gives a https://<random>.grove.elyracode.com URL — no [tunnel] config needed.
  • Open-server mode. grove-tunnel can run without a token (omit --token) for a public community server; clients no longer need a token.
  • On-demand HTTPS authorization. grove-tunnel exposes /__grove_ask so a fronting Caddy can mint per-subdomain Let's Encrypt certificates safely (only for hostnames under the server's own domain) — no DNS API required.
  • Deployment kit in deploy/tunnel/: Caddyfile, systemd unit and a step-by-step guide for running your own server.

0.2.1 — 2026-06-30

Added

  • Tunnel management in the GUI — a new Tunnels panel and a per-row Share button in the Sites table. The daemon now owns tunnel lifecycles, so the GUI/CLI can start, stop and list public tunnels.
  • Request inspector — a live table of recent tunnelled requests (time, site, method, path, status, duration), ideal for debugging webhooks. grove share also prints requests live in the terminal.
  • Remove a site from the list — grove forget <name> (and a trash button in the GUI) hides a site without deleting its files; grove restore <name> brings it back. Backed by a new ignored list in config.toml.

Removed

  • docs/SIGNING.md (internal signing notes) is no longer part of the docs.

0.2.0 — 2026-06-30

Added

  • Public tunnels (grove share) — a native, self-hostable alternative to Expose/ngrok, built in with zero external dependencies:
    • grove share <site> exposes a local *.test site at a public URL for demos, real-device testing and webhooks.
    • New grove-tunnel server binary you deploy on a host with a wildcard domain. Requests are multiplexed over a single yamux connection and proxied with hyper end-to-end (streaming bodies, rewritten Host).
    • Options: --subdomain, --server, --token, --basic-auth.
    • [tunnel] config section (server, token) so the flags can be omitted.
    • See docs/TUNNEL.md.

0.1.5 — 2026-06-30

Fixed

  • GUI now connects to the daemon reliably. GrovePaths uses a fixed Grove directory (e.g. ~/Library/Application Support/Grove) instead of a reverse-DNS ProjectDirs name, so the CLI, root daemon and GUI always agree on the same home + IPC socket. Previously the GUI looked in com.elyra.Grove while the daemon ran in Grove, so it showed “Stopped”.

Added

  • sudo grove install now also ensures the system resolver and root CA, so *.test keeps resolving even if another tool (e.g. Herd) removed /etc/resolver/<tld>.

0.1.4 — 2026-06-30

Added

  • Root background service on macOS: sudo grove install now installs a system LaunchDaemon that binds the privileged ports (53/80/443), starts at boot, and runs PHP workers as your user (GROVE_RUN_USER). This is the piece that makes *.test serving work after just installing the app + running sudo grove install — no more manual sudo grove start.

Fixed

  • The daemon's IPC socket is now world-accessible, so the user-level GUI can talk to the root daemon.

0.1.3 — 2026-06-30

Fixed

  • PHP now serves under a privileged (root) daemon: PHP-FPM workers run as the real user (SUDO_USER/GROVE_RUN_USER) with --allow-to-run-as-root on the master, instead of php-fpm refusing to start as root.
  • Static assets are served directly (try_files): existing files such as built Vite assets under /build/ are returned as-is instead of being routed through index.php, so SPA/Vite front-ends render correctly.

Changed

  • Bumped tauri-action to v1 and several GUI dev-dependencies (Dependabot).

0.1.2 — 2026-06-29

Added

  • The desktop app now bundles the grove CLI as a sidecar, so it can locate and start the daemon (with fallbacks to common install paths).
  • macOS builds are code-signed and notarized, so the app opens without the configured — no more “app is damaged” on download.

Fixed

  • GUI “spawning daemon: No such file or directory” when the CLI wasn't on PATH.

0.1.1 — 2026-06-29

Added

  • In-app auto-update (macOS/Linux GUI): the app checks for new releases on launch and offers a one-click “Install & restart”. Updates are cryptographically signed; the release pipeline publishes signed updater artifacts + latest.json.

0.1.0 — 2026-06-29

First public release. A native, cross-platform local development environment in Rust that serves *.test domains with local HTTPS, multi-version PHP/Node and bundled services — with zero external dependencies.

Core

  • Embedded DNS resolver for *.<tld> (default test) → loopback; refuses any other TLD so it can't act as an open resolver (hickory).
  • HTTP/HTTPS reverse proxy binding 80/443, routing by Host header (hyper), with a minimal built-in FastCGI client to PHP-FPM.
  • Driver system: Laravel, WordPress, generic PHP, static, and reverse-proxy (Vite/Node) — auto-detected from filesystem signatures.
  • Local TLS: a private root CA generated on first run, with per-site leaf certificates issued on demand via SNI (rcgen + rustls, ring provider).
  • Declarative TOML config as the single source of truth.
  • Single long-running daemon binding the privileged ports; CLI and GUI are thin clients over a Unix-socket JSON-RPC (grove-ipc).

Runtimes

  • Bundled PHP: download self-contained static PHP-FPM builds (grove php install 8.5|8.4|8.3) — no Homebrew/Herd. Plus bring-your-own (grove php register) and auto-discovery.
  • Per-site PHP version (grove isolate) with lazy, on-demand FPM pools.
  • Bundled Node.js: download official node/npm/npx builds (grove node install 22); per-site Node version (grove node use).

Services (bundled, no separate install)

  • PostgreSQL and MySQL via portable prebuilt binaries; Redis built from source on install — all downloaded and supervised by Grove under $GROVE_HOME/services.
  • grove service install|start|stop|restart, persisted auto-start that only runs installed services on daemon boot, and per-service port config.
  • Built-in mail-catcher: an SMTP server that captures outgoing mail, with a Mailpit-style viewer.
  • grove env [site] generates a .env snippet wiring an app to the bundled services (DB/Redis/mail).

Sites

  • grove new — scaffold a fresh Laravel project (bundled PHP CLI + Composer) or a static site, or link an existing project.
  • grove park / link / secure / proxy; ~/Code is parked by default on grove init.
  • Valet import (grove import) for migrating existing setups.

GUI (Tauri 2 + Svelte 5)

  • Desktop app sharing the Elyra Conductor look & feel (Tokyo Night palette, JetBrains Mono), as a thin client over the daemon.
  • Panels: Sites (driver, per-site PHP/Node, HTTPS toggle, open in browser/Finder), Services, Mail, PHP, Node, Logs, Doctor, plus Settings (⌘,) and About.
  • Create New Site wizard and Park folder import.
  • macOS menu-bar icon: click to open, right-click to quit; closing the window hides Grove to the menu bar.
  • Animated boot splash.

Lifecycle & ops

  • grove init (first-run setup), start / stop / restart, gui, install / uninstall as an OS service (launchd/systemd), doctor, logs, and --json everywhere for scripting / elyra-conductor.
  • macOS resolver + trust-store integration; Linux/Windows stubs.

Notes

  • macOS is the verified platform for 0.1.0. Linux/Windows resolver and trust integration are stubbed and tracked for a later release.