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
UNIONor copy them out withINSERT ... 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.
AVGover an integer column returnsDECIMALrather thanDOUBLE, so code that decodes it strictly as a float (sqlxf64) must accept a decimal. And an account made withCREATE USERstarts 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, andLOAD DATAis 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 startwould 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 indata/were untouched. -
The standalone macOS CLI is signed and notarized. The
grove-<version>-aarch64-apple-darwin.tar.gzdownload held ad hoc signedgroveandgrove-tunnelbinaries. Fetched with a browser, they are quarantined, and Gatekeeper killed them on first run: 1.10.0'sgrove --versionexited 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 insideGrove.appwas already signed and notarized. -
The
.dmgitself is notarized and stapled. Tauri notarizes the app inside it but not the disk image, which was Developer ID signed and unnotarized, sospctlrejected it. The release now notarizes and staples the DMG and checks it withspctlbefore uploading. -
The release workflow can be dry-run.
workflow_dispatchon 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 --resetempties it. grove tryandgrove bisectput worktrees in~/.grove/try/. Try databases are named<db>__gt_<hash>. Both commands take away what they made,grove try --donewhen you are finished andgrove bisectat 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/17and/orders/18count as one route. When a change makes a route slower, the daemon logs it andgrove routesshows it, with a request id to pass togrove 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 21msTypical 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.jsonevery 30 seconds and at shutdown, so they survive a restart. The MCP toolgrove_routesgives 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-datathe 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-waland-shm.grove replay <id> --forgetdrops the baseline. The starting point is the data as it is at that first--same-datareplay, 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-datareplays 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, runscomposer installonly whencomposer.lockchanged, replays the request with its method, path, headers and body, and letsgit bisectnarrow 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 reportA commit is good when the replay answers below 500, or exactly
--expect-statuswhen 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-writeoffersgrove_sandbox_openandgrove_sandbox_close, plusgrove_sandbox_listwithout 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 ongrove try, which gained--newfor 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/invoicesIt makes a git worktree, fetching the branch from
originif it is not local. It copies your.envinto it with the site's hostname moved everywhere it appears (APP_URL,SESSION_DOMAIN,SANCTUM_STATEFUL_DOMAINS, subdomains) andDB_DATABASEpointed 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/andpublic/build/are cloned from your checkout (copy-on-write on APFS, so seconds and no disk). Thencomposer installcatches 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.--doneremoves the site, the try's database and the worktree, and refuses while the worktree holds uncommitted work, naming the files, unless--force.--listshows 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.testbefore 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.envuses, 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 installafterwards, 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 installonce 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> onorgrove db branches on.
Added
-
Databases that run only while something is connected.
grove service on-demand mysql onhands 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 listshowsidlefor 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
SIGTERMand fifteen seconds to finish, notSIGKILL, so MySQL shuts down clean and does not replay its redo log on the next start.grove service on-demand mysql offgives the port back and runs the server all the time again. -
A database per git branch.
grove db branches onin a project, and checking out a branch swaps in that branch's database: a feature branch's migrations no longer land inmain's tables, and going back tomainbrings 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 artisanin 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 TABLEmoving 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 answers503withRetry-Afterand itsgrove devprocesses restart, so a queue worker comes back on the new branch's code and data. A detachedHEAD— 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 branchesshows what is live and parked, and flags copies whose git branch is gone;offkeeps the copies andonpicks them up again;dropis 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::privdropis gone, and with it thepre_execblock that calledsetgroups/setgid/setuidbetween fork and exec — the most delicateunsafein 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 ofgeteuidwent too, as did--allow-to-run-as-rootand theuser/listen.ownerpool 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 installcreates files as root and has to hand them to the user who will read them. -
crates/grove-core/tests/privdrop_root.rsandcrates/grove-runtime/tests/probe_root.rs, which proved a drop that no longer happens.ca_ownership_root.rsstays: 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 installas 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 startsaid "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] AbortingA 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,-3suffix, reserved across concurrent requests. -
Restoring a MySQL snapshot left tables that were created after it. A
mysqldump --databasesfile recreates the tables it holds and says nothing about any others, so a restore after a migration that createdinvoicesstill hadinvoices. 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 ofunknown 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 installonce, 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, includingconfig.toml, every PHP build and the CA key, so it is onechown -Rrather 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 andgrove doctorrepeats it on thegrove-homeline.- 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 indocs/ARCHITECTURE.mdunder 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 installputs everything back.
Added
-
The daemon runs as you. The unit carries
UserNameon launchd andUser=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 restartno longer asks launchd to kickstart a system job, which it could not do unprivileged; it exits andKeepAlivebrings 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 wheregrove installcannot 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 installnow writes aSocketsdictionary into the launchd plist and a companiongrove.socketunit 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 installis what switches a machine over.grove doctorsays which sockets arrived, on theprivilegesline, and no longer warns about an unprivileged daemon that was handed :80. -
grove doctorre-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), andtrust-store— is the CA on disk what the system trusts, and is it the only Grove CA trusted. The last catches whatgrove ca rotatewithoutsudoused 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-scopelikewise reads theNameConstraintsextension off the certificate instead of trustingca-meta.jsonbeside 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.comis 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.localtestis outside.test). And the request-path sanitizer — the one function between an attacker's URL anddocument_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 @varinside a stored procedure now reaches the session instead of vanishing whenCALLreturns, 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 addsUTC_TIMESTAMP()/CONVERT_TZ(), freezesNOW()to one instant per statement, and makes||honourPIPES_AS_CONCAT. The archive unpacks to a versioned directory beside the data directory, sogrove service install elyrasqlfetches 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 restartunder the system service could leave an unprivileged daemon behind. It stopped the daemon and spawned a new one from the shell, racing launchd'sKeepAlive; 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,restartnow 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;startrefuses to spawn beside an installed service and names the command that starts it.- Linux:
grove restartleft the daemon down. The unit saidRestart=on-failure, and a deliberate restart is a clean exit. NowRestart=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 elyrasqldownloads 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 on127.0.0.1:3307beside MySQL. Your app uses the MySQL driver (DB_CONNECTION=mysql,DB_DATABASE=elyra);grove envprints the block.grove db snapshot --engine elyrasqltakes a hot, consistent copy of the database file viaBACKUP TO, andgrove db restoreputs it back. Grove tells an ElyraSQL site from a MySQL one by the port it connects to, so the agent-safe migration sandbox andgrove bundlesnapshot 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-connectionSET 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 doctorexits 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://tohttps://(301for GET/HEAD,308otherwise). A curl in a script or a webhook target that reached agrove secured site over plaintext should use the HTTPS URL. grove uninstallneedssudoand leavesGROVE_HOME(config, PHP builds, databases) and the PATH shims in place unless you pass--purge.grove initexits 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--userunit 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
101and a dead socket, so Vite HMR on a proxy site, Next/Nuxt dev servers and Reverb overwss://myapp.testreconnect-looped. Upgrades get a dedicated upstream connection and a bidirectional copy until either side closes. http://→https://redirect for secured sites.site.securewas set bygrove secureand never read on the request path; a typed hostname served plaintext, PHP sawHTTPS="", and Laravel builthttp://asset URLs — mixed content against the HTTPS Vite server. TheLocationcarries the configured HTTPS port.- Error pages that name the fix. Every Grove-generated error was a bare
text/plainline — 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 offersgrove dev start <site>; an unknown host offersgrove link; a missing PHP offersgrove php install <v>. - Static files:
Rangerequests (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 asapplication/octet-stream. - Dev-sized PHP defaults via the pool config: 512M uploads, post size and
memory, 300s scripts, and a 600s
request_terminate_timeoutso a wedged worker is recycled. PHP's compiled-in 8M/128M/30s applied before, since Grove writes no php.ini. Set withphp_value, so an app's ownini_set()wins. grove statusandgrove doctorreport what actually bound. Each of dns/http/https/mail is recorded at bind time and shown with the OS error and, wherelsof/sswill 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 indaemon.log.grove doctorworks without the daemon. The config, CA and resolver checks run locally when the socket does not answer, followed by an explicitdaemonfailure, instead of the old "not running" and nothing else. It also reports state files that were set aside as unparsable.- A
resolvercheck 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 reloadre-readsconfig.tomland rebuilds the site list without a restart. Agrove link/securethat would overwrite a hand edit the daemon has not read refuses and points here; before, the edit was silently lost.grove uninstall --purgealso removesGROVE_HOMEand 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 daemonrefuses to start over a live one instead of unlinking its socket and overwriting its pidfile. grove startparsesconfig.tomlfirst 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 installwrites a system unit (root, children dropped to you) instead of a--userunit that could not bind 53/80/443; DNS is routed through systemd-resolved via agrove0dummy 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-prowhen 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_TOKENwhen 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
statusshapes 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 stoponly 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_HOMEfails 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
502until 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 rotatewithoutsudobroke 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:authorityand has noHostheader; the handler read only the header. Caught by the end-to-end smoke test the same day h2 was enabled, before release. grove uninstallwithoutsudoprinted "removed" having removed nothing. Every step waslet _ =. It now refuses without elevation, stops the daemon first, reports each step, and exits non-zero if any failed.grove initexited 0 when the PHP download or CA generation failed. Undersudo, theconfig.tomlit writes is now chowned to the invoking user, so a later plaingrove initorgrove importno longer fails on a root-owned file.grove path showsaid Grove's toolchain was on PATH when Homebrew'sphpstill won. It checked membership, not order; it now names thephpthat actually resolves first and how to move Grove ahead of it.- The SPA fallback served
index.htmlas200 text/htmlfor 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-Lengthstreamed through unchecked. - A hand edit to
config.tomlwas silently overwritten by the nextgrove link;Request::Reloadrebuilt 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.mdshowedgrove initandgrove doctoroutput that was never what they printed — including✓ resolverand✓ dnsdoctor 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, andplist/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 doctorwarns until you do. Every site's certificate is re-issued on next use.grove-tunnelnow requires--token. An empty token used to disable authentication, so the simplest way to start a server was also the open one. Pass--allow-anonymousto keep that behaviour deliberately.grove secret shareis 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 curlnow emitsAuthorization: [redacted]. The snippet will not reproduce an authenticated request until you put your own credential back.grove replayis 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
-
cpxis part of the bundled toolchain.grove path installnow creates acpxshim alongsidephp,composer,node,npm,npxandlaravel, 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 analyseNothing 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 '…'andcpx tinkerboot your Laravel app (config, facades,.env,$app), andcpx tinkerhands off to the project's ownphp artisan tinkerwhen it has one.The PHAR lives at
~/.grove/cpx.pharrather than under the root-owned$GROVE_HOME, socpx self-updatecan 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 extdiffs a build's realphp -magainst 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 installprints required-tier misses right after installing,grove php listcarries a one-line summary per build,grove doctorhas aphp-extensionscheck, 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
commonhaspdo_sqlite/pdo_pgsqlbut nointl,mysqli,sodium,readline,apcuorxsl; upstreambulkhas those six but drops both PDO drivers. Laravel's default database is SQLite, Grove bundles PostgreSQL, WordPress speaks onlymysqli, and a large slice of Packagistrequiresext-intl— so no choice between the two was the right one..github/workflows/php-build.ymlnow builds the union with static-php-cli, for macOS and Linux on both architectures, and publishes it to a rollingphp-runtimesrelease.grove php installfetches that by default (--variant grove,[general].php_variant = "grove").The extension list is authored once, in
grove_runtime::extensions::BUILD_SET, andgrove php craftprints 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 installfalls back to upstreamcommon, names the extensions that costs you, and labels the build with the set it actually got rather than the one requested. Asking for--variant commonor--variant bulkexplicitly never falls back: silently trading one extension hole for the other would be worse than an error.GROVE_PHP_MIRRORstill 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 installnow fetches the matching CLI binary along withphp-fpm, and records which variant a build came from.grove php ext,grove php listand the PATH shims all need the CLI, and auditing a build whosephp -mcame 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-dbis pinned to a revision in[workspace.dependencies]rather than tracking whateverkwhorne/elast pushed. An unpinned git dependency let a commit there land in Grove's lockfile with no review — which is howkeyring, and with it alibdbus-1-devbuild requirement on Linux, nearly arrived.
Security
- The daemon's IPC socket authenticates its callers. It was
0777with no check at all, and its requests are not advisory:PhpInstallandServiceInstallmake root download and execute a binary,DbDumpFilemakes root write a file to a caller-chosen path. Any local process could issue them — a compromisednpm/composerpostinstall hook, or one of the PHP apps Grove serves. The socket is now0660, owned by the user Grove serves, and every connection is checked withSO_PEERCRED/LOCAL_PEERCREDbefore 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 thephp-builds.jsonthat 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, andgrove 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.keyhad root write andchmodthe target. The opposite failure was the Vite dev-server keys, which got nochmodat all and so were simply left world-readable. Every sensitive write now sets its mode inopen(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.comor 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 viaredis/redis-hashes. Upstreamcommon/bulkpublish 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
Authorizationheader, 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 replayuses 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-Foris 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
0600rather than0644. 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_HOMEis user-controlled undersudo -E, and a value containing</string>could have injected keys into a root service.
Fixed
- Leaf certificates now actually renew.
issue_leafgives 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 ownnotAfter. - An incomplete root CA is no longer silently replaced.
load_or_createregenerated 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 trustno 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 — "timeoutheader_read_timeoutset, but no timer set". The HTTP and HTTPS listeners now installTokioTimer.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 statusand the GUI both showing Running, while not a single site rendered. Under the previouspanic = "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-versionsaid1.80while the real floor had moved to1.94, socargo buildon the documented version failed insidetime-corewith "the package requires the Cargo feature callededition2024" — 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 newMSRVjob 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.
svelte5.56.8,vite8.2.0,@sveltejs/vite-plugin-svelte7.2.0,svelte-check4.7.4,@fontsource/jetbrains-mono5.3.0 and@tauri-apps/plugin-dialog2.7.2. TypeScript stays on 6: version 7 is the Go port, which needs both 7 and 6 installed plus a--tsgoflag, andsvelte-checkdoes 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. base640.23.1,thiserror2.0.20,toml1.1.3, andpnpm/action-setup4 → 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, butcert_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.
acceptfailing was answered with a barecontinue; underEMFILEthat 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/existswere 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
hyperclient 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 answerIf-None-Matchwith304, so reloading a dev site with unchanged assets no longer re-reads and re-sends every byte.Cache-Control: no-cachekeeps 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'smDNSResponderin that path on every first byte. The answer is always loopback and never changes, so it is nowTTL 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. Nowopt-level = 3with 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:
rcgen0.13→0.14 (Issuerreplaces passing a certificate and key tosigned_by),sqlx0.8→0.9 (non-literal queries now require an explicitAssertSqlSafe, with the audit written down where the conversion happens),hickory-dns0.24→0.26 (Server,zone_handler,Metadata/HeaderCounts,request_info()), andage0.10→0.12 (borrowed recipients, andis_scrypt()in place of theDecryptorenum). Alsothiserror1→2,ed25519-dalek2→3,rand0.8→0.10,toml0.8→1.1,directories5→6,base640.22→0.23.- Requests to the DNS resolver carrying anything other than exactly one question are now refused rather than guessed at.
- The
ageupgrade 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_LENGTHrequirement 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
STDINrecords withCONTENT_LENGTHset 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
411would have made Grove the reason a valid request fails; nginx and Apache spool, so Grove spools too. Spool files are0600inside a0700directory and are removed when the body is dropped — tied toDrop, 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 replayand 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.
- The proxy driver (Vite, Node) now forwards the body as it arrives,
whatever its transfer encoding. CGI's
- 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
STDINwhile 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.
Recordnow 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
STDOUTrecord and returned only onEND_REQUEST, andFull<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 ontext/event-stream: the problem was general. .phpfiles are executed, not served as source. With a PHP driver, any existing.phpfile in the document root was handed back as text — so/index.phpdisclosed the front controller on every PHP site. It also meant WordPress could not work at all, sincewp-login.phpandwp-admin/*.phpmust execute. Such requests now go to PHP-FPM withSCRIPT_FILENAMEpointing at the file, matching what nginx and Apache do. Matched case-insensitively, because a case-insensitive filesystem resolves/INDEX.PHPto the same file.- Dot-prefixed paths are refused. A plain PHP project's document root is the
project root, so
/.envreturnedAPP_KEYin 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_msin 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_LENGTHfor 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 devnow runs the processes your app declares. On Laravel 13.16+, Grove readsphp artisan dev:list --json --except-vendorand supervises that list instead of guessing, so userland processes registered throughDevCommands— Reverb, Horizon,stripe listen— are started alongside Vite and the queue worker, each with its owndev-<site>-<name>.log.serverandlogsare skipped: Grove already serves the site over FPM, andgrove logsalready 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'sNodePackageManagerdetection by way of the declared command,pnpm,yarnandbunprojects now work without special casing. Rungrove devinstead ofphp artisan dev, not alongside it.grove path installputs thegroveCLI itself on yourPATH, next to thephp/composer/node/npm/npx/laravelshims. Previously a user who installed the macOS app and rangrove path installstill had nogrovecommand, because the binary only existed inside the.appbundle.grove path show --jsonreports this ascli_installed.grove dev startwarns about a competingphp 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 stoptake an optional site argument, defaulting to the site in the current directory (resolved fromgrove.toml'sname, else the directory name) — matchinggrove link,grove secureandgrove up.
Fixed
grove dev stopno longer orphans processes. Dev processes were killed directly, butnpm run devspawns Vite as a grandchild, which survived and kept holding port 5173 — breaking the nextgrove dev start. Each dev process now runs in its own process group and is stopped group-wide (SIGTERM, thenSIGKILL).
Notes
- Intel macOS is no longer shipped. Notarization on GitHub's
macos-13runner routinely hangs, so the release workflow builds Apple Silicon and Linux only. Intel Macs can still build from source withcargo build --release. This took effect after 1.2.1; 1.3.0 is the first release to state it. grove devreplacesphp artisan devrather than complementing it. Running both doubles every process;grove dev startnow warns when it sees a competingphp artisan dev.- On Laravel 13.16+,
grove dev startboots the application once to readdev: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_sandboxedandgrove_sql_sandboxednow return achainalongside 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 newChainForWindowIPC command that generalizes the request causal chain to any time window.
Fixed
- Auto-updater 403s. The release now rewrites
latest.jsonto use the publicbrowser_download_urlfor each artifact instead of the rate-limitedapi.github.comasset endpoint, so in-place updates no longer fail intermittently with403 Forbidden.
Internal
grove-servicesnow declares thetimecrate'sparsingfeature 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. Runsphp artisan <command>(defaultmigrate --force) inside an automatic snapshot sandbox: Grove snapshots the database first, runs the migration, reports the schema diff, and automatically rolls back on failure. Passroll_back: truefor 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, returningrows_affected. Read-only statements are refused — usegrove_db_queryfor those.- Both write tools cover bundled MySQL/PostgreSQL (daemon snapshot) and
SQLite (snapshotted by copying the
.sqlitefile).
Added — request causal chain
grove_request_chainMCP tool (andRequestChainIPC 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 thegrove_explainMCP 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 mcpruns 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 atgrove mcpand 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 with200— a local webhook.site. Expose it withgrove 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 hookson the CLI. - Turn a request into a test. From any captured request or webhook, copy it
as a
curlcommand, a.httpfile, 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 exportpackages a project'sgrove.toml,.env, and database into one shareable file;grove bundle importunpacks 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 runningSELECTqueries 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
.envwith your team securely — encrypted end-to-end, so it never gets pasted into a chat window. Manage access withgrove 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 installno 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/binto your PATH (the command prints the line).
0.8.0 — 2026-07-07
Added
grove.toml+grove up— reproducible project environments. Commit a smallgrove.tomldescribing what a project needs (PHP/Node versions, bundled services, HTTPS, dev processes); a teammate then goes fromgit cloneto a running, identical setup with a single command:grove uplinks the project, pins its PHP/Node, ensures its services are installed + running, and (optionally) starts its dev processes.grove up --writescaffolds a friendly, commented startergrove.toml.grove up [path]targets a directory other than the cwd;--no-devskips 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
*.testsite, 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 (--jsonsupported). 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 forphp,composer,node,npm,npxandlaravelthat resolve to whatever version each project pins (viagrove 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 strayviteservers squatting ports (which causedpublic/hotto point at a stale server).
0.5.1 — 2026-07-06
Added
- Vite over HTTPS, automatically. When
grove devstarts the Vite server for an HTTPS site, Grove issues a CA-trusted leaf certificate for the host and passes it via the standardVITE_DEV_SERVER_CERT/VITE_DEV_SERVER_KEYenv vars thatlaravel-vite-pluginreads. Vite then serves HTTPS with a trusted cert — no mixed-content, HMR just works — with no Herd/Valet directories involved. (Use the standardlaravel-vite-plugininvite.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-syncqueue, 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 noartisan serveto run. Toggle it with the ⚡ button per site in the GUI, orgrove dev start|stop|list <site>— a Grove-aware replacement forcomposer run dev.
0.4.2 — 2026-07-06
Fixed
- Proxy sites now hit the right virtual host. The reverse proxy set
Hostto the upstream authority (and forwards the public host asX-Forwarded-HostX-Forwarded-Proto), so name-based vhosts — e.g. an nginx container withserver_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 composeprojects are now served as<project>.testeven without labels — Grove picks the web container (by service name / published web port) and proxies to it. Explicitdev.orbstack.domains/grove.hostlabels still take precedence. - Start / stop / restart containers from the GUI. Docker sites in the Sites
table gain ▶ / ⏹ / ↻ controls; stopped containers show as
stoppedwith 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>.testwith its trusted local HTTPS — right next to native sites, in the same dashboard. A container is picked up when it carries adev.orbstack.domainslabel (Grove reuses OrbStack's own routing) or an explicitgrove.hostlabel; 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 sharecan tunnel them publicly too. Toggle with[general].dockerinconfig.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 — vialaravel new --using=<repo>.
0.3.0 — 2026-07-03
Changed
- New sites now scaffold with the official
laravel newinstaller (latest Laravel) and a starter-kit picker — None, Livewire, React (Inertia) or Vue (Inertia) — replacingcomposer create-project. The GUI's “Create a new site” dialog gained a Starter kit selector; on the CLI usegrove 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-dINI overrides — the globalphp.iniis never touched, and pools respawn instantly when toggled. Xdebug runs instart_with_request=triggermode, so it stays dormant (near-zero overhead) until a request opts in with theXDEBUG_TRIGGERcookie/param;grove debug envprints 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 loadablexdebug.soin itsextension_dir. Grove's own fully-static builds can't load Xdebug (static PHP can'tdlopen, and static-php-cli can't compile it in), so those report as unavailable ingrove 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
Hostheader to the local site name — it preserves the public host so the app builds correct public asset URLs, and routes locally via a newX-Grove-Siteheader instead. It also setsX-Forwarded-Proto, and Grove's proxy maps it to FastCGIHTTPS=on, so apps generatehttps://URLs (no mixed-content blocking) without needing TrustProxies configured.Update both the macOS app and the
grove-tunnelserver 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.shfor 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, sogrove share <site>works out of the box and gives ahttps://<random>.grove.elyracode.comURL — no[tunnel]config needed. - Open-server mode.
grove-tunnelcan run without a token (omit--token) for a public community server; clients no longer need a token. - On-demand HTTPS authorization.
grove-tunnelexposes/__grove_askso 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 sharealso 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 newignoredlist inconfig.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*.testsite at a public URL for demos, real-device testing and webhooks.- New
grove-tunnelserver binary you deploy on a host with a wildcard domain. Requests are multiplexed over a single yamux connection and proxied withhyperend-to-end (streaming bodies, rewrittenHost). - 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.
GrovePathsuses a fixedGrovedirectory (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 incom.elyra.Grovewhile the daemon ran inGrove, so it showed “Stopped”.
Added
sudo grove installnow also ensures the system resolver and root CA, so*.testkeeps 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 installnow 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*.testserving work after just installing the app + runningsudo grove install— no more manualsudo 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-rooton 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 throughindex.php, so SPA/Vite front-ends render correctly.
Changed
- Bumped
tauri-actionto v1 and several GUI dev-dependencies (Dependabot).
0.1.2 — 2026-06-29
Added
- The desktop app now bundles the
groveCLI 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>(defaulttest) → loopback; refuses any other TLD so it can't act as an open resolver (hickory). - HTTP/HTTPS reverse proxy binding 80/443, routing by
Hostheader (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,
ringprovider). - 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.envsnippet 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;~/Codeis parked by default ongrove 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/uninstallas an OS service (launchd/systemd),doctor,logs, and--jsoneverywhere 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.