lint: fix credential keygen lint, fix test keygen
This commit is contained in:
parent
82c17b4134
commit
71fbf6db9d
6 changed files with 43 additions and 17 deletions
|
|
@ -6,10 +6,11 @@ import (
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
|
||||||
|
"golang.org/x/term"
|
||||||
|
|
||||||
"github.com/sipeed/picoclaw/cmd/picoclaw/internal"
|
"github.com/sipeed/picoclaw/cmd/picoclaw/internal"
|
||||||
"github.com/sipeed/picoclaw/pkg/config"
|
"github.com/sipeed/picoclaw/pkg/config"
|
||||||
"github.com/sipeed/picoclaw/pkg/credential"
|
"github.com/sipeed/picoclaw/pkg/credential"
|
||||||
"golang.org/x/term"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func onboard() {
|
func onboard() {
|
||||||
|
|
@ -49,7 +50,7 @@ func onboard() {
|
||||||
// the current process and disappears when it exits.
|
// the current process and disappears when it exits.
|
||||||
os.Setenv(credential.PassphraseEnvVar, passphrase)
|
os.Setenv(credential.PassphraseEnvVar, passphrase)
|
||||||
|
|
||||||
if err := setupSSHKey(); err != nil {
|
if err = setupSSHKey(); err != nil {
|
||||||
fmt.Printf("Error generating SSH key: %v\n", err)
|
fmt.Printf("Error generating SSH key: %v\n", err)
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -834,7 +834,9 @@ func LoadConfig(path string) (*Config, error) {
|
||||||
if passphrase := credential.PassphraseProvider(); passphrase != "" {
|
if passphrase := credential.PassphraseProvider(); passphrase != "" {
|
||||||
for _, m := range cfg.ModelList {
|
for _, m := range cfg.ModelList {
|
||||||
if m.APIKey != "" && !strings.HasPrefix(m.APIKey, "enc://") && !strings.HasPrefix(m.APIKey, "file://") {
|
if m.APIKey != "" && !strings.HasPrefix(m.APIKey, "enc://") && !strings.HasPrefix(m.APIKey, "file://") {
|
||||||
fmt.Fprintf(os.Stderr, "picoclaw: warning: model %q has a plaintext api_key; call SaveConfig to encrypt it\n", m.ModelName)
|
fmt.Fprintf(os.Stderr,
|
||||||
|
"picoclaw: warning: model %q has a plaintext api_key; call SaveConfig to encrypt it\n",
|
||||||
|
m.ModelName)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -11,6 +11,18 @@ import (
|
||||||
"github.com/sipeed/picoclaw/pkg/credential"
|
"github.com/sipeed/picoclaw/pkg/credential"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// mustSetupSSHKey generates a temporary Ed25519 SSH key in t.TempDir() and sets
|
||||||
|
// PICOCLAW_SSH_KEY_PATH to its path for the duration of the test. This is required
|
||||||
|
// whenever a test exercises encryption/decryption via credential.Encrypt or SaveConfig.
|
||||||
|
func mustSetupSSHKey(t *testing.T) {
|
||||||
|
t.Helper()
|
||||||
|
keyPath := filepath.Join(t.TempDir(), "picoclaw_ed25519.key")
|
||||||
|
if err := credential.GenerateSSHKey(keyPath); err != nil {
|
||||||
|
t.Fatalf("mustSetupSSHKey: %v", err)
|
||||||
|
}
|
||||||
|
t.Setenv("PICOCLAW_SSH_KEY_PATH", keyPath)
|
||||||
|
}
|
||||||
|
|
||||||
func TestAgentModelConfig_UnmarshalString(t *testing.T) {
|
func TestAgentModelConfig_UnmarshalString(t *testing.T) {
|
||||||
var m AgentModelConfig
|
var m AgentModelConfig
|
||||||
if err := json.Unmarshal([]byte(`"gpt-4"`), &m); err != nil {
|
if err := json.Unmarshal([]byte(`"gpt-4"`), &m); err != nil {
|
||||||
|
|
@ -666,7 +678,7 @@ func TestSaveConfig_EncryptsPlaintextAPIKey(t *testing.T) {
|
||||||
cfgPath := filepath.Join(dir, "config.json")
|
cfgPath := filepath.Join(dir, "config.json")
|
||||||
|
|
||||||
t.Setenv("PICOCLAW_KEY_PASSPHRASE", "test-passphrase")
|
t.Setenv("PICOCLAW_KEY_PASSPHRASE", "test-passphrase")
|
||||||
t.Setenv("PICOCLAW_SSH_KEY_PATH", "")
|
mustSetupSSHKey(t)
|
||||||
|
|
||||||
cfg := DefaultConfig()
|
cfg := DefaultConfig()
|
||||||
cfg.ModelList = []ModelConfig{
|
cfg.ModelList = []ModelConfig{
|
||||||
|
|
@ -755,7 +767,7 @@ func TestSaveConfig_MixedKeys(t *testing.T) {
|
||||||
cfgPath := filepath.Join(dir, "config.json")
|
cfgPath := filepath.Join(dir, "config.json")
|
||||||
|
|
||||||
t.Setenv("PICOCLAW_KEY_PASSPHRASE", "test-passphrase")
|
t.Setenv("PICOCLAW_KEY_PASSPHRASE", "test-passphrase")
|
||||||
t.Setenv("PICOCLAW_SSH_KEY_PATH", "")
|
mustSetupSSHKey(t)
|
||||||
|
|
||||||
// Pre-encrypt one key so we have a genuine enc:// value to put in the config.
|
// Pre-encrypt one key so we have a genuine enc:// value to put in the config.
|
||||||
if err := SaveConfig(cfgPath, &Config{
|
if err := SaveConfig(cfgPath, &Config{
|
||||||
|
|
@ -844,7 +856,7 @@ func TestLoadConfig_MixedKeys_NoPassphrase(t *testing.T) {
|
||||||
|
|
||||||
// First encrypt a key so we have a real enc:// value.
|
// First encrypt a key so we have a real enc:// value.
|
||||||
t.Setenv("PICOCLAW_KEY_PASSPHRASE", "test-passphrase")
|
t.Setenv("PICOCLAW_KEY_PASSPHRASE", "test-passphrase")
|
||||||
t.Setenv("PICOCLAW_SSH_KEY_PATH", "")
|
mustSetupSSHKey(t)
|
||||||
if err := SaveConfig(cfgPath, &Config{
|
if err := SaveConfig(cfgPath, &Config{
|
||||||
ModelList: []ModelConfig{
|
ModelList: []ModelConfig{
|
||||||
{ModelName: "m", Model: "openai/gpt-4", APIKey: "sk-secret"},
|
{ModelName: "m", Model: "openai/gpt-4", APIKey: "sk-secret"},
|
||||||
|
|
@ -901,7 +913,7 @@ func TestSaveConfig_UsesPassphraseProvider(t *testing.T) {
|
||||||
|
|
||||||
// Ensure the env var is empty — passphrase must come from PassphraseProvider only.
|
// Ensure the env var is empty — passphrase must come from PassphraseProvider only.
|
||||||
t.Setenv("PICOCLAW_KEY_PASSPHRASE", "")
|
t.Setenv("PICOCLAW_KEY_PASSPHRASE", "")
|
||||||
t.Setenv("PICOCLAW_SSH_KEY_PATH", "")
|
mustSetupSSHKey(t)
|
||||||
|
|
||||||
// Replace PassphraseProvider with an in-memory function (simulating SecureStore).
|
// Replace PassphraseProvider with an in-memory function (simulating SecureStore).
|
||||||
const testPassphrase = "provider-passphrase"
|
const testPassphrase = "provider-passphrase"
|
||||||
|
|
@ -931,7 +943,7 @@ func TestLoadConfig_UsesPassphraseProvider(t *testing.T) {
|
||||||
|
|
||||||
// Ensure the env var is empty throughout.
|
// Ensure the env var is empty throughout.
|
||||||
t.Setenv("PICOCLAW_KEY_PASSPHRASE", "")
|
t.Setenv("PICOCLAW_KEY_PASSPHRASE", "")
|
||||||
t.Setenv("PICOCLAW_SSH_KEY_PATH", "")
|
mustSetupSSHKey(t)
|
||||||
|
|
||||||
const testPassphrase = "provider-passphrase"
|
const testPassphrase = "provider-passphrase"
|
||||||
const plainKey = "sk-secret"
|
const plainKey = "sk-secret"
|
||||||
|
|
@ -947,7 +959,7 @@ func TestLoadConfig_UsesPassphraseProvider(t *testing.T) {
|
||||||
{"model_name": "test", "model": "openai/gpt-4", "api_key": encrypted},
|
{"model_name": "test", "model": "openai/gpt-4", "api_key": encrypted},
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
if err := os.WriteFile(cfgPath, raw, 0o600); err != nil {
|
if err = os.WriteFile(cfgPath, raw, 0o600); err != nil {
|
||||||
t.Fatalf("setup: %v", err)
|
t.Fatalf("setup: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
// Package credential resolves API credential values for model_list entries.
|
// Package credential resolves API credential values for model_list entries.
|
||||||
//
|
//
|
||||||
// An API key is a form of authorization credential. This package centralises
|
// An API key is a form of authorization credential. This package centralizes
|
||||||
// how raw credential strings—plaintext or file references—are resolved into
|
// how raw credential strings—plaintext or file references—are resolved into
|
||||||
// their actual values, keeping that logic out of the config loader.
|
// their actual values, keeping that logic out of the config loader.
|
||||||
//
|
//
|
||||||
|
|
@ -88,8 +88,8 @@ type Resolver struct {
|
||||||
func NewResolver(configDir string) *Resolver {
|
func NewResolver(configDir string) *Resolver {
|
||||||
resolved := configDir
|
resolved := configDir
|
||||||
if configDir != "" {
|
if configDir != "" {
|
||||||
if real, err := filepath.EvalSymlinks(configDir); err == nil {
|
if linkedPath, err := filepath.EvalSymlinks(configDir); err == nil {
|
||||||
resolved = real
|
resolved = linkedPath
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return &Resolver{configDir: configDir, resolvedConfigDir: resolved}
|
return &Resolver{configDir: configDir, resolvedConfigDir: resolved}
|
||||||
|
|
@ -278,10 +278,15 @@ func allowedSSHKeyPath(path string) bool {
|
||||||
// sshKeyPath must be non-empty; returns an error otherwise.
|
// sshKeyPath must be non-empty; returns an error otherwise.
|
||||||
func deriveKey(passphrase, sshKeyPath string, salt []byte) ([]byte, error) {
|
func deriveKey(passphrase, sshKeyPath string, salt []byte) ([]byte, error) {
|
||||||
if sshKeyPath == "" {
|
if sshKeyPath == "" {
|
||||||
return nil, fmt.Errorf("credential: SSH private key is required but not found (set PICOCLAW_SSH_KEY_PATH or place key at ~/.ssh/picoclaw_ed25519.key)")
|
return nil, fmt.Errorf(
|
||||||
|
"credential: SSH private key is required but not found" +
|
||||||
|
" (set PICOCLAW_SSH_KEY_PATH or place key at ~/.ssh/picoclaw_ed25519.key)")
|
||||||
}
|
}
|
||||||
if !allowedSSHKeyPath(sshKeyPath) {
|
if !allowedSSHKeyPath(sshKeyPath) {
|
||||||
return nil, fmt.Errorf("credential: SSH key path %q is not in an allowed location (PICOCLAW_SSH_KEY_PATH, PICOCLAW_HOME, or ~/.ssh/)", sshKeyPath)
|
return nil, fmt.Errorf(
|
||||||
|
"credential: SSH key path %q is not in an allowed location (PICOCLAW_SSH_KEY_PATH, PICOCLAW_HOME, or ~/.ssh/)",
|
||||||
|
sshKeyPath,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
sshBytes, err := os.ReadFile(sshKeyPath)
|
sshBytes, err := os.ReadFile(sshKeyPath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
|
||||||
|
|
@ -42,7 +42,7 @@ func GenerateSSHKey(path string) error {
|
||||||
}
|
}
|
||||||
privPEM := pem.EncodeToMemory(block)
|
privPEM := pem.EncodeToMemory(block)
|
||||||
|
|
||||||
if err := os.WriteFile(path, privPEM, 0o600); err != nil {
|
if err = os.WriteFile(path, privPEM, 0o600); err != nil {
|
||||||
return fmt.Errorf("credential: keygen: write private key %q: %w", path, err)
|
return fmt.Errorf("credential: keygen: write private key %q: %w", path, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -61,10 +61,16 @@ func TestGenerateSSHKey_CreatesFiles(t *testing.T) {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("read public key: %v", err)
|
t.Fatalf("read public key: %v", err)
|
||||||
}
|
}
|
||||||
_, _, _, _, err = ssh.ParseAuthorizedKey(pubBytes)
|
pubKey, _, _, rest, err := ssh.ParseAuthorizedKey(pubBytes)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("parse public key: %v", err)
|
t.Fatalf("parse public key: %v", err)
|
||||||
}
|
}
|
||||||
|
if pubKey == nil {
|
||||||
|
t.Fatal("expected non-nil public key")
|
||||||
|
}
|
||||||
|
if len(rest) > 0 {
|
||||||
|
t.Errorf("unexpected trailing bytes after public key: %d bytes", len(rest))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestGenerateSSHKey_OverwritesExisting(t *testing.T) {
|
func TestGenerateSSHKey_OverwritesExisting(t *testing.T) {
|
||||||
|
|
@ -80,7 +86,7 @@ func TestGenerateSSHKey_OverwritesExisting(t *testing.T) {
|
||||||
t.Fatalf("read first key: %v", err)
|
t.Fatalf("read first key: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := GenerateSSHKey(keyPath); err != nil {
|
if err = GenerateSSHKey(keyPath); err != nil {
|
||||||
t.Fatalf("second GenerateSSHKey() error = %v", err)
|
t.Fatalf("second GenerateSSHKey() error = %v", err)
|
||||||
}
|
}
|
||||||
second, err := os.ReadFile(keyPath)
|
second, err := os.ReadFile(keyPath)
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue