v0.2-alpha: Docker backend for all 4 plugins (mysql / postgres / redis / elasticsearch)#6
Merged
Merged
Conversation
The v0.1.x nix backend hit two blockers under dogfood:
1. macOS 104-char sun_path limit truncates services-flake's
configure-mysql socket path when worktrees nest deeply, so the
init script cannot create root@'%' and the host-side ReadyCheck
probe (which dials 127.0.0.1) is never accepted by mysqld.
2. The mysql plugin's ReadyCheck has no TCP-fallback; it shells out
to `mysql -e "SELECT 1"` which requires the mysql client on PATH
(absent when bough is invoked outside a nix shell).
Both blockers disappear under Docker: the container's internal socket
is short by construction, mysqladmin ships inside the image, and host
clients only ever speak TCP via the published port.
This commit adds the Docker code path to the mysql plugin only — the
other three plugins still use nix exclusively and will follow in
subsequent commits. Selection is via `databases[].backend: docker` in
the YAML, which the host injects into `UpReq.Extras["backend"]`. Down
/ ReadyCheck self-detect by looking up a `bough-mysql-<port>` container
so the plugin stays backward-compatible with the existing proto.
Image: `mysql:8.4` (override with `extras["docker.image"]`).
Wait strategy: TCP dial → mysqladmin ping via `ContainerExecCreate`,
inspired by testcontainers-go modules/mysql.
Stop timeout: 30 s (InnoDB graceful shutdown budget).
Labels: `com.bough.managed=true`, `com.bough.engine=mysql`,
`com.bough.image=mysql:8.4`, `com.bough.host-port=<n>` for
label-query-based orphan cleanup later.
Smoke test (cmd/_smoke-docker-mysql) timing on darwin/arm64,
warm Docker Desktop, empty datadir:
Up 24.0 s (image pull mysql:8.4 + create + start)
ReadyCheck 7.6 s (TCP probe + mysqladmin ping)
Down 30.0 s (full graceful budget)
Cleanup 0.008 s
Total 64 s (vs v0.1.x nix path which never completed)
Docker SDK: `github.com/docker/docker` v28 monorepo path (the
`moby/moby/client` spinoff at v0.4.1 introduces ambiguous-import
errors with the monorepo's vendored copies of the same packages).
Extends the v0.2-alpha Docker backend rollout to the remaining three plugins so all four engines now ship the dispatch pair (`req.Extras[ "backend"] == "docker"` → docker.go) and back-detection helper that mysql got in 1d2bd97 / eb41946. Each plugin keeps the existing nix code path intact for users who set `backend: nix` or omit it. Per-engine choices (research notes are in the file headers): postgres - image: postgres:16-alpine (Docker Hub recommended) - env: POSTGRES_PASSWORD=bough, POSTGRES_USER=bough, POSTGRES_DB=<initial-databases[0]> - cmd: `postgres -c fsync=off` — testcontainers' default for dev speed; per-worktree env is throwaway - wait: TCP + `pg_isready -h localhost` (driver-agnostic so the strategy survives image swaps to CockroachDB / YugabyteDB) - stop timeout: 15 s redis - image: redis:7-alpine (~5 MB) - cmd: `redis-server --bind 0.0.0.0 --save 60 1 --loglevel warning --appendonly yes` (verbatim Docker Hub-recommended persistence invocation; bind 0.0.0.0 is intentional — Docker publish target is 127.0.0.1:<host-port> so external access stays shut) - wait: TCP + `redis-cli ping` → PONG - stop timeout: 5 s elasticsearch - image: docker.elastic.co/elasticsearch/elasticsearch:7.17.29 (last 7-line LTS with first-class linux/arm64 layer) - env: discovery.type=single-node, xpack.security.enabled=false, cluster.routing.allocation.disk.threshold_enabled=false, ES_JAVA_OPTS=-Xms1g -Xmx1g - ulimits: memlock=-1:-1, nofile=65535 (HostConfig.Resources.Ulimits per Elastic 7.17 docs) - host: vm.max_map_count >= 262144 is required but not enforced by the plugin (Docker Desktop VM and Linux kernel are out of bough's control); operators see ES log a warning and fail-fast before ReadyCheck times out - wait: TCP + HTTP GET / (200 ⇒ yellow-or-better, single-node ES is always yellow because no replica to assign) - stop timeout: 60 s — translog flush + Lucene commit can run long once an index has data Smoke timings (darwin/arm64, warm Docker Desktop 29.2.1, cold image pull for postgres / redis / elasticsearch — mysql was already pulled from the previous run): postgres Up 8.4 s + Ready 2.9 s + Down 0.2 s = 13.6 s redis Up 3.9 s + Ready 0.1 s + Down 0.2 s = 6.3 s elasticsearch Up 19 s + Ready 21 s + Down 1.2 s = 43.6 s vs the v0.1.x nix path which never reached ready inside 600 s on the auba dogfood for any of the three. All three smoke programs leave no orphaned containers behind (label- query verified after each Cleanup).
ikeikeikeike
added a commit
that referenced
this pull request
Jun 21, 2026
…em0 namespace Round 4 review #23 surfaced six findings that block v0.6.0 ship. This commit clears all six in-place; remaining medium / low items land in follow-up commits. #1 internal/cli/capability.go: runCompile passed `currentScope(nil, ...)`, but currentScope dereferences cfg.Repositories on the default path → nil pointer panic on every `bough capability compile/preview`. Use an explicit Scope{Level: worktree, WorktreeID: "default"} literal until --scope flags land in v0.6.x. #2 + #3 cmd/bough-mcp-server/main.go + internal/mcp/server.go: WatchStdin and Server.Run both Read from os.Stdin concurrently, so the watchdog stole bytes the JSON-RPC scanner needed. The ZOMBIE guard also only triggered on io.EOF, so wrapped errors left the goroutine spinning. Both go away by deleting WatchStdin entirely and letting Server.Run's bufio.Scanner detect EOF — main.go's `defer server.Shutdown()` handles the subprocess teardown. #4 + #5 internal/pluginsign/verifier.go + internal/cli/plugin_verify.go: verifyCosign defaulted SigPath to ".bundle" but .goreleaser.yaml writes ".sig", and the command was missing the --certificate-identity / --certificate-oidc-issuer flags cosign 2.x requires for keyless verify. Default SigPath now prefers .sig (with .bundle fallback for legacy artifacts), CertPath defaults to the GoReleaser .pem companion, and the Request grows CertIdentity / CertIdentityRegexp / CertOIDCIssuer fields the CLI exposes as --cert-identity / --cert-identity-regexp / --cert-issuer. PreRunE now validates --pubkey is set for minisign and --cert-identity (or regexp) is set for cosign BEFORE shelling out so the operator sees an Args error rather than a phantom verify failure. #6 internal/mcp/server.go: handleResourcesRead built Scope{Level: level} with no RepoName/WorktreeID, then asked the backend for a count. sqlite's scopeID returns "/" for that shape, so the filter never matched a real worktree row and the resource reported `worktree: 0` even when worktree rows existed. The honest fix is to stop returning a faithful count the host cannot guarantee: readScopesResource now lists the three scope tiers with descriptions and points clients at memory.query for counts (v0.6.x will add an explicit count API once Server learns about cfg.Repositories). #7 (review #23) plugins/memory/mem0/namespace.go: scopeToMem0 worktree case derived rootHash from `RepoName + "/" + WorktreeID`, so two worktrees of the same repo got different user_ids and the documented user-tier sharing did not hold. Use hashShort(RepoName) for both segments; branch identity continues to live entirely in session_id. Build clean, all unit + conformance tests pass, lint 0 issues.
ikeikeikeike
added a commit
that referenced
this pull request
Jun 21, 2026
…em0 namespace Round 4 review #23 surfaced six findings that block v0.6.0 ship. This commit clears all six in-place; remaining medium / low items land in follow-up commits. #1 internal/cli/capability.go: runCompile passed `currentScope(nil, ...)`, but currentScope dereferences cfg.Repositories on the default path → nil pointer panic on every `bough capability compile/preview`. Use an explicit Scope{Level: worktree, WorktreeID: "default"} literal until --scope flags land in v0.6.x. #2 + #3 cmd/bough-mcp-server/main.go + internal/mcp/server.go: WatchStdin and Server.Run both Read from os.Stdin concurrently, so the watchdog stole bytes the JSON-RPC scanner needed. The ZOMBIE guard also only triggered on io.EOF, so wrapped errors left the goroutine spinning. Both go away by deleting WatchStdin entirely and letting Server.Run's bufio.Scanner detect EOF — main.go's `defer server.Shutdown()` handles the subprocess teardown. #4 + #5 internal/pluginsign/verifier.go + internal/cli/plugin_verify.go: verifyCosign defaulted SigPath to ".bundle" but .goreleaser.yaml writes ".sig", and the command was missing the --certificate-identity / --certificate-oidc-issuer flags cosign 2.x requires for keyless verify. Default SigPath now prefers .sig (with .bundle fallback for legacy artifacts), CertPath defaults to the GoReleaser .pem companion, and the Request grows CertIdentity / CertIdentityRegexp / CertOIDCIssuer fields the CLI exposes as --cert-identity / --cert-identity-regexp / --cert-issuer. PreRunE now validates --pubkey is set for minisign and --cert-identity (or regexp) is set for cosign BEFORE shelling out so the operator sees an Args error rather than a phantom verify failure. #6 internal/mcp/server.go: handleResourcesRead built Scope{Level: level} with no RepoName/WorktreeID, then asked the backend for a count. sqlite's scopeID returns "/" for that shape, so the filter never matched a real worktree row and the resource reported `worktree: 0` even when worktree rows existed. The honest fix is to stop returning a faithful count the host cannot guarantee: readScopesResource now lists the three scope tiers with descriptions and points clients at memory.query for counts (v0.6.x will add an explicit count API once Server learns about cfg.Repositories). #7 (review #23) plugins/memory/mem0/namespace.go: scopeToMem0 worktree case derived rootHash from `RepoName + "/" + WorktreeID`, so two worktrees of the same repo got different user_ids and the documented user-tier sharing did not hold. Use hashShort(RepoName) for both segments; branch identity continues to live entirely in session_id. Build clean, all unit + conformance tests pass, lint 0 issues.
This was referenced Jun 28, 2026
Merged
ikeikeikeike
added a commit
that referenced
this pull request
Jul 3, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Λ-5a complete: Docker code path added to all four DB plugins.
Selection via
databases[].backend: dockerin.worktree-isolation.yaml; the host injects the choice intoUpReq.Extras["backend"]. Down / ReadyCheck self-detect by looking upa
bough-<engine>-<port>container, so the proto stays unchanged.The existing nix code path remains the default when
backend:isomitted, so v0.1.x users see no behaviour change.
Why
Λ-2.5b auba dogfood (v0.1.1 ship + flake.lock + 600 s ready timeout)
surfaced two structural bugs in the nix mysql plugin that block real
use:
configure-mysqlscript hits the macOS 104-charsun_pathlimit on deeply-nested worktree datadirs.ReadyCheckshells out to themysqlclient withouta TCP fallback, so it can't see a running mysqld unless the host is
in a nix shell.
Docker resolves both structurally:
docker execSmoke (darwin/arm64, warm Docker Desktop 29.2.1)
vs v0.1.1 nix which never reached ready inside 600 s for mysql.
All four leave no orphan containers behind (label-query verified).
Engine-specific patterns
Each plugin's
docker.gocarries a verbose comment header pointing atthe testcontainers-go module, the Docker Hub image page, and any
Elastic-specific docs that motivated the choice. The high-level
decisions are:
mysql:8.4, envMYSQL_ALLOW_EMPTY_PASSWORD=yes(dev only),wait via TCP +
mysqladmin pingpostgres:16-alpine,postgres -c fsync=offfor cold-start speed, wait via TCP +
pg_isreadyredis:7-alpine,--bind 0.0.0.0 --save 60 1 --appendonly yes, wait via TCP +redis-cli pingdocker.elastic.co/elasticsearch/elasticsearch:7.17.29,discovery.type=single-node,ES_JAVA_OPTS=-Xms1g -Xmx1g,ulimits memlock + nofile, wait via TCP + HTTP GET /
Test plan
go build ./...succeeds innix develop .#cigo test ./...passes (all 12 packages, no skips)cycle
else docker)
backend: dockeron all three databasesOut of scope (follow-ups)
automode that detects nix/docker availability at runtime(Λ-5b)
newDockerClient/pullIfMissing/lookupByName/usingDockerBackendhelpers into a sharedpkg/dockerutil/package (intentionally left duplicated for v0.2-alpha so each plugin file is self-contained)
backendfield onUpReq(currentlypassed through
Extrasmap)