Skip to content

Commit 32507e2

Browse files
DreadPirateRobertztrilliumclaude
authored andcommitted
fix: propagate BEADS_DOLT_SERVER_HOST so bd doesn't default to localhost
Adds GT_DOLT_HOST → BEADS_DOLT_SERVER_HOST translation everywhere gt already propagates GT_DOLT_PORT → BEADS_DOLT_PORT. Without this, bd subprocesses fall back to 127.0.0.1 when the Dolt server runs on a remote machine (e.g., over Tailscale), causing recurring config drift. Fixes #2827. Co-Authored-By: Trillium Smith <[email protected]> Co-Authored-By: Claude Opus 4.6 <[email protected]>
1 parent a7e81a8 commit 32507e2

8 files changed

Lines changed: 103 additions & 28 deletions

File tree

docs/design/dolt-storage.md

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -32,8 +32,26 @@ Dolt SQL Server (one per town, port 3307)
3232
**Data directory**: `~/gt/.dolt-data/` — each subdirectory is a database
3333
accessible via `USE <name>` in SQL.
3434

35-
**Connection**: `root@tcp(127.0.0.1:3307)/<database>` (no password for
36-
localhost).
35+
**Connection**: `root@tcp(<host>:3307)/<database>` (no password).
36+
37+
## Environment Variables
38+
39+
gt and bd use separate env vars for Dolt connection. gt automatically
40+
translates its variables to bd's equivalents when spawning agents.
41+
42+
| gt (Gas Town) | bd (Beads) | Purpose |
43+
|---------------|------------|---------|
44+
| `GT_DOLT_HOST` | `BEADS_DOLT_SERVER_HOST` | Server host (bd defaults to `127.0.0.1` if unset) |
45+
| `GT_DOLT_PORT` | `BEADS_DOLT_PORT` | Server port (default: `3307`) |
46+
47+
**Remote Dolt servers**: If Dolt runs on a different machine (e.g., over
48+
Tailscale), set `GT_DOLT_HOST` in the environment. gt propagates this as
49+
`BEADS_DOLT_SERVER_HOST` to all bd subprocesses, overriding bd's hardcoded
50+
`127.0.0.1` default. Without this, every new rig/worktree/polecat silently
51+
connects to localhost and fails.
52+
53+
Per-workspace override: set `dolt.host` in a rig's `.beads/config.yaml`.
54+
This takes priority over the env var for that specific workspace.
3755

3856
## Commands
3957

internal/beads/beads.go

Lines changed: 20 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -576,14 +576,15 @@ func (b *Beads) buildRoutingEnv() []string {
576576
// environment slice. This ensures test isolation by preventing inherited
577577
// BD_ACTOR, BEADS_DB, GT_ROOT, HOME etc. from routing commands to production databases.
578578
//
579-
// Preserves GT_DOLT_PORT and BEADS_DOLT_PORT so that isolated-mode tests can
580-
// reach a test Dolt server on a non-default port.
579+
// Preserves GT_DOLT_PORT, BEADS_DOLT_PORT, and BEADS_DOLT_SERVER_HOST so that
580+
// isolated-mode tests can reach a test Dolt server on a non-default port/host.
581581
func filterBeadsEnv(environ []string) []string {
582582
filtered := make([]string, 0, len(environ))
583583
for _, env := range environ {
584-
// Preserve port env vars needed to reach test Dolt servers.
584+
// Preserve Dolt connection env vars needed to reach test/remote Dolt servers.
585585
// These must be checked before the broad BEADS_ prefix strip below.
586586
if strings.HasPrefix(env, "BEADS_DOLT_PORT=") ||
587+
strings.HasPrefix(env, "BEADS_DOLT_SERVER_HOST=") ||
587588
strings.HasPrefix(env, "GT_DOLT_PORT=") {
588589
filtered = append(filtered, env)
589590
continue
@@ -603,24 +604,34 @@ func filterBeadsEnv(environ []string) []string {
603604
return filtered
604605
}
605606

606-
// translateDoltPort ensures BEADS_DOLT_PORT is set when GT_DOLT_PORT is present.
607-
// Gas Town uses GT_DOLT_PORT; beads uses BEADS_DOLT_PORT. This translation
608-
// prevents bd subprocesses from falling back to metadata.json's port 3307
609-
// (production) when a test or daemon has set GT_DOLT_PORT to an alternate port.
607+
// translateDoltPort ensures BEADS_DOLT_PORT and BEADS_DOLT_SERVER_HOST are set
608+
// when their GT_ counterparts are present. Gas Town uses GT_DOLT_PORT and
609+
// GT_DOLT_HOST; beads uses BEADS_DOLT_PORT and BEADS_DOLT_SERVER_HOST. This
610+
// translation prevents bd subprocesses from falling back to localhost:3307
611+
// when a test or daemon has set GT_DOLT_* to alternate values.
610612
func translateDoltPort(env []string) []string {
611-
var gtPort string
612-
hasBDP := false
613+
var gtPort, gtHost string
614+
hasBDP, hasBDH := false, false
613615
for _, e := range env {
614616
if strings.HasPrefix(e, "GT_DOLT_PORT=") {
615617
gtPort = strings.TrimPrefix(e, "GT_DOLT_PORT=")
616618
}
619+
if strings.HasPrefix(e, "GT_DOLT_HOST=") {
620+
gtHost = strings.TrimPrefix(e, "GT_DOLT_HOST=")
621+
}
617622
if strings.HasPrefix(e, "BEADS_DOLT_PORT=") {
618623
hasBDP = true
619624
}
625+
if strings.HasPrefix(e, "BEADS_DOLT_SERVER_HOST=") {
626+
hasBDH = true
627+
}
620628
}
621629
if gtPort != "" && !hasBDP {
622630
env = append(env, "BEADS_DOLT_PORT="+gtPort)
623631
}
632+
if gtHost != "" && !hasBDH {
633+
env = append(env, "BEADS_DOLT_SERVER_HOST="+gtHost)
634+
}
624635
return env
625636
}
626637

internal/cmd/dashboard.go

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -153,11 +153,11 @@ func runDashboard(cmd *cobra.Command, args []string) error {
153153
return server.ListenAndServe()
154154
}
155155

156-
// ensureDoltPortEnv sets GT_DOLT_PORT and BEADS_DOLT_PORT to the actual Dolt
157-
// SQL server port. This prevents bd subprocesses from inheriting a stale or
158-
// incorrect port (e.g., the dashboard's HTTP listen port) from the environment.
159-
// Reads the running port from daemon/dolt-state.json; falls back to the
160-
// daemon.json env config; otherwise uses the Dolt default (3307).
156+
// ensureDoltPortEnv sets GT_DOLT_PORT, BEADS_DOLT_PORT, and BEADS_DOLT_SERVER_HOST
157+
// to the actual Dolt server connection info. This prevents bd subprocesses from
158+
// inheriting stale or incorrect values from the environment.
159+
// Reads the running state from daemon/dolt-state.json; falls back to
160+
// doltserver.DefaultConfig; otherwise uses the Dolt defaults.
161161
func ensureDoltPortEnv(townRoot string) {
162162
var port int
163163
if state, err := doltserver.LoadState(townRoot); err == nil && state.Port > 0 {
@@ -168,6 +168,12 @@ func ensureDoltPortEnv(townRoot string) {
168168
portStr := strconv.Itoa(port)
169169
os.Setenv("GT_DOLT_PORT", portStr)
170170
os.Setenv("BEADS_DOLT_PORT", portStr)
171+
172+
// Propagate host so bd doesn't fall back to 127.0.0.1.
173+
doltCfg := doltserver.DefaultConfig(townRoot)
174+
if doltCfg.Host != "" {
175+
os.Setenv("BEADS_DOLT_SERVER_HOST", doltCfg.Host)
176+
}
171177
}
172178

173179
// openBrowser opens the specified URL in the default browser.

internal/cmd/up.go

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -311,13 +311,18 @@ func runUp(cmd *cobra.Command, args []string) error {
311311
// was skipped, polling the port would just burn the full timeout. (review finding #1)
312312
if !doltSkipped && doltOK {
313313
waitForDoltReady(townRoot)
314-
// Propagate Dolt port to process env so all subsequently spawned
314+
// Propagate Dolt connection info to process env so all subsequently spawned
315315
// agents (witnesses, refineries, crew) inherit it. Without this,
316316
// bd auto-starts rogue Dolt instances in agent tmux sessions. (GH#2412)
317+
// Host propagation prevents bd from falling back to 127.0.0.1 when the
318+
// Dolt server runs on a remote machine (e.g., mini2 over Tailscale).
317319
doltCfg := doltserver.DefaultConfig(townRoot)
318320
portStr := fmt.Sprintf("%d", doltCfg.Port)
319321
os.Setenv("GT_DOLT_PORT", portStr)
320322
os.Setenv("BEADS_DOLT_PORT", portStr)
323+
if doltCfg.Host != "" {
324+
os.Setenv("BEADS_DOLT_SERVER_HOST", doltCfg.Host)
325+
}
321326
}
322327

323328
// 5 & 6. Witnesses and Refineries (using prefetched rigs)

internal/config/env.go

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -309,6 +309,16 @@ func AgentEnv(cfg AgentEnvConfig) map[string]string {
309309
env["BEADS_DOLT_AUTO_START"] = "0"
310310
}
311311

312+
// Propagate Dolt server host so bd doesn't fall back to 127.0.0.1 when
313+
// the server runs on a remote machine (e.g., mini2 over Tailscale).
314+
if _, ok := env["BEADS_DOLT_SERVER_HOST"]; !ok {
315+
if v := os.Getenv("BEADS_DOLT_SERVER_HOST"); v != "" {
316+
env["BEADS_DOLT_SERVER_HOST"] = v
317+
} else if v := os.Getenv("GT_DOLT_HOST"); v != "" {
318+
env["BEADS_DOLT_SERVER_HOST"] = v
319+
}
320+
}
321+
312322
// Pass through cloud API credentials and provider configuration from the parent shell.
313323
// Only variables explicitly listed here are forwarded; all others are blocked for isolation.
314324
for _, key := range []string{

internal/daemon/daemon.go

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -175,12 +175,16 @@ func New(config *Config) (*Daemon, error) {
175175
doltServer = NewDoltServerManager(config.TownRoot, patrolConfig.Patrols.DoltServer, logger.Printf)
176176
if doltServer.IsEnabled() {
177177
logger.Printf("Dolt server management enabled (port %d)", patrolConfig.Patrols.DoltServer.Port)
178-
// Propagate Dolt port to process env so AgentEnv() passes it to
178+
// Propagate Dolt connection info to process env so AgentEnv() passes it to
179179
// all spawned agent sessions. Without this, bd in agent sessions
180-
// auto-starts rogue Dolt instances. (GH#2412)
180+
// auto-starts rogue Dolt instances or connects to localhost. (GH#2412)
181181
portStr := strconv.Itoa(patrolConfig.Patrols.DoltServer.Port)
182182
os.Setenv("GT_DOLT_PORT", portStr)
183183
os.Setenv("BEADS_DOLT_PORT", portStr)
184+
if patrolConfig.Patrols.DoltServer.Host != "" {
185+
os.Setenv("GT_DOLT_HOST", patrolConfig.Patrols.DoltServer.Host)
186+
os.Setenv("BEADS_DOLT_SERVER_HOST", patrolConfig.Patrols.DoltServer.Host)
187+
}
184188
}
185189
}
186190

@@ -197,6 +201,16 @@ func New(config *Config) (*Daemon, error) {
197201
}
198202
}
199203

204+
// Propagate Dolt host to process env so bd doesn't fall back to 127.0.0.1
205+
// when the server runs on a remote machine (e.g., mini2 over Tailscale).
206+
if os.Getenv("BEADS_DOLT_SERVER_HOST") == "" {
207+
doltCfg := doltserver.DefaultConfig(config.TownRoot)
208+
if doltCfg.Host != "" {
209+
os.Setenv("BEADS_DOLT_SERVER_HOST", doltCfg.Host)
210+
logger.Printf("Set BEADS_DOLT_SERVER_HOST=%s from Dolt config", doltCfg.Host)
211+
}
212+
}
213+
200214
// PATCH-006: Resolve binary paths at startup.
201215
gtPath, err := exec.LookPath("gt")
202216
if err != nil {

internal/rig/manager.go

Lines changed: 14 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -912,22 +912,31 @@ func (m *Manager) InitBeads(rigPath, prefix, rigName string) error {
912912
}
913913
filteredEnv = append(filteredEnv, "BEADS_DIR="+beadsDir)
914914

915-
// Ensure BEADS_DOLT_PORT is set when GT_DOLT_PORT is present, so that
916-
// bd subprocesses connect to the correct Dolt server (especially in tests
917-
// where an ephemeral server runs on a non-default port).
918-
var gtDoltPort string
919-
hasBDP := false
915+
// Ensure BEADS_DOLT_PORT and BEADS_DOLT_SERVER_HOST are set when their GT_
916+
// counterparts are present, so that bd subprocesses connect to the correct
917+
// Dolt server (especially in tests or when the server is remote).
918+
var gtDoltPort, gtDoltHost string
919+
hasBDP, hasBDH := false, false
920920
for _, e := range filteredEnv {
921921
if strings.HasPrefix(e, "GT_DOLT_PORT=") {
922922
gtDoltPort = strings.TrimPrefix(e, "GT_DOLT_PORT=")
923923
}
924+
if strings.HasPrefix(e, "GT_DOLT_HOST=") {
925+
gtDoltHost = strings.TrimPrefix(e, "GT_DOLT_HOST=")
926+
}
924927
if strings.HasPrefix(e, "BEADS_DOLT_PORT=") {
925928
hasBDP = true
926929
}
930+
if strings.HasPrefix(e, "BEADS_DOLT_SERVER_HOST=") {
931+
hasBDH = true
932+
}
927933
}
928934
if gtDoltPort != "" && !hasBDP {
929935
filteredEnv = append(filteredEnv, "BEADS_DOLT_PORT="+gtDoltPort)
930936
}
937+
if gtDoltHost != "" && !hasBDH {
938+
filteredEnv = append(filteredEnv, "BEADS_DOLT_SERVER_HOST="+gtDoltHost)
939+
}
931940

932941
// Run bd init if available (Dolt is the only backend since bd v0.51.0).
933942
// --server tells bd to set dolt_mode=server in metadata.json so bd

internal/testutil/cmd.go

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,17 +7,19 @@ import (
77
)
88

99
// CleanGTEnv returns os.Environ() with GT_* and BD_* variables removed, except
10-
// GT_DOLT_PORT which is preserved so subprocesses connect to the ephemeral test
11-
// Dolt server instead of production on port 3307. BEADS_DOLT_PORT (prefix
12-
// BEADS_, not BD_) passes through implicitly since only BD_* is stripped.
10+
// GT_DOLT_PORT and GT_DOLT_HOST which are preserved so subprocesses connect to
11+
// the correct Dolt server. BEADS_DOLT_PORT and BEADS_DOLT_SERVER_HOST (prefix
12+
// BEADS_, not BD_) pass through implicitly since only BD_* is stripped.
1313
//
1414
// Use this when setting cmd.Env on bd/gt subprocess calls in tests.
1515
// If you do NOT set cmd.Env, the process env (including GT_DOLT_PORT) is
1616
// inherited automatically — no need for this function in that case.
1717
func CleanGTEnv(extraEnv ...string) []string {
1818
var clean []string
1919
for _, e := range os.Environ() {
20-
if strings.HasPrefix(e, "GT_") && !strings.HasPrefix(e, "GT_DOLT_PORT=") {
20+
if strings.HasPrefix(e, "GT_") &&
21+
!strings.HasPrefix(e, "GT_DOLT_PORT=") &&
22+
!strings.HasPrefix(e, "GT_DOLT_HOST=") {
2123
continue
2224
}
2325
if strings.HasPrefix(e, "BD_") {

0 commit comments

Comments
 (0)