Skip to content
Merged
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
45 changes: 45 additions & 0 deletions cmd/cmd.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ import (
"path"
"strings"

"github.com/appleboy/CodeGPT/util"

"github.com/appleboy/com/file"
"github.com/spf13/cobra"
"github.com/spf13/viper"
Expand All @@ -31,6 +33,46 @@ const (
DRONE = "drone"
)

// sensitiveConfigKeys lists the config keys that should be stored in the
// secure credential store rather than in the plaintext YAML config file.
var sensitiveConfigKeys = []string{"openai.api_key", "gemini.api_key"}

// migrateCredentialsToStore moves any plaintext API keys found in the YAML
// config into the secure credential store and clears them from the config file.
func migrateCredentialsToStore() {
for _, key := range sensitiveConfigKeys {
// Only migrate values that actually exist in the config file.
// This prevents env vars (e.g. OPENAI_API_KEY) from being silently
// persisted into the credential store.
if !viper.InConfig(key) {
continue
}
val := viper.GetString(key)
if val == "" {
continue
}
// Check if already in credstore; skip if already migrated.
existing, err := util.GetCredential(key)
if err != nil || existing != "" {
continue
}
if err := util.SetCredential(key, val); err != nil {
fmt.Fprintf(os.Stderr, "warning: could not migrate %s to secure store: %v\n", key, err)
continue
}
// Remove from YAML.
viper.Set(key, "")
if err := viper.WriteConfig(); err != nil {
fmt.Fprintf(
os.Stderr,
"warning: could not update config after migrating %s: %v\n",
key,
err,
)
}
}
}

func init() {
cobra.OnInitialize(initConfig)

Expand Down Expand Up @@ -124,6 +166,9 @@ func initConfig() {
}
}

// Auto-migrate plaintext API keys to secure store.
migrateCredentialsToStore()

switch {
case promptFolder != "":
// If a prompt folder is specified by the promptFolder variable,
Expand Down
24 changes: 21 additions & 3 deletions cmd/config_list.go
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
package cmd

import (
"slices"
"sort"

"github.com/appleboy/CodeGPT/util"

"github.com/fatih/color"
"github.com/rodaine/table"
"github.com/spf13/cobra"
Expand Down Expand Up @@ -73,9 +76,24 @@ var configListCmd = &cobra.Command{

// Add the key and value to the table
for _, v := range keys {
// Hide the api key
if v == "openai.api_key" || v == "gemini.api_key" {
tbl.AddRow(v, "****************")
if slices.Contains(sensitiveConfigKeys, v) {
cred, err := util.GetCredential(v)
if err != nil {
tbl.AddRow(v, "(error reading secure store)")
continue
}
switch {
case cred != "":
if util.CredStoreIsKeyring() {
tbl.AddRow(v, "(stored in keyring)")
} else {
tbl.AddRow(v, "(stored in secure file)")
}
case viper.InConfig(v):
tbl.AddRow(v, "**************** (YAML — run config set to migrate)")
default:
tbl.AddRow(v, "(not set)")
}
continue
}
tbl.AddRow(v, viper.Get(v))
Expand Down
19 changes: 19 additions & 0 deletions cmd/config_set.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,11 @@ package cmd

import (
"errors"
"fmt"
"strings"

"github.com/appleboy/CodeGPT/util"

"github.com/fatih/color"
"github.com/spf13/cobra"
"github.com/spf13/viper"
Expand Down Expand Up @@ -103,6 +106,22 @@ var configSetCmd = &cobra.Command{
)
}

// Sensitive keys go to secure store, not YAML.
for _, sensitiveKey := range sensitiveConfigKeys {
if args[0] == sensitiveKey {
if err := util.SetCredential(args[0], args[1]); err != nil {
return fmt.Errorf("failed to store credential in secure store: %w", err)
}
// Ensure the key is cleared from YAML.
viper.Set(args[0], "")
if err := viper.WriteConfig(); err != nil {
return err
}
color.Green("you can see the config file: %s", viper.ConfigFileUsed())
return nil
}
}

// Set config value in viper
if args[0] == "git.exclude_list" {
viper.Set(args[0], strings.Split(args[1], ","))
Expand Down
42 changes: 36 additions & 6 deletions cmd/provider.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,20 @@ import (
"github.com/spf13/viper"
)

// getAPIKey retrieves an API key from the secure credential store first,
// then falls back to viper (env vars or legacy YAML).
func getAPIKey(viperKey string) (string, error) {
val, err := util.GetCredential(viperKey)
if err != nil {
return "", err
}
if val != "" {
return val, nil
}
// Fallback: env var or legacy YAML (not yet migrated).
return viper.GetString(viperKey), nil
}

func NewOpenAI(ctx context.Context) (*openai.Client, error) {
var apiKey string

Expand All @@ -35,7 +49,11 @@ func NewOpenAI(ctx context.Context) (*openai.Client, error) {
}
apiKey = key
} else {
apiKey = viper.GetString("openai.api_key")
key, err := getAPIKey("openai.api_key")
if err != nil {
return nil, err
}
apiKey = key
}

return openai.New(
Expand Down Expand Up @@ -81,7 +99,11 @@ func NewGemini(ctx context.Context) (*gemini.Client, error) {
apiKey = key
} else {
// Fallback to static config: gemini.api_key -> openai.api_key
apiKey = viper.GetString("gemini.api_key")
key, err := getAPIKey("gemini.api_key")
if err != nil {
return nil, err
}
apiKey = key
if apiKey == "" {
// Try openai.api_key_helper as fallback
if helper := viper.GetString("openai.api_key_helper"); helper != "" {
Expand All @@ -95,13 +117,17 @@ func NewGemini(ctx context.Context) (*gemini.Client, error) {
// Not set, use default
refreshInterval = util.DefaultRefreshInterval
}
key, err := util.GetAPIKeyFromHelperWithCache(ctx, helper, refreshInterval)
helperKey, err := util.GetAPIKeyFromHelperWithCache(ctx, helper, refreshInterval)
if err != nil {
return nil, err
}
apiKey = key
apiKey = helperKey
} else {
apiKey = viper.GetString("openai.api_key")
openaiKey, err := getAPIKey("openai.api_key")
if err != nil {
return nil, err
}
apiKey = openaiKey
}
}
}
Expand Down Expand Up @@ -150,7 +176,11 @@ func NewAnthropic(ctx context.Context) (*anthropic.Client, error) {
}
apiKey = key
} else {
apiKey = viper.GetString("openai.api_key")
key, err := getAPIKey("openai.api_key")
if err != nil {
return nil, err
}
apiKey = key
}

return anthropic.New(
Expand Down
7 changes: 6 additions & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ require (
github.com/charmbracelet/bubbletea v1.3.10
github.com/erikgeiser/promptkit v0.9.0
github.com/fatih/color v1.18.0
github.com/go-authgate/sdk-go v0.2.0
github.com/joho/godotenv v1.5.1
github.com/liushuangls/go-anthropic/v2 v2.17.1
github.com/rodaine/table v1.3.0
Expand All @@ -17,11 +18,12 @@ require (
github.com/spf13/viper v1.21.0
github.com/yassinebenaid/godump v0.11.1
golang.org/x/net v0.51.0
golang.org/x/sys v0.41.0
golang.org/x/sys v0.42.0
google.golang.org/genai v1.49.0
)

require (
al.essio.dev/pkg/shellescape v1.6.0 // indirect
cloud.google.com/go v0.123.0 // indirect
cloud.google.com/go/auth v0.18.2 // indirect
cloud.google.com/go/compute/metadata v0.9.0 // indirect
Expand All @@ -35,12 +37,14 @@ require (
github.com/charmbracelet/x/term v0.2.2 // indirect
github.com/clipperhouse/displaywidth v0.11.0 // indirect
github.com/clipperhouse/uax29/v2 v2.7.0 // indirect
github.com/danieljoos/wincred v1.2.3 // indirect
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f // indirect
github.com/felixge/httpsnoop v1.0.4 // indirect
github.com/fsnotify/fsnotify v1.9.0 // indirect
github.com/go-logr/logr v1.4.3 // indirect
github.com/go-logr/stdr v1.2.2 // indirect
github.com/go-viper/mapstructure/v2 v2.5.0 // indirect
github.com/godbus/dbus/v5 v5.2.2 // indirect
github.com/google/go-cmp v0.7.0 // indirect
github.com/google/s2a-go v0.1.9 // indirect
github.com/googleapis/enterprise-certificate-proxy v0.3.14 // indirect
Expand All @@ -64,6 +68,7 @@ require (
github.com/spf13/pflag v1.0.10 // indirect
github.com/subosito/gotenv v1.6.0 // indirect
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect
github.com/zalando/go-keyring v0.2.6 // indirect
go.opentelemetry.io/auto/sdk v1.2.1 // indirect
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.67.0 // indirect
go.opentelemetry.io/otel v1.42.0 // indirect
Expand Down
17 changes: 15 additions & 2 deletions go.sum
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
al.essio.dev/pkg/shellescape v1.6.0 h1:NxFcEqzFSEVCGN2yq7Huv/9hyCEGVa/TncnOOBBeXHA=
al.essio.dev/pkg/shellescape v1.6.0/go.mod h1:6sIqp7X2P6mThCQ7twERpZTuigpr6KbZWtls1U8I890=
cloud.google.com/go v0.123.0 h1:2NAUJwPR47q+E35uaJeYoNhuNEM9kM8SjgRgdeOJUSE=
cloud.google.com/go v0.123.0/go.mod h1:xBoMV08QcqUGuPW65Qfm1o9Y4zKZBpGS+7bImXLTAZU=
cloud.google.com/go/auth v0.18.2 h1:+Nbt5Ev0xEqxlNjd6c+yYUeosQ5TtEUaNcN/3FozlaM=
Expand Down Expand Up @@ -37,6 +39,8 @@ github.com/clipperhouse/displaywidth v0.11.0/go.mod h1:bkrFNkf81G8HyVqmKGxsPufD3
github.com/clipperhouse/uax29/v2 v2.7.0 h1:+gs4oBZ2gPfVrKPthwbMzWZDaAFPGYK72F0NJv2v7Vk=
github.com/clipperhouse/uax29/v2 v2.7.0/go.mod h1:EFJ2TJMRUaplDxHKj1qAEhCtQPW2tJSwu5BF98AuoVM=
github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
github.com/danieljoos/wincred v1.2.3 h1:v7dZC2x32Ut3nEfRH+vhoZGvN72+dQ/snVXo/vMFLdQ=
github.com/danieljoos/wincred v1.2.3/go.mod h1:6qqX0WNrS4RzPZ1tnroDzq9kY3fu1KwE7MRLQK4X0bs=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
Expand All @@ -52,20 +56,26 @@ github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHk
github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=
github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k=
github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0=
github.com/go-authgate/sdk-go v0.2.0 h1:w22f+sAg/YMqnLOcS/4SAuMZXTbPurzkSQBsjb1hcbw=
github.com/go-authgate/sdk-go v0.2.0/go.mod h1:RGqvrFdrPnOumndoQQV8qzu8zP1KFUZPdhX0IkWduho=
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=
github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
github.com/go-viper/mapstructure/v2 v2.5.0 h1:vM5IJoUAy3d7zRSVtIwQgBj7BiWtMPfmPEgAXnvj1Ro=
github.com/go-viper/mapstructure/v2 v2.5.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM=
github.com/godbus/dbus/v5 v5.2.2 h1:TUR3TgtSVDmjiXOgAAyaZbYmIeP3DPkld3jgKGV8mXQ=
github.com/godbus/dbus/v5 v5.2.2/go.mod h1:3AAv2+hPq5rdnr5txxxRwiGjPXamgoIHgz9FPBfOp3c=
github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
github.com/google/s2a-go v0.1.9 h1:LGD7gtMgezd8a/Xak7mEWL0PjoTQFvpRudN895yqKW0=
github.com/google/s2a-go v0.1.9/go.mod h1:YA0Ei2ZQL3acow2O62kdp9UlnvMmU7kA6Eutn0dXayM=
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 h1:El6M4kTTCOh6aBiKaUGG7oYTSPP8MxqL4YI3kZKwcP4=
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510/go.mod h1:pupxD2MaaD3pAXIBCelhxNneeOaAeabZDe5s4K6zSpQ=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/googleapis/enterprise-certificate-proxy v0.3.14 h1:yh8ncqsbUY4shRD5dA6RlzjJaT4hi3kII+zYw8wmLb8=
Expand Down Expand Up @@ -135,6 +145,7 @@ github.com/spf13/viper v1.21.0/go.mod h1:P0lhsswPGWD/1lZJ9ny3fYnVqxiegrlNrEmgLjb
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY=
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
Expand All @@ -148,6 +159,8 @@ github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavM
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM=
github.com/yassinebenaid/godump v0.11.1 h1:SPujx/XaYqGDfmNh7JI3dOyCUVrG0bG2duhO3Eh2EhI=
github.com/yassinebenaid/godump v0.11.1/go.mod h1:dc/0w8wmg6kVIvNGAzbKH1Oa54dXQx8SNKh4dPRyW44=
github.com/zalando/go-keyring v0.2.6 h1:r7Yc3+H+Ux0+M72zacZoItR3UDxeWfKTcabvkI8ua9s=
github.com/zalando/go-keyring v0.2.6/go.mod h1:2TCrxYrbUNYfNS/Kgy/LSrkSQzZ5UPVH85RwfczwvcI=
go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64=
go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.67.0 h1:OyrsyzuttWTSur2qN/Lm0m2a8yqyIjUVBZcxFPuXq2o=
Expand All @@ -174,8 +187,8 @@ golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4=
golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.41.0 h1:Ivj+2Cp/ylzLiEU89QhWblYnOE9zerudt9Ftecq2C6k=
golang.org/x/sys v0.41.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/sys v0.42.0 h1:omrd2nAlyT5ESRdCLYdm3+fMfNFE/+Rf4bDIQImRJeo=
golang.org/x/sys v0.42.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
golang.org/x/text v0.34.0 h1:oL/Qq0Kdaqxa1KbNeMKwQq0reLCCaFtqu2eNuSeNHbk=
golang.org/x/text v0.34.0/go.mod h1:homfLqTYRFyVYemLBFl5GgL/DWEiH5wcsQ5gSh1yziA=
gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk=
Expand Down
57 changes: 57 additions & 0 deletions util/credstore.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
package util

import (
"os"
"path/filepath"

"github.com/go-authgate/sdk-go/credstore"
)

const credServiceName = "codegpt"

// credStore is the singleton SecureStore[string] instance.
// Initialized once; uses OS keyring with file-based fallback.
var credStore *credstore.SecureStore[string]

func init() {
home, err := os.UserHomeDir()
var fallbackPath string
if err != nil || home == "" {
fallbackPath = filepath.Join(os.TempDir(), "codegpt", "credentials.json")
} else {
fallbackPath = filepath.Join(home, ".config", "codegpt", ".cache", "credentials.json")
}

// Ensure the directory for the fallback credential file exists.
dir := filepath.Dir(fallbackPath)
_ = os.MkdirAll(dir, 0o700)

keyring := credstore.NewStringKeyringStore(credServiceName)
file := credstore.NewStringFileStore(fallbackPath)
credStore = credstore.NewSecureStore(keyring, file)
}

// GetCredential retrieves a stored credential by key.
// Returns ("", nil) if not found.
func GetCredential(key string) (string, error) {
val, err := credStore.Load(key)
if err == credstore.ErrNotFound {
return "", nil
}
return val, err
}

// SetCredential stores a credential by key.
func SetCredential(key, value string) error {
return credStore.Save(key, value)
}

// DeleteCredential removes a credential by key.
func DeleteCredential(key string) error {
return credStore.Delete(key)
}

// CredStoreIsKeyring reports whether the active backend is the OS keyring.
func CredStoreIsKeyring() bool {
return credStore.UseKeyring()
}
Loading
Loading