Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 21 additions & 18 deletions internal/config/keyring.go
Original file line number Diff line number Diff line change
Expand Up @@ -68,17 +68,16 @@ func (c Credential) RequestOptions() []option.RequestOption {
return opts
}

// ResolveEnvVar checks the environment and .env files for a variable.
// ResolveEnvVar checks the environment and known .env files for a variable.
// Returns the value and true if found, or empty string and false.
func ResolveEnvVar(name string) (string, bool) {
if v := os.Getenv(name); v != "" {
return v, true
}
if v := loadKeyFromEnvFile(loadExeEnvFilePath(), name); v != "" {
return v, true
}
if v := loadKeyFromEnvFile(".env", name); v != "" {
return v, true
for _, path := range envFileSearchPaths() {
if v := loadKeyFromEnvFile(path, name); v != "" {
return v, true
}
}
return "", false
}
Expand Down Expand Up @@ -113,14 +112,9 @@ func resolveKey(envVar, keyringUser string) (string, KeySource) {
}
}

// 3. .env next to executable
// 3. Known .env files (executable-adjacent, ~/.vix/.env, then CWD).
if envVar != "" {
if key := loadKeyFromEnvFile(loadExeEnvFilePath(), envVar); key != "" {
return key, KeySourceEnvFile
}

// 4. .env in CWD
if key := loadKeyFromEnvFile(".env", envVar); key != "" {
if key, ok := ResolveEnvVar(envVar); ok {
return key, KeySourceEnvFile
}
}
Expand Down Expand Up @@ -233,7 +227,7 @@ func reorderAuthMethods(methods []AuthMethod, pref string) []AuthMethod {
// or env-provided base URL overrides the method's static BaseURL.
func buildCredential(value string, src KeySource, m AuthMethod) Credential {
baseURL := m.BaseURL
if m.RequiresBaseURL {
if m.RequiresBaseURL || m.BaseURLEnv != "" {
if u := resolveMethodBaseURL(m); u != "" {
baseURL = u
}
Expand All @@ -246,15 +240,15 @@ func buildCredential(value string, src KeySource, m AuthMethod) Credential {
}

// resolveMethodBaseURL returns the user-supplied endpoint for a method: the
// BaseURLEnv environment variable first, then the keychain entry stored next to
// the key. Returns "" when neither is set.
// BaseURLEnv value first, then (for RequiresBaseURL methods) the keychain entry
// stored next to the key. Returns "" when neither is set.
func resolveMethodBaseURL(m AuthMethod) string {
if m.BaseURLEnv != "" {
if v := os.Getenv(m.BaseURLEnv); v != "" {
if v, ok := ResolveEnvVar(m.BaseURLEnv); ok {
return v
}
}
if m.Keyring != "" {
if m.RequiresBaseURL && m.Keyring != "" {
if v, err := defaultStore().Get(methodBaseURLKeyringUser(m.Keyring)); err == nil && v != "" {
return v
}
Expand Down Expand Up @@ -583,6 +577,15 @@ func loadExeEnvFilePath() string {
return filepath.Join(filepath.Dir(exe), "..", "..", ".env")
}

func envFileSearchPaths() []string {
paths := []string{loadExeEnvFilePath()}
if home := HomeVixDir(); home != "" {
paths = append(paths, filepath.Join(home, ".env"))
}
paths = append(paths, ".env")
return paths
}

// loadKeyFromEnvFile reads a .env file and extracts the value of the given variable name.
func loadKeyFromEnvFile(path, varName string) string {
if path == "" {
Expand Down
39 changes: 39 additions & 0 deletions internal/config/keyring_test.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
package config

import (
"os"
"path/filepath"
"testing"

"github.com/zalando/go-keyring"
Expand Down Expand Up @@ -184,6 +186,43 @@ func TestListStoredProviderKeys(t *testing.T) {
}
}

func TestResolveEnvVarSearchesHomeVixDotEnv(t *testing.T) {
t.Setenv("HOME", t.TempDir())
t.Setenv("ANTHROPIC_API_KEY", "")
if err := os.MkdirAll(HomeVixDir(), 0o755); err != nil {
t.Fatalf("mkdir home .vix: %v", err)
}
if err := os.WriteFile(filepath.Join(HomeVixDir(), ".env"), []byte("ANTHROPIC_API_KEY=home-file-key\n"), 0o600); err != nil {
t.Fatalf("write home .env: %v", err)
}

got, ok := ResolveEnvVar("ANTHROPIC_API_KEY")
if !ok || got != "home-file-key" {
t.Fatalf("ResolveEnvVar = %q, %v; want home-file-key, true", got, ok)
}
}

func TestResolveProviderCredentialBaseURLFromDotEnv(t *testing.T) {
t.Setenv("HOME", t.TempDir())
t.Setenv("ANTHROPIC_API_KEY", "")
t.Setenv("ANTHROPIC_BASE_URL", "")
DeleteProviderKey("anthropic")

cwd := t.TempDir()
t.Chdir(cwd)
if err := os.WriteFile(filepath.Join(cwd, ".env"), []byte("ANTHROPIC_API_KEY=dotenv-key\nANTHROPIC_BASE_URL=https://mock.example/v1\n"), 0o600); err != nil {
t.Fatalf("write cwd .env: %v", err)
}

cred := ResolveProviderCredential("anthropic")
if cred.Value != "dotenv-key" || cred.Source != KeySourceEnvFile {
t.Fatalf("credential = %+v; want key from .env", cred)
}
if cred.BaseURL != "https://mock.example/v1" {
t.Fatalf("baseURL = %q, want .env override", cred.BaseURL)
}
}

// TestMiMoTokenPlanCredential covers the Token Plan credential method: a key and
// a user-supplied base URL are stored together, resolution returns both, and the
// BaseURLEnv override wins over the stored endpoint.
Expand Down
8 changes: 7 additions & 1 deletion internal/config/providers.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
package config

import (
"strings"

"github.com/get-vix/vix/internal/auth"
"github.com/get-vix/vix/internal/providers"
)
Expand Down Expand Up @@ -111,6 +113,10 @@ func credKindToAuthKind(kind string) AuthKind {

// authMethodFromSpec projects a registry credential method into an AuthMethod.
func authMethodFromSpec(m providers.CredentialMethod) AuthMethod {
baseURLEnv := m.BaseURLEnv
if baseURLEnv == "" && strings.HasSuffix(m.EnvVar, "_API_KEY") {
baseURLEnv = strings.TrimSuffix(m.EnvVar, "_API_KEY") + "_BASE_URL"
}
am := AuthMethod{
Kind: credKindToAuthKind(m.Kind),
EnvVar: m.EnvVar,
Expand All @@ -119,7 +125,7 @@ func authMethodFromSpec(m providers.CredentialMethod) AuthMethod {
BaseURL: m.BaseURL,
Label: m.Label,
RequiresBaseURL: m.RequiresBaseURL,
BaseURLEnv: m.BaseURLEnv,
BaseURLEnv: baseURLEnv,
}
if m.HeaderStyle == providers.AuthSchemeBearer {
am.HeaderStyle = BearerHeader
Expand Down