config: default GarminTokenStoreRoot instead of leaving it empty

Per-user profile isolation namespaces each user's mcp-garmin session
cache under {GarminTokenStoreRoot}/{userID} (api.Server.garminFor), but
Load() left GarminTokenStoreRoot ("" when GARMIN_TOKENSTORE is unset)
with no required-var check and no safe default. In that state
garminFor's `if cfg.TokenStorePath != ""` guard skips the per-user
join entirely, so every user's subprocess would fall back to the same
default token cache -- a cross-user Garmin-session collision risk in
any deployment that forgets to set GARMIN_TOKENSTORE.

Default it to a "garmin-tokenstores" directory next to DBPath when
unset, so every deployment gets per-user isolation automatically,
while GARMIN_TOKENSTORE can still override it explicitly. Log the
derived default. Update the field's doc comment (no longer "if set")
and add config_test.go cases covering the default derivation and the
explicit-override precedence.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
2026-07-25 18:55:53 +02:00
parent 7f5d2da6a2
commit 00605f950b
2 changed files with 44 additions and 6 deletions

View File

@@ -7,7 +7,9 @@ package config
import ( import (
"fmt" "fmt"
"log"
"os" "os"
"path/filepath"
"strconv" "strconv"
"strings" "strings"
"time" "time"
@@ -24,12 +26,13 @@ type Config struct {
GarminPythonPath string GarminPythonPath string
// GarminServerPath is mcp-garmin's server.py. // GarminServerPath is mcp-garmin's server.py.
GarminServerPath string GarminServerPath string
// GarminTokenStoreRoot, if set, is the root directory under which each // GarminTokenStoreRoot is the root directory under which each user's
// user's mcp-garmin session cache lives (one subdirectory per user id, // mcp-garmin session cache lives (one subdirectory per user id, e.g.
// e.g. "<root>/3"), overriding mcp-garmin's default ~/.garth. Read from // "<root>/3"). Read from the GARMIN_TOKENSTORE env var; if unset, it
// the same GARMIN_TOKENSTORE env var as before Task 1's per-user // defaults to a "garmin-tokenstores" directory next to DBPath so every
// scoping -- only its meaning changed (a root directory rather than a // deployment gets per-user isolation automatically -- multi-tenant
// single path). // operation always relies on this being a real, distinct-per-user path
// (see api.Server.garminFor), so it can never be silently left empty.
GarminTokenStoreRoot string GarminTokenStoreRoot string
MinConfidence float64 MinConfidence float64
@@ -78,6 +81,11 @@ func Load() (Config, error) {
LegacyOwnerOIDCSub: os.Getenv("GENIUSRUN_LEGACY_OWNER_OIDC_SUB"), LegacyOwnerOIDCSub: os.Getenv("GENIUSRUN_LEGACY_OWNER_OIDC_SUB"),
} }
if cfg.GarminTokenStoreRoot == "" {
cfg.GarminTokenStoreRoot = filepath.Join(filepath.Dir(cfg.DBPath), "garmin-tokenstores")
log.Printf("GARMIN_TOKENSTORE not set, defaulting to %q", cfg.GarminTokenStoreRoot)
}
if cfg.GarminPythonPath == "" { if cfg.GarminPythonPath == "" {
return cfg, fmt.Errorf("MCP_GARMIN_PYTHON is required (path to mcp-garmin's venv python)") return cfg, fmt.Errorf("MCP_GARMIN_PYTHON is required (path to mcp-garmin's venv python)")
} }

View File

@@ -1,6 +1,7 @@
package config package config
import ( import (
"path/filepath"
"testing" "testing"
"time" "time"
) )
@@ -80,6 +81,35 @@ func TestLoad_SessionSecretTooShort(t *testing.T) {
} }
} }
func TestLoad_GarminTokenStoreRootDefaultsNextToDBPath(t *testing.T) {
setRequiredEnv(t)
t.Setenv("GARMIN_TOKENSTORE", "")
t.Setenv("GENIUSRUN_DB_PATH", "/var/lib/geniusrun/geniusrun.db")
cfg, err := Load()
if err != nil {
t.Fatalf("Load: %v", err)
}
want := filepath.Join("/var/lib/geniusrun", "garmin-tokenstores")
if cfg.GarminTokenStoreRoot != want {
t.Errorf("GarminTokenStoreRoot = %q, want %q", cfg.GarminTokenStoreRoot, want)
}
}
func TestLoad_GarminTokenStoreRootExplicitOverridesDefault(t *testing.T) {
setRequiredEnv(t)
t.Setenv("GARMIN_TOKENSTORE", "/custom/tokenstores")
t.Setenv("GENIUSRUN_DB_PATH", "/var/lib/geniusrun/geniusrun.db")
cfg, err := Load()
if err != nil {
t.Fatalf("Load: %v", err)
}
if cfg.GarminTokenStoreRoot != "/custom/tokenstores" {
t.Errorf("GarminTokenStoreRoot = %q, want explicit override to take precedence", cfg.GarminTokenStoreRoot)
}
}
func TestLoad_CustomRoleAndDuration(t *testing.T) { func TestLoad_CustomRoleAndDuration(t *testing.T) {
setRequiredEnv(t) setRequiredEnv(t)
t.Setenv("GENIUSRUN_OIDC_REQUIRED_ROLE", "admin") t.Setenv("GENIUSRUN_OIDC_REQUIRED_ROLE", "admin")