Unified Agent + reusable Go agent core.
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

address review feedback: surface bedrock credential errors, validate static creds, docs, tests

- ResolveBedrockCredentials errors are now returned by New() instead of being ignored
- Static credentials now require both aws_key and aws_secret; partial configs return an error
- Update config.example.yaml and docs/configuration.md with aws_profile and aws_session_token
- Add bedrock_credentials_test.go covering nil config, partial static credentials,
valid static credentials, default credential chain, and nonexistent profiles

authored by

Teteuya and committed by
Lyric Wai
39f366c0 f60d8387

+189 -6
+6
assets/config/config.example.yaml
··· 56 56 azure: 57 57 deployment: "" # Azure OpenAI deployment name 58 58 # Provider-specific settings for AWS Bedrock. 59 + # Authentication methods (in order of precedence when multiple are set): 60 + # 1. Named profile: set aws_profile to use ~/.aws/config 61 + # 2. Static credentials: set aws_key + aws_secret (+ optional aws_session_token) 62 + # 3. Default chain: leave all credential fields empty to use env vars / instance metadata 59 63 bedrock: 60 64 aws_key: "${AWS_ACCESS_KEY_ID}" 61 65 aws_secret: "${AWS_SECRET_ACCESS_KEY}" 66 + aws_session_token: "" # Optional: for STS temporary credentials / SSO 67 + aws_profile: "" # Optional: named AWS profile from ~/.aws/config 62 68 region: "" 63 69 model_arn: "" 64 70 # Provider-specific settings for Cloudflare Workers AI.
+2
docs/configuration.md
··· 272 272 273 273 - `llm.azure.deployment` -> `MISTER_MORPH_LLM_AZURE_DEPLOYMENT` 274 274 - `llm.bedrock.model_arn` -> `MISTER_MORPH_LLM_BEDROCK_MODEL_ARN` 275 + - `llm.bedrock.aws_profile` -> `MISTER_MORPH_LLM_BEDROCK_AWS_PROFILE` 276 + - `llm.bedrock.aws_session_token` -> `MISTER_MORPH_LLM_BEDROCK_AWS_SESSION_TOKEN` 275 277 276 278 ## Key Config Areas 277 279
+4 -1
internal/llmutil/llmutil.go
··· 169 169 } 170 170 switch provider { 171 171 case "openai", "openai_resp", "openai_custom", "deepseek", "xai", "gemini", "azure", "anthropic", "bedrock", "susanoo", "cloudflare": 172 - c := uniaiProvider.New(uniaiProvider.Config{ 172 + c, err := uniaiProvider.New(uniaiProvider.Config{ 173 173 Provider: uniaiProviderName, 174 174 Endpoint: strings.TrimSpace(cfg.Endpoint), 175 175 APIKey: strings.TrimSpace(cfg.APIKey), ··· 201 201 ), 202 202 CloudflareAPIBase: strings.TrimSpace(cfg.Endpoint), 203 203 }) 204 + if err != nil { 205 + return nil, err 206 + } 204 207 return c, nil 205 208 default: 206 209 return nil, fmt.Errorf("unknown provider: %s", cfg.Provider)
+7 -1
providers/uniai/bedrock_credentials.go
··· 25 25 accessKey := strings.TrimSpace(cfg.AwsKey) 26 26 secretKey := strings.TrimSpace(cfg.AwsSecret) 27 27 sessionToken := strings.TrimSpace(cfg.AwsSessionToken) 28 - if accessKey != "" || secretKey != "" || sessionToken != "" { 28 + hasAccessKey := accessKey != "" 29 + hasSecretKey := secretKey != "" 30 + hasSessionToken := sessionToken != "" 31 + 32 + if hasAccessKey && hasSecretKey { 29 33 opts = append(opts, awsconfig.WithCredentialsProvider( 30 34 awscredentials.NewStaticCredentialsProvider(accessKey, secretKey, sessionToken), 31 35 )) 36 + } else if hasAccessKey || hasSecretKey || hasSessionToken { 37 + return fmt.Errorf("bedrock static credentials incomplete: aws_key and aws_secret must both be set when either is provided (aws_session_token is optional)") 32 38 } 33 39 34 40 awsCfg, err := awsconfig.LoadDefaultConfig(ctx, opts...)
+161
providers/uniai/bedrock_credentials_test.go
··· 1 + package uniai 2 + 3 + import ( 4 + "context" 5 + "strings" 6 + "testing" 7 + ) 8 + 9 + func TestResolveBedrockCredentialsNilConfig(t *testing.T) { 10 + err := ResolveBedrockCredentials(context.Background(), nil) 11 + if err == nil { 12 + t.Fatal("expected error for nil config, got nil") 13 + } 14 + if !strings.Contains(err.Error(), "bedrock config is nil") { 15 + t.Fatalf("unexpected error message: %v", err) 16 + } 17 + } 18 + 19 + func TestResolveBedrockCredentialsPartialStaticKeyOnly(t *testing.T) { 20 + cfg := Config{ 21 + Provider: "bedrock", 22 + AwsKey: "AKIA...", 23 + AwsRegion: "us-east-1", 24 + } 25 + err := ResolveBedrockCredentials(context.Background(), &cfg) 26 + if err == nil { 27 + t.Fatal("expected error for partial static credentials (key only), got nil") 28 + } 29 + if !strings.Contains(err.Error(), "incomplete") { 30 + t.Fatalf("expected incomplete credentials error, got: %v", err) 31 + } 32 + } 33 + 34 + func TestResolveBedrockCredentialsPartialStaticSecretOnly(t *testing.T) { 35 + cfg := Config{ 36 + Provider: "bedrock", 37 + AwsSecret: "secret...", 38 + AwsRegion: "us-east-1", 39 + } 40 + err := ResolveBedrockCredentials(context.Background(), &cfg) 41 + if err == nil { 42 + t.Fatal("expected error for partial static credentials (secret only), got nil") 43 + } 44 + if !strings.Contains(err.Error(), "incomplete") { 45 + t.Fatalf("expected incomplete credentials error, got: %v", err) 46 + } 47 + } 48 + 49 + func TestResolveBedrockCredentialsPartialStaticSessionTokenOnly(t *testing.T) { 50 + cfg := Config{ 51 + Provider: "bedrock", 52 + AwsSessionToken: "token...", 53 + AwsRegion: "us-east-1", 54 + } 55 + err := ResolveBedrockCredentials(context.Background(), &cfg) 56 + if err == nil { 57 + t.Fatal("expected error for partial static credentials (session token only), got nil") 58 + } 59 + if !strings.Contains(err.Error(), "incomplete") { 60 + t.Fatalf("expected incomplete credentials error, got: %v", err) 61 + } 62 + } 63 + 64 + func TestResolveBedrockCredentialsPartialStaticKeyAndToken(t *testing.T) { 65 + cfg := Config{ 66 + Provider: "bedrock", 67 + AwsKey: "AKIA...", 68 + AwsSessionToken: "token...", 69 + AwsRegion: "us-east-1", 70 + } 71 + err := ResolveBedrockCredentials(context.Background(), &cfg) 72 + if err == nil { 73 + t.Fatal("expected error for partial static credentials (key + token), got nil") 74 + } 75 + if !strings.Contains(err.Error(), "incomplete") { 76 + t.Fatalf("expected incomplete credentials error, got: %v", err) 77 + } 78 + } 79 + 80 + func TestResolveBedrockCredentialsStaticCredentialsOK(t *testing.T) { 81 + cfg := Config{ 82 + Provider: "bedrock", 83 + AwsKey: "AKIAIOSFODNN7EXAMPLE", 84 + AwsSecret: "wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY", 85 + AwsSessionToken: "optional-session-token", 86 + AwsRegion: "us-east-1", 87 + } 88 + err := ResolveBedrockCredentials(context.Background(), &cfg) 89 + if err != nil { 90 + t.Fatalf("unexpected error for valid static credentials: %v", err) 91 + } 92 + if cfg.AwsKey != "AKIAIOSFODNN7EXAMPLE" { 93 + t.Fatalf("expected key to be preserved, got %q", cfg.AwsKey) 94 + } 95 + if cfg.AwsSecret != "wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY" { 96 + t.Fatalf("expected secret to be preserved, got %q", cfg.AwsSecret) 97 + } 98 + if cfg.AwsSessionToken != "optional-session-token" { 99 + t.Fatalf("expected session token to be preserved, got %q", cfg.AwsSessionToken) 100 + } 101 + if cfg.AwsRegion != "us-east-1" { 102 + t.Fatalf("expected region to be preserved, got %q", cfg.AwsRegion) 103 + } 104 + } 105 + 106 + func TestResolveBedrockCredentialsStaticCredentialsWithoutSessionToken(t *testing.T) { 107 + cfg := Config{ 108 + Provider: "bedrock", 109 + AwsKey: "AKIAIOSFODNN7EXAMPLE", 110 + AwsSecret: "wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY", 111 + AwsRegion: "us-east-1", 112 + } 113 + err := ResolveBedrockCredentials(context.Background(), &cfg) 114 + if err != nil { 115 + t.Fatalf("unexpected error for valid static credentials without session token: %v", err) 116 + } 117 + if cfg.AwsKey != "AKIAIOSFODNN7EXAMPLE" { 118 + t.Fatalf("expected key to be preserved, got %q", cfg.AwsKey) 119 + } 120 + if cfg.AwsSecret != "wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY" { 121 + t.Fatalf("expected secret to be preserved, got %q", cfg.AwsSecret) 122 + } 123 + } 124 + 125 + func TestResolveBedrockCredentialsDefaultChainNoCreds(t *testing.T) { 126 + // When no credentials are provided at all, LoadDefaultConfig falls back 127 + // to the ambient credential chain. In a test environment with no AWS 128 + // config/env, this will fail at credential retrieval. 129 + cfg := Config{ 130 + Provider: "bedrock", 131 + AwsRegion: "us-east-1", 132 + } 133 + err := ResolveBedrockCredentials(context.Background(), &cfg) 134 + if err == nil { 135 + t.Fatal("expected error when ambient AWS credentials are missing, got nil") 136 + } 137 + if !strings.Contains(err.Error(), "retrieve bedrock aws credentials") { 138 + t.Fatalf("expected credential retrieval error, got: %v", err) 139 + } 140 + } 141 + 142 + func TestResolveBedrockCredentialsProfileNotFound(t *testing.T) { 143 + cfg := Config{ 144 + Provider: "bedrock", 145 + AwsProfile: "nonexistent-profile-" + randomSuffix(), 146 + AwsRegion: "us-east-1", 147 + } 148 + err := ResolveBedrockCredentials(context.Background(), &cfg) 149 + if err == nil { 150 + t.Fatal("expected error for nonexistent profile, got nil") 151 + } 152 + } 153 + 154 + func randomSuffix() string { 155 + // Simple random suffix to avoid colliding with real profiles 156 + b := make([]byte, 8) 157 + for i := range b { 158 + b[i] = byte('a' + (i*7)%26) 159 + } 160 + return string(b) 161 + }
+5 -3
providers/uniai/client.go
··· 64 64 client *uniaiapi.Client 65 65 } 66 66 67 - func New(cfg Config) *Client { 67 + func New(cfg Config) (*Client, error) { 68 68 provider := strings.ToLower(strings.TrimSpace(cfg.Provider)) 69 69 pricing := cfg.Pricing 70 70 if pricing == nil { ··· 72 72 } 73 73 74 74 if provider == "bedrock" { 75 - _ = ResolveBedrockCredentials(context.Background(), &cfg) 75 + if err := ResolveBedrockCredentials(context.Background(), &cfg); err != nil { 76 + return nil, fmt.Errorf("resolve bedrock credentials: %w", err) 77 + } 76 78 } 77 79 78 80 openAIBase := normalizeOpenAIBase(cfg.Endpoint) ··· 126 128 cacheKeyPrefix: strings.TrimSpace(cfg.CacheKeyPrefix), 127 129 toolsEmulationMode: normalizeToolsEmulationMode(cfg.ToolsEmulationMode), 128 130 client: uniaiapi.New(uCfg), 129 - } 131 + }, nil 130 132 } 131 133 132 134 func (c *Client) Chat(ctx context.Context, req llm.Request) (llm.Result, error) {
+4 -1
providers/uniai/client_test.go
··· 872 872 } 873 873 874 874 func TestNewStoresCacheTTLDefault(t *testing.T) { 875 - client := New(Config{ 875 + client, err := New(Config{ 876 876 Provider: "openai_resp", 877 877 Model: "gpt-5.2", 878 878 CacheTTL: "long", 879 879 CacheKeyPrefix: "test-prefix", 880 880 }) 881 + if err != nil { 882 + t.Fatalf("unexpected error: %v", err) 883 + } 881 884 882 885 if client.cacheTTL != "long" { 883 886 t.Fatalf("cacheTTL = %q, want long", client.cacheTTL)