diff --git a/pkg/environment/default.go b/pkg/environment/default.go index d9b7d4826..6a87aaae0 100644 --- a/pkg/environment/default.go +++ b/pkg/environment/default.go @@ -13,5 +13,10 @@ func NewDefaultProvider() Provider { p = append(p, passProvider) } + keychainProvider, err := NewKeychainProvider() + if err == nil { + p = append(p, keychainProvider) + } + return NewMultiProvider(p...) } diff --git a/pkg/environment/keychain.go b/pkg/environment/keychain.go new file mode 100644 index 000000000..7e3866343 --- /dev/null +++ b/pkg/environment/keychain.go @@ -0,0 +1,52 @@ +package environment + +import ( + "bytes" + "context" + "errors" + "fmt" + "log/slog" + "os/exec" + "strings" +) + +// KeychainProvider is a provider that retrieves secrets using the macOS keychain +// via the `security` command-line tool. +type KeychainProvider struct{} + +type ErrKeychainNotAvailable struct{} + +func (ErrKeychainNotAvailable) Error() string { + return "security command is not available (macOS keychain access)" +} + +// NewKeychainProvider creates a new KeychainProvider instance. +// It verifies that the `security` command is available on the system. +func NewKeychainProvider() (*KeychainProvider, error) { + path, err := exec.LookPath("security") + if err != nil && !errors.Is(err, exec.ErrNotFound) { + slog.Warn("failed to lookup `security` binary", "error", err) + } + if path == "" { + return nil, ErrKeychainNotAvailable{} + } + return &KeychainProvider{}, nil +} + +// Get retrieves the value of a secret by its service name from the macOS keychain. +// It uses the `security find-generic-password -w -s ` command to fetch the password. +func (p *KeychainProvider) Get(ctx context.Context, name string) (string, error) { + cmd := exec.CommandContext(ctx, "security", "find-generic-password", "-w", "-s", name) + + var out bytes.Buffer + var stderr bytes.Buffer + cmd.Stdout = &out + cmd.Stderr = &stderr + + err := cmd.Run() + if err != nil { + return "", fmt.Errorf("failed to retrieve secret from keychain: %w, stderr: %v", err, stderr.String()) + } + + return strings.TrimSpace(out.String()), nil +} diff --git a/pkg/teamloader/teamloader.go b/pkg/teamloader/teamloader.go index abf4cff28..4007f3ba2 100644 --- a/pkg/teamloader/teamloader.go +++ b/pkg/teamloader/teamloader.go @@ -173,6 +173,12 @@ func getModelsForAgent(ctx context.Context, cfg *latest.Config, a *latest.AgentC providers = append(providers, passProvider) } + // Append keychain provider if available + keychainProvider, err := environment.NewKeychainProvider() + if err == nil { + providers = append(providers, keychainProvider) + } + env := environment.NewMultiProvider(providers...) model, err := provider.New(ctx, &modelCfg, env, opts...)