refactor: isolate OpenClaw SecretRef resolution
This commit is contained in:
parent
4cc6bb05b9
commit
6808268342
@ -11,7 +11,7 @@ All notable changes to `discrawl` will be documented in this file.
|
||||
|
||||
### Fixes
|
||||
|
||||
- OpenClaw Discord token loading now accepts SecretRef objects backed by file or env providers in addition to plaintext token strings. (#49) Thanks @TeodoroRodrigo.
|
||||
- OpenClaw Discord token loading now accepts SecretRef objects backed by file or env providers in addition to plaintext token strings, with shared resolver coverage for provider defaults, allowlists, empty values, and unsupported exec refs. (#49) Thanks @TeodoroRodrigo.
|
||||
|
||||
## 0.6.0 - 2026-04-24
|
||||
|
||||
|
||||
@ -108,92 +108,6 @@ type openClawDiscordAcct struct {
|
||||
Guilds map[string]json.RawMessage `json:"guilds"`
|
||||
}
|
||||
|
||||
type openClawSecretValue struct {
|
||||
Plain string
|
||||
Ref *openClawSecretRef
|
||||
}
|
||||
|
||||
type openClawSecretRef struct {
|
||||
Source string `json:"source"`
|
||||
Provider string `json:"provider"`
|
||||
ID string `json:"id"`
|
||||
}
|
||||
|
||||
type openClawSecrets struct {
|
||||
Providers map[string]openClawSecretProvider
|
||||
Aliases map[string]openClawSecretProvider
|
||||
Defaults openClawSecretDefaults
|
||||
}
|
||||
|
||||
type openClawSecretProvider struct {
|
||||
Source string `json:"source"`
|
||||
Path string `json:"path"`
|
||||
Mode string `json:"mode"`
|
||||
Allowlist []string `json:"allowlist"`
|
||||
}
|
||||
|
||||
type openClawSecretDefaults struct {
|
||||
Env string `json:"env"`
|
||||
File string `json:"file"`
|
||||
Exec string `json:"exec"`
|
||||
}
|
||||
|
||||
func (v *openClawSecretValue) UnmarshalJSON(data []byte) error {
|
||||
var plain string
|
||||
if err := json.Unmarshal(data, &plain); err == nil {
|
||||
v.Plain = plain
|
||||
v.Ref = nil
|
||||
return nil
|
||||
}
|
||||
var ref openClawSecretRef
|
||||
if err := json.Unmarshal(data, &ref); err != nil {
|
||||
return err
|
||||
}
|
||||
v.Plain = ""
|
||||
v.Ref = &ref
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *openClawSecrets) UnmarshalJSON(data []byte) error {
|
||||
var aux struct {
|
||||
Providers map[string]openClawSecretProvider `json:"providers"`
|
||||
Defaults openClawSecretDefaults `json:"defaults"`
|
||||
}
|
||||
if err := json.Unmarshal(data, &aux); err != nil {
|
||||
return err
|
||||
}
|
||||
var raw map[string]json.RawMessage
|
||||
if err := json.Unmarshal(data, &raw); err != nil {
|
||||
return err
|
||||
}
|
||||
aliases := map[string]openClawSecretProvider{}
|
||||
for name, value := range raw {
|
||||
if name == "providers" || name == "defaults" {
|
||||
continue
|
||||
}
|
||||
var provider openClawSecretProvider
|
||||
if err := json.Unmarshal(value, &provider); err == nil && (provider.Source != "" || provider.Path != "") {
|
||||
aliases[name] = provider
|
||||
}
|
||||
}
|
||||
s.Providers = aux.Providers
|
||||
s.Aliases = aliases
|
||||
s.Defaults = aux.Defaults
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s openClawSecrets) provider(name string) (openClawSecretProvider, bool) {
|
||||
name = strings.TrimSpace(name)
|
||||
if name == "" {
|
||||
name = "default"
|
||||
}
|
||||
if provider, ok := s.Providers[name]; ok {
|
||||
return provider, true
|
||||
}
|
||||
provider, ok := s.Aliases[name]
|
||||
return provider, ok
|
||||
}
|
||||
|
||||
func Default() Config {
|
||||
home, _ := os.UserHomeDir()
|
||||
base := filepath.Join(home, ".discrawl")
|
||||
@ -532,7 +446,8 @@ func loadOpenClawDiscordFile(path, account string) (OpenClawDiscord, error) {
|
||||
return OpenClawDiscord{}, fmt.Errorf("parse openclaw config: %w", err)
|
||||
}
|
||||
discord := payload.Channels.Discord
|
||||
token, err := resolveOpenClawSecretValue(discord.Token, payload.Secrets, expanded)
|
||||
resolver := newOpenClawSecretResolver(payload.Secrets, expanded)
|
||||
token, err := resolver.resolve(discord.Token)
|
||||
if err != nil {
|
||||
return OpenClawDiscord{}, fmt.Errorf("resolve openclaw discord token: %w", err)
|
||||
}
|
||||
@ -542,7 +457,7 @@ func loadOpenClawDiscordFile(path, account string) (OpenClawDiscord, error) {
|
||||
if acct.Token.empty() && account != normalizeAccount(account) {
|
||||
acct = discord.Accounts[account]
|
||||
}
|
||||
token, err = resolveOpenClawSecretValue(acct.Token, payload.Secrets, expanded)
|
||||
token, err = resolver.resolve(acct.Token)
|
||||
if err != nil {
|
||||
return OpenClawDiscord{}, fmt.Errorf("resolve openclaw discord account token: %w", err)
|
||||
}
|
||||
@ -578,148 +493,6 @@ func NormalizeBotToken(raw string) string {
|
||||
return strings.TrimSpace(raw)
|
||||
}
|
||||
|
||||
func (v openClawSecretValue) empty() bool {
|
||||
return strings.TrimSpace(v.Plain) == "" && v.Ref == nil
|
||||
}
|
||||
|
||||
func resolveOpenClawSecretValue(value openClawSecretValue, secrets openClawSecrets, configPath string) (string, error) {
|
||||
if value.Ref == nil {
|
||||
return NormalizeBotToken(os.ExpandEnv(value.Plain)), nil
|
||||
}
|
||||
ref := *value.Ref
|
||||
source := strings.ToLower(strings.TrimSpace(ref.Source))
|
||||
switch source {
|
||||
case "env":
|
||||
return resolveOpenClawEnvSecret(ref, secrets)
|
||||
case "file":
|
||||
return resolveOpenClawFileSecret(ref, secrets, configPath)
|
||||
case "":
|
||||
return "", errors.New("secret ref missing source")
|
||||
default:
|
||||
return "", fmt.Errorf("unsupported secret ref source %q", ref.Source)
|
||||
}
|
||||
}
|
||||
|
||||
func resolveOpenClawEnvSecret(ref openClawSecretRef, secrets openClawSecrets) (string, error) {
|
||||
id := strings.TrimSpace(ref.ID)
|
||||
if id == "" {
|
||||
return "", errors.New("env secret ref missing id")
|
||||
}
|
||||
providerName := strings.TrimSpace(ref.Provider)
|
||||
if providerName == "" {
|
||||
providerName = defaultOpenClawSecretProvider(secrets.Defaults.Env, "default")
|
||||
}
|
||||
if provider, ok := secrets.provider(providerName); ok {
|
||||
source := strings.ToLower(strings.TrimSpace(provider.Source))
|
||||
if source != "" && source != "env" {
|
||||
return "", fmt.Errorf("secret provider %q has source %q, want env", providerName, provider.Source)
|
||||
}
|
||||
if len(provider.Allowlist) > 0 && !stringInSlice(id, provider.Allowlist) {
|
||||
return "", fmt.Errorf("environment variable %q is not allowlisted in secret provider %q", id, providerName)
|
||||
}
|
||||
} else if providerName != defaultOpenClawSecretProvider(secrets.Defaults.Env, "default") {
|
||||
return "", fmt.Errorf("secret provider %q not found", providerName)
|
||||
}
|
||||
value, ok := os.LookupEnv(id)
|
||||
if !ok || strings.TrimSpace(value) == "" {
|
||||
return "", fmt.Errorf("environment variable %q is missing or empty", id)
|
||||
}
|
||||
return NormalizeBotToken(value), nil
|
||||
}
|
||||
|
||||
func resolveOpenClawFileSecret(ref openClawSecretRef, secrets openClawSecrets, configPath string) (string, error) {
|
||||
providerName := strings.TrimSpace(ref.Provider)
|
||||
if providerName == "" {
|
||||
providerName = defaultOpenClawSecretProvider(secrets.Defaults.File, "filemain")
|
||||
}
|
||||
provider, ok := secrets.provider(providerName)
|
||||
if !ok {
|
||||
return "", fmt.Errorf("secret provider %q not found", providerName)
|
||||
}
|
||||
source := strings.ToLower(strings.TrimSpace(provider.Source))
|
||||
if source != "" && source != "file" {
|
||||
return "", fmt.Errorf("secret provider %q has source %q, want file", providerName, provider.Source)
|
||||
}
|
||||
path := strings.TrimSpace(provider.Path)
|
||||
if path == "" {
|
||||
return "", fmt.Errorf("secret provider %q missing path", providerName)
|
||||
}
|
||||
expanded, err := expandOpenClawSecretPath(path, configPath)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
data, err := os.ReadFile(expanded)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
mode := strings.ToLower(strings.TrimSpace(provider.Mode))
|
||||
if mode == "" {
|
||||
mode = "json"
|
||||
}
|
||||
switch mode {
|
||||
case "json":
|
||||
token, err := readJSONPointerString(data, ref.ID)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("read secret provider %q id %q: %w", providerName, ref.ID, err)
|
||||
}
|
||||
return NormalizeBotToken(os.ExpandEnv(token)), nil
|
||||
case "singlevalue":
|
||||
if strings.TrimSpace(ref.ID) != "value" {
|
||||
return "", fmt.Errorf("secret provider %q singleValue mode requires id %q", providerName, "value")
|
||||
}
|
||||
return NormalizeBotToken(os.ExpandEnv(string(data))), nil
|
||||
default:
|
||||
return "", fmt.Errorf("unsupported secret provider %q mode %q", providerName, provider.Mode)
|
||||
}
|
||||
}
|
||||
|
||||
func expandOpenClawSecretPath(path, configPath string) (string, error) {
|
||||
path = os.ExpandEnv(strings.TrimSpace(path))
|
||||
if path == "" {
|
||||
return "", errors.New("empty secret provider path")
|
||||
}
|
||||
if strings.HasPrefix(path, "~") || filepath.IsAbs(path) {
|
||||
return ExpandPath(path)
|
||||
}
|
||||
return filepath.Clean(filepath.Join(filepath.Dir(configPath), path)), nil
|
||||
}
|
||||
|
||||
func readJSONPointerString(data []byte, pointer string) (string, error) {
|
||||
pointer = strings.TrimSpace(pointer)
|
||||
if pointer == "" || pointer[0] != '/' {
|
||||
return "", errors.New("id must be an absolute JSON pointer")
|
||||
}
|
||||
var current any
|
||||
if err := json.Unmarshal(data, ¤t); err != nil {
|
||||
return "", fmt.Errorf("parse secret file: %w", err)
|
||||
}
|
||||
for _, rawSegment := range strings.Split(pointer[1:], "/") {
|
||||
segment := strings.ReplaceAll(strings.ReplaceAll(rawSegment, "~1", "/"), "~0", "~")
|
||||
object, ok := current.(map[string]any)
|
||||
if !ok {
|
||||
return "", fmt.Errorf("segment %q is not an object", segment)
|
||||
}
|
||||
next, ok := object[segment]
|
||||
if !ok {
|
||||
return "", fmt.Errorf("segment %q not found", segment)
|
||||
}
|
||||
current = next
|
||||
}
|
||||
secret, ok := current.(string)
|
||||
if !ok {
|
||||
return "", errors.New("secret value is not a string")
|
||||
}
|
||||
return secret, nil
|
||||
}
|
||||
|
||||
func defaultOpenClawSecretProvider(configured, fallback string) string {
|
||||
configured = strings.TrimSpace(configured)
|
||||
if configured != "" {
|
||||
return configured
|
||||
}
|
||||
return fallback
|
||||
}
|
||||
|
||||
func normalizeAccount(account string) string {
|
||||
account = strings.TrimSpace(strings.ToLower(account))
|
||||
if account == "" {
|
||||
@ -756,12 +529,3 @@ func uniqueStrings(in []string) []string {
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func stringInSlice(needle string, haystack []string) bool {
|
||||
for _, item := range haystack {
|
||||
if item == needle {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
275
internal/config/openclaw_secrets.go
Normal file
275
internal/config/openclaw_secrets.go
Normal file
@ -0,0 +1,275 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type openClawSecretValue struct {
|
||||
Plain string
|
||||
Ref *openClawSecretRef
|
||||
}
|
||||
|
||||
type openClawSecretRef struct {
|
||||
Source string `json:"source"`
|
||||
Provider string `json:"provider"`
|
||||
ID string `json:"id"`
|
||||
}
|
||||
|
||||
type openClawSecrets struct {
|
||||
Providers map[string]openClawSecretProvider
|
||||
Aliases map[string]openClawSecretProvider
|
||||
Defaults openClawSecretDefaults
|
||||
}
|
||||
|
||||
type openClawSecretProvider struct {
|
||||
Source string `json:"source"`
|
||||
Path string `json:"path"`
|
||||
Mode string `json:"mode"`
|
||||
Allowlist []string `json:"allowlist"`
|
||||
}
|
||||
|
||||
type openClawSecretDefaults struct {
|
||||
Env string `json:"env"`
|
||||
File string `json:"file"`
|
||||
Exec string `json:"exec"`
|
||||
}
|
||||
|
||||
type openClawSecretResolver struct {
|
||||
secrets openClawSecrets
|
||||
configPath string
|
||||
env func(string) (string, bool)
|
||||
readFile func(string) ([]byte, error)
|
||||
}
|
||||
|
||||
func (v *openClawSecretValue) UnmarshalJSON(data []byte) error {
|
||||
var plain string
|
||||
if err := json.Unmarshal(data, &plain); err == nil {
|
||||
v.Plain = plain
|
||||
v.Ref = nil
|
||||
return nil
|
||||
}
|
||||
var ref openClawSecretRef
|
||||
if err := json.Unmarshal(data, &ref); err != nil {
|
||||
return err
|
||||
}
|
||||
v.Plain = ""
|
||||
v.Ref = &ref
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *openClawSecrets) UnmarshalJSON(data []byte) error {
|
||||
var aux struct {
|
||||
Providers map[string]openClawSecretProvider `json:"providers"`
|
||||
Defaults openClawSecretDefaults `json:"defaults"`
|
||||
}
|
||||
if err := json.Unmarshal(data, &aux); err != nil {
|
||||
return err
|
||||
}
|
||||
var raw map[string]json.RawMessage
|
||||
if err := json.Unmarshal(data, &raw); err != nil {
|
||||
return err
|
||||
}
|
||||
aliases := map[string]openClawSecretProvider{}
|
||||
for name, value := range raw {
|
||||
if name == "providers" || name == "defaults" {
|
||||
continue
|
||||
}
|
||||
var provider openClawSecretProvider
|
||||
if err := json.Unmarshal(value, &provider); err == nil && (provider.Source != "" || provider.Path != "") {
|
||||
aliases[name] = provider
|
||||
}
|
||||
}
|
||||
s.Providers = aux.Providers
|
||||
s.Aliases = aliases
|
||||
s.Defaults = aux.Defaults
|
||||
return nil
|
||||
}
|
||||
|
||||
func newOpenClawSecretResolver(secrets openClawSecrets, configPath string) openClawSecretResolver {
|
||||
return openClawSecretResolver{
|
||||
secrets: secrets,
|
||||
configPath: configPath,
|
||||
env: os.LookupEnv,
|
||||
readFile: os.ReadFile,
|
||||
}
|
||||
}
|
||||
|
||||
func (v openClawSecretValue) empty() bool {
|
||||
return strings.TrimSpace(v.Plain) == "" && v.Ref == nil
|
||||
}
|
||||
|
||||
func (r openClawSecretResolver) resolve(value openClawSecretValue) (string, error) {
|
||||
if value.Ref == nil {
|
||||
return NormalizeBotToken(os.ExpandEnv(value.Plain)), nil
|
||||
}
|
||||
token, err := r.resolveRef(*value.Ref)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if token == "" {
|
||||
return "", errors.New("secret reference resolved to an empty value")
|
||||
}
|
||||
return token, nil
|
||||
}
|
||||
|
||||
func (r openClawSecretResolver) resolveRef(ref openClawSecretRef) (string, error) {
|
||||
source := strings.ToLower(strings.TrimSpace(ref.Source))
|
||||
switch source {
|
||||
case "env":
|
||||
return r.resolveEnv(ref)
|
||||
case "file":
|
||||
return r.resolveFile(ref)
|
||||
case "exec":
|
||||
return "", errors.New("exec SecretRefs are not supported by discrawl init --from-openclaw")
|
||||
case "":
|
||||
return "", errors.New("secret ref missing source")
|
||||
default:
|
||||
return "", fmt.Errorf("unsupported secret ref source %q", ref.Source)
|
||||
}
|
||||
}
|
||||
|
||||
func (r openClawSecretResolver) resolveEnv(ref openClawSecretRef) (string, error) {
|
||||
id := strings.TrimSpace(ref.ID)
|
||||
if id == "" {
|
||||
return "", errors.New("env secret ref missing id")
|
||||
}
|
||||
providerName := strings.TrimSpace(ref.Provider)
|
||||
if providerName == "" {
|
||||
providerName = defaultOpenClawSecretProvider(r.secrets.Defaults.Env, "default")
|
||||
}
|
||||
if provider, ok := r.secrets.provider(providerName); ok {
|
||||
source := strings.ToLower(strings.TrimSpace(provider.Source))
|
||||
if source != "" && source != "env" {
|
||||
return "", fmt.Errorf("secret provider %q has source %q, want env", providerName, provider.Source)
|
||||
}
|
||||
if len(provider.Allowlist) > 0 && !stringInSlice(id, provider.Allowlist) {
|
||||
return "", fmt.Errorf("environment variable %q is not allowlisted in secret provider %q", id, providerName)
|
||||
}
|
||||
} else if providerName != defaultOpenClawSecretProvider(r.secrets.Defaults.Env, "default") {
|
||||
return "", fmt.Errorf("secret provider %q not found", providerName)
|
||||
}
|
||||
value, ok := r.env(id)
|
||||
if !ok || strings.TrimSpace(value) == "" {
|
||||
return "", fmt.Errorf("environment variable %q is missing or empty", id)
|
||||
}
|
||||
return NormalizeBotToken(value), nil
|
||||
}
|
||||
|
||||
func (r openClawSecretResolver) resolveFile(ref openClawSecretRef) (string, error) {
|
||||
providerName := strings.TrimSpace(ref.Provider)
|
||||
if providerName == "" {
|
||||
providerName = defaultOpenClawSecretProvider(r.secrets.Defaults.File, "filemain")
|
||||
}
|
||||
provider, ok := r.secrets.provider(providerName)
|
||||
if !ok {
|
||||
return "", fmt.Errorf("secret provider %q not found", providerName)
|
||||
}
|
||||
source := strings.ToLower(strings.TrimSpace(provider.Source))
|
||||
if source != "" && source != "file" {
|
||||
return "", fmt.Errorf("secret provider %q has source %q, want file", providerName, provider.Source)
|
||||
}
|
||||
path := strings.TrimSpace(provider.Path)
|
||||
if path == "" {
|
||||
return "", fmt.Errorf("secret provider %q missing path", providerName)
|
||||
}
|
||||
expanded, err := expandOpenClawSecretPath(path, r.configPath)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
data, err := r.readFile(expanded)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
mode := strings.ToLower(strings.TrimSpace(provider.Mode))
|
||||
if mode == "" {
|
||||
mode = "json"
|
||||
}
|
||||
switch mode {
|
||||
case "json":
|
||||
token, err := readJSONPointerString(data, ref.ID)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("read secret provider %q id %q: %w", providerName, ref.ID, err)
|
||||
}
|
||||
return NormalizeBotToken(os.ExpandEnv(token)), nil
|
||||
case "singlevalue":
|
||||
if strings.TrimSpace(ref.ID) != "value" {
|
||||
return "", fmt.Errorf("secret provider %q singleValue mode requires id %q", providerName, "value")
|
||||
}
|
||||
return NormalizeBotToken(os.ExpandEnv(string(data))), nil
|
||||
default:
|
||||
return "", fmt.Errorf("unsupported secret provider %q mode %q", providerName, provider.Mode)
|
||||
}
|
||||
}
|
||||
|
||||
func (s openClawSecrets) provider(name string) (openClawSecretProvider, bool) {
|
||||
name = strings.TrimSpace(name)
|
||||
if name == "" {
|
||||
name = "default"
|
||||
}
|
||||
if provider, ok := s.Providers[name]; ok {
|
||||
return provider, true
|
||||
}
|
||||
provider, ok := s.Aliases[name]
|
||||
return provider, ok
|
||||
}
|
||||
|
||||
func expandOpenClawSecretPath(path, configPath string) (string, error) {
|
||||
path = os.ExpandEnv(strings.TrimSpace(path))
|
||||
if path == "" {
|
||||
return "", errors.New("empty secret provider path")
|
||||
}
|
||||
if strings.HasPrefix(path, "~") || filepath.IsAbs(path) {
|
||||
return ExpandPath(path)
|
||||
}
|
||||
return filepath.Clean(filepath.Join(filepath.Dir(configPath), path)), nil
|
||||
}
|
||||
|
||||
func readJSONPointerString(data []byte, pointer string) (string, error) {
|
||||
pointer = strings.TrimSpace(pointer)
|
||||
if pointer == "" || pointer[0] != '/' {
|
||||
return "", errors.New("id must be an absolute JSON pointer")
|
||||
}
|
||||
var current any
|
||||
if err := json.Unmarshal(data, ¤t); err != nil {
|
||||
return "", fmt.Errorf("parse secret file: %w", err)
|
||||
}
|
||||
for _, rawSegment := range strings.Split(pointer[1:], "/") {
|
||||
segment := strings.ReplaceAll(strings.ReplaceAll(rawSegment, "~1", "/"), "~0", "~")
|
||||
object, ok := current.(map[string]any)
|
||||
if !ok {
|
||||
return "", fmt.Errorf("segment %q is not an object", segment)
|
||||
}
|
||||
next, ok := object[segment]
|
||||
if !ok {
|
||||
return "", fmt.Errorf("segment %q not found", segment)
|
||||
}
|
||||
current = next
|
||||
}
|
||||
secret, ok := current.(string)
|
||||
if !ok {
|
||||
return "", errors.New("secret value is not a string")
|
||||
}
|
||||
return secret, nil
|
||||
}
|
||||
|
||||
func defaultOpenClawSecretProvider(configured, fallback string) string {
|
||||
configured = strings.TrimSpace(configured)
|
||||
if configured != "" {
|
||||
return configured
|
||||
}
|
||||
return fallback
|
||||
}
|
||||
|
||||
func stringInSlice(needle string, haystack []string) bool {
|
||||
for _, item := range haystack {
|
||||
if item == needle {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
138
internal/config/openclaw_secrets_test.go
Normal file
138
internal/config/openclaw_secrets_test.go
Normal file
@ -0,0 +1,138 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestOpenClawSecretResolverUsesFileDefaultProvider(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
secretsPath := filepath.Join(dir, "secrets.json")
|
||||
require.NoError(t, writeTestFile(secretsPath, `{
|
||||
"discord": {
|
||||
"token": "Bot default-file-token"
|
||||
}
|
||||
}`))
|
||||
|
||||
resolver := newOpenClawSecretResolver(openClawSecrets{
|
||||
Defaults: openClawSecretDefaults{File: "vaultfile"},
|
||||
Providers: map[string]openClawSecretProvider{
|
||||
"vaultfile": {
|
||||
Source: "file",
|
||||
Path: "secrets.json",
|
||||
Mode: "json",
|
||||
},
|
||||
},
|
||||
}, filepath.Join(dir, "openclaw.json"))
|
||||
|
||||
token, err := resolver.resolve(openClawSecretValue{Ref: &openClawSecretRef{
|
||||
Source: "file",
|
||||
ID: "/discord/token",
|
||||
}})
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, "default-file-token", token)
|
||||
}
|
||||
|
||||
func TestOpenClawSecretResolverReadsSingleValueFile(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
require.NoError(t, writeTestFile(filepath.Join(dir, "discord-token.txt"), "Bot single-value-token\n"))
|
||||
resolver := newOpenClawSecretResolver(openClawSecrets{
|
||||
Providers: map[string]openClawSecretProvider{
|
||||
"filemain": {
|
||||
Source: "file",
|
||||
Path: "discord-token.txt",
|
||||
Mode: "singleValue",
|
||||
},
|
||||
},
|
||||
}, filepath.Join(dir, "openclaw.json"))
|
||||
|
||||
token, err := resolver.resolve(openClawSecretValue{Ref: &openClawSecretRef{
|
||||
Source: "file",
|
||||
Provider: "filemain",
|
||||
ID: "value",
|
||||
}})
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, "single-value-token", token)
|
||||
}
|
||||
|
||||
func TestOpenClawSecretResolverRejectsSingleValueFilePointerIDs(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
require.NoError(t, writeTestFile(filepath.Join(dir, "discord-token.txt"), "token"))
|
||||
resolver := newOpenClawSecretResolver(openClawSecrets{
|
||||
Providers: map[string]openClawSecretProvider{
|
||||
"filemain": {
|
||||
Source: "file",
|
||||
Path: "discord-token.txt",
|
||||
Mode: "singleValue",
|
||||
},
|
||||
},
|
||||
}, filepath.Join(dir, "openclaw.json"))
|
||||
|
||||
_, err := resolver.resolve(openClawSecretValue{Ref: &openClawSecretRef{
|
||||
Source: "file",
|
||||
Provider: "filemain",
|
||||
ID: "/discord/token",
|
||||
}})
|
||||
require.ErrorContains(t, err, `singleValue mode requires id "value"`)
|
||||
}
|
||||
|
||||
func TestOpenClawSecretResolverRejectsEmptyFileSecret(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
require.NoError(t, writeTestFile(filepath.Join(dir, "secrets.json"), `{"discord":{"token":" "}}`))
|
||||
resolver := newOpenClawSecretResolver(openClawSecrets{
|
||||
Providers: map[string]openClawSecretProvider{
|
||||
"filemain": {
|
||||
Source: "file",
|
||||
Path: "secrets.json",
|
||||
Mode: "json",
|
||||
},
|
||||
},
|
||||
}, filepath.Join(dir, "openclaw.json"))
|
||||
|
||||
_, err := resolver.resolve(openClawSecretValue{Ref: &openClawSecretRef{
|
||||
Source: "file",
|
||||
Provider: "filemain",
|
||||
ID: "/discord/token",
|
||||
}})
|
||||
require.ErrorContains(t, err, "secret reference resolved to an empty value")
|
||||
}
|
||||
|
||||
func TestOpenClawSecretResolverRejectsFileProviderSourceMismatch(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
resolver := newOpenClawSecretResolver(openClawSecrets{
|
||||
Providers: map[string]openClawSecretProvider{
|
||||
"filemain": {
|
||||
Source: "env",
|
||||
Path: "secrets.json",
|
||||
},
|
||||
},
|
||||
}, filepath.Join(dir, "openclaw.json"))
|
||||
|
||||
_, err := resolver.resolve(openClawSecretValue{Ref: &openClawSecretRef{
|
||||
Source: "file",
|
||||
Provider: "filemain",
|
||||
ID: "/discord/token",
|
||||
}})
|
||||
require.ErrorContains(t, err, `secret provider "filemain" has source "env", want file`)
|
||||
}
|
||||
|
||||
func TestOpenClawSecretResolverRejectsExecRefsClearly(t *testing.T) {
|
||||
resolver := newOpenClawSecretResolver(openClawSecrets{}, filepath.Join(t.TempDir(), "openclaw.json"))
|
||||
|
||||
_, err := resolver.resolve(openClawSecretValue{Ref: &openClawSecretRef{
|
||||
Source: "exec",
|
||||
Provider: "vault",
|
||||
ID: "discord/token",
|
||||
}})
|
||||
require.ErrorContains(t, err, "exec SecretRefs are not supported by discrawl init --from-openclaw")
|
||||
}
|
||||
|
||||
func writeTestFile(path, contents string) error {
|
||||
if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil {
|
||||
return err
|
||||
}
|
||||
return os.WriteFile(path, []byte(contents), 0o600)
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user