- config.Explain(envFiles, cliOverrides): per-key source layer following the application precedence cli > process-env > env-file > default; registry is reporting-only (values still applied solely by Load); secrets never shown - ParseEnvFileKeys shares applyEnvFile's parsing rules - cmd/server 'config explain' resolves the same env-file stack as Load and renders the attribution table; smoke: HTTP_PORT/AGENT_BACKEND -> cli - tests: registry liveness sample + layer attribution (file/process/cli/ default) + secret masking - full per-field snapshot with applied/desired revisions remains U10b core
179 lines
5.5 KiB
Go
179 lines
5.5 KiB
Go
package config
|
||
|
||
import (
|
||
"bufio"
|
||
"fmt"
|
||
"os"
|
||
"path/filepath"
|
||
"sort"
|
||
"strings"
|
||
)
|
||
|
||
// knownEnvKeys 是配置环境键注册表(U10b 第一片:reporting-only)。
|
||
// 仅用于 config explain 的来源解释,不参与取值——取值仍由 Load 的
|
||
// 应用逻辑唯一决定;新增环境键时须同步登记,漂移由抽样测试约束。
|
||
var knownEnvKeys = map[string]bool{
|
||
"ADMIN_PASSWORD": true,
|
||
"ADMIN_USERNAME": true,
|
||
"AGENTX_API_BASE": true,
|
||
"AGENTX_API_KEY": true,
|
||
"AGENTX_MODEL": true,
|
||
"AGENTX_TIMEOUT": true,
|
||
"AGENT_BACKEND": true,
|
||
"AGENT_TIMEOUT": true,
|
||
"ALLOW_IN_MEMORY": true,
|
||
"APP_DB_DSN": true,
|
||
"APP_PUBLISH_HOST": true,
|
||
"AUTH_REGISTRATION_ENABLED": true,
|
||
"AUTH_REQUIRE_APPROVAL": true,
|
||
"CONTEXT_DB_HOST": true,
|
||
"CONTEXT_DB_NAME": true,
|
||
"CONTEXT_DB_PASSWORD": true,
|
||
"CONTEXT_DB_PORT": true,
|
||
"CONTEXT_DB_USER": true,
|
||
"CORS_ALLOWED_ORIGINS": true,
|
||
"DATA_DIR": true,
|
||
"EMBEDDING_API_KEY": true,
|
||
"EMBEDDING_ENDPOINT_URL": true,
|
||
"EMBEDDING_MODEL": true,
|
||
"EMBEDDING_PROVIDER": true,
|
||
"ENV": true,
|
||
"FAV_DEGRADED_READ": true,
|
||
"FAV_PUBLISHER_ENABLED": true,
|
||
"HTTP_ADDR": true,
|
||
"HTTP_HOST": true,
|
||
"HTTP_PORT": true,
|
||
"JEV_API_KEY": true,
|
||
"JEV_BASE_URL": true,
|
||
"JEV_DECIDE_MODE": true,
|
||
"KB_DB_HOST": true,
|
||
"KB_DB_NAME": true,
|
||
"KB_DB_PASSWORD": true,
|
||
"KB_DB_PORT": true,
|
||
"KB_DB_USER": true,
|
||
"MATERIAL_FULLTEXT_MAX_BYTES": true,
|
||
"MATERIAL_RAG_TOP_K": true,
|
||
"MAX_CONCURRENT_AGENTS": true,
|
||
"MINERU_API_KEY": true,
|
||
"MINERU_ENDPOINT_URL": true,
|
||
"MINERU_LANGUAGE": true,
|
||
"MINERU_MODEL_VERSION": true,
|
||
"MINERU_TIMEOUT": true,
|
||
"MINIMAX_API_KEY": true,
|
||
"MINIMAX_ENDPOINT_URL": true,
|
||
"PI_AGENT_BIN": true,
|
||
"PI_AGENT_BRIDGE_BIN": true,
|
||
"PI_AGENT_MODEL": true,
|
||
"PI_AGENT_PROVIDER": true,
|
||
"PI_AGENT_RUNNER_AUTH_TOKEN": true,
|
||
"PI_AGENT_RUNNER_URL": true,
|
||
"POSTGRES_SHARED_KB_DSN": true,
|
||
"RERANKER_API_KEY": true,
|
||
"RERANKER_ENDPOINT_URL": true,
|
||
"RERANKER_MODEL": true,
|
||
"RERANKER_PROVIDER": true,
|
||
"RUNNER_AUTH_TOKEN": true,
|
||
"VAULT_MASTER_KEY": true,
|
||
"VAULT_STATE_ROOT": true,
|
||
"VAULT_STORAGE_IDENTITY": true,
|
||
"WORKSPACE_QUOTA_BYTES": true,
|
||
"WORKSPACE_TTL": true,
|
||
}
|
||
|
||
// ExplainLayer 是配置键的一个来源层。
|
||
type ExplainLayer = string
|
||
|
||
const (
|
||
LayerDefault ExplainLayer = "default"
|
||
LayerEnvFile ExplainLayer = "env-file"
|
||
LayerProcessEnv ExplainLayer = "process-env"
|
||
LayerCLI ExplainLayer = "cli"
|
||
)
|
||
|
||
// ExplainEntry 是单个配置键的解释行。
|
||
type ExplainEntry struct {
|
||
Key string
|
||
Layer ExplainLayer
|
||
Value string // 秘密键只显示 <set>/<unset>;env-file 层显示文件路径
|
||
}
|
||
|
||
// ParseEnvFileKeys 用与 applyEnvFile 相同的解析规则列出文件中出现的键。
|
||
func ParseEnvFileKeys(path string) ([]string, error) {
|
||
f, err := os.Open(path)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
defer f.Close()
|
||
var keys []string
|
||
scanner := bufio.NewScanner(f)
|
||
for scanner.Scan() {
|
||
line := strings.TrimSpace(scanner.Text())
|
||
if line == "" || strings.HasPrefix(line, "#") {
|
||
continue
|
||
}
|
||
parts := strings.SplitN(line, "=", 2)
|
||
if len(parts) != 2 {
|
||
continue
|
||
}
|
||
keys = append(keys, strings.TrimSpace(parts[0]))
|
||
}
|
||
return keys, scanner.Err()
|
||
}
|
||
|
||
// Explain 生成各配置键的来源层解释(KTD17 config explain 第一片)。
|
||
// envFiles 按加载顺序(后者覆盖前者);cliOverrides 为 CLI 显式覆盖键。
|
||
// 层序与生效优先级一致:cli > process-env > env-file > default。
|
||
// 非秘密且来源为 env-file/process-env 的键展示脱敏值;秘密键只显示 set 状态。
|
||
func Explain(envFiles []string, cliOverrides map[string]string) []ExplainEntry {
|
||
fileKeys := map[string]string{} // key -> 首个出现的文件
|
||
for _, p := range envFiles {
|
||
keys, err := ParseEnvFileKeys(p)
|
||
if err != nil {
|
||
continue
|
||
}
|
||
for _, k := range keys {
|
||
fileKeys[k] = filepath.ToSlash(p)
|
||
}
|
||
}
|
||
|
||
var out []ExplainEntry
|
||
for k := range knownEnvKeys {
|
||
e := ExplainEntry{Key: k, Layer: LayerDefault, Value: "<unset>"}
|
||
if src, ok := fileKeys[k]; ok {
|
||
e.Layer = LayerEnvFile
|
||
e.Value = src
|
||
if v := os.Getenv(k); v != "" {
|
||
e.Layer = LayerProcessEnv
|
||
e.Value = maskedValue(v)
|
||
}
|
||
} else if v := os.Getenv(k); v != "" {
|
||
e.Layer = LayerProcessEnv
|
||
e.Value = maskedValue(v)
|
||
}
|
||
if v, ok := cliOverrides[k]; ok {
|
||
e.Layer = LayerCLI
|
||
e.Value = maskedValue(v)
|
||
}
|
||
out = append(out, e)
|
||
}
|
||
sort.Slice(out, func(i, j int) bool { return out[i].Key < out[j].Key })
|
||
return out
|
||
}
|
||
|
||
func maskedValue(v string) string {
|
||
if strings.TrimSpace(v) == "" {
|
||
return "<unset>"
|
||
}
|
||
return "<set>"
|
||
}
|
||
|
||
// FormatExplanation 渲染 explain 表(供 CLI 输出与测试断言)。
|
||
func FormatExplanation(entries []ExplainEntry) string {
|
||
var b strings.Builder
|
||
b.WriteString("key layer detail\n")
|
||
for _, e := range entries {
|
||
fmt.Fprintf(&b, "%-37s %-12s %s\n", e.Key, e.Layer, e.Value)
|
||
}
|
||
return b.String()
|
||
}
|