Files
Agent-System/internal/config/config.go
T
weichenandCursor 3478a5a05a feat(config): add FAV vault flags defaulting off
Gate FAV_DEGRADED_READ and FAV_PUBLISHER_ENABLED behind explicit opt-in,
and refuse VAULT_STATE_ROOT colliding with runtime data/{env}.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-16 15:40:10 +00:00

562 lines
17 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package config
import (
"bufio"
"fmt"
"os"
"path/filepath"
"strconv"
"strings"
"time"
)
type Config struct {
Env string `json:"env"`
DataDir string `json:"data_dir"`
HTTPHost string `json:"http_host"`
HTTPPort int `json:"http_port"`
AgentBackend string `json:"agent_backend"`
AgentTimeout time.Duration `json:"agent_timeout"`
CursorAPIKey string `json:"cursor_api_key"`
CursorModel string `json:"cursor_model"`
CursorBin string `json:"cursor_bin"`
CursorBridgeBin string `json:"cursor_bridge_bin"`
PiAgentBin string `json:"pi_agent_bin"`
PiAgentBridgeBin string `json:"pi_agent_bridge_bin"`
PiAgentProvider string `json:"pi_agent_provider"`
PiAgentModel string `json:"pi_agent_model"`
PiAgentRunnerURL string `json:"pi_agent_runner_url"`
PiAgentRunnerAuthToken string `json:"pi_agent_runner_auth_token"`
KbDbHost string `json:"kb_db_host"`
KbDbPort int `json:"kb_db_port"`
KbDbUser string `json:"kb_db_user"`
KbDbPassword string `json:"kb_db_password"`
KbDbName string `json:"kb_db_name"`
// 专属记忆与多级上下文数据库配置
ContextDbHost string `json:"context_db_host"`
ContextDbPort int `json:"context_db_port"`
ContextDbUser string `json:"context_db_user"`
ContextDbPassword string `json:"context_db_password"`
ContextDbName string `json:"context_db_name"`
// 向量化 Embedding 服务配置
EmbeddingEndpointURL string `json:"embedding_endpoint_url"`
EmbeddingProvider string `json:"embedding_provider"`
EmbeddingModel string `json:"embedding_model"`
EmbeddingAPIKey string `json:"embedding_api_key"`
// 语义重排 Reranker 服务配置
RerankerEndpointURL string `json:"reranker_endpoint_url"`
RerankerProvider string `json:"reranker_provider"`
RerankerModel string `json:"reranker_model"`
RerankerAPIKey string `json:"reranker_api_key"`
// 系统管理员与准入认证配置
AdminUsername string `json:"admin_username"`
AdminPassword string `json:"admin_password"`
AuthRegistrationEnabled bool `json:"auth_registration_enabled"`
AuthRequireApproval bool `json:"auth_require_approval"`
// Agent 全局并发上限(保护 pi-agent / GPU / API)
MaxConcurrentAgents int `json:"max_concurrent_agents"`
// AllowInMemory 显式允许数据库不可用时降级内存模式(仅限本地 Demo)
AllowInMemory bool `json:"allow_in_memory"`
// CORSAllowedOrigins 允许的浏览器 Origin 列表;空列表时不回显 Access-Control-Allow-Origin
CORSAllowedOrigins []string `json:"cors_allowed_origins"`
// Final Artifact Vault (FAV) — defaults stay off until separate approval.
VaultStateRoot string `json:"vault_state_root"`
VaultStorageIdentity string `json:"vault_storage_identity"`
VaultMasterKey string `json:"vault_master_key"`
FAVDegradedRead bool `json:"fav_degraded_read"` // FAV_DEGRADED_READ; default false
FAVPublisherEnabled bool `json:"fav_publisher_enabled"` // FAV_PUBLISHER_ENABLED; default false
}
func Load(path string) (*Config, error) {
cfg := &Config{
Env: "dev",
DataDir: "./data/dev",
HTTPHost: "127.0.0.1",
HTTPPort: 8080,
AgentBackend: "pi-agent",
AgentTimeout: 30 * time.Minute,
CursorModel: "auto",
CursorBin: "/usr/local/bin/cursor",
CursorBridgeBin: "tools/cursor-bridge/index.mjs",
PiAgentBin: "/opt/homebrew/bin/pi",
PiAgentBridgeBin: "tools/pi-bridge/index.mjs",
PiAgentProvider: "minimax",
PiAgentModel: "MiniMax-M2.7-highspeed",
KbDbHost: "127.0.0.1",
KbDbPort: 5432,
KbDbUser: "regkb",
KbDbPassword: "shared_kb_secret_pass",
KbDbName: "regulations",
ContextDbHost: "127.0.0.1",
ContextDbPort: 5433,
ContextDbUser: "app",
ContextDbPassword: "dev_secret_db_pass",
ContextDbName: "agent_system_dev",
EmbeddingEndpointURL: "https://www.dmxapi.cn/v1/embeddings",
EmbeddingProvider: "openai_compatible",
EmbeddingModel: "bge-m3",
EmbeddingAPIKey: "",
RerankerEndpointURL: "https://www.dmxapi.cn/v1/rerank",
RerankerProvider: "openai_compatible",
RerankerModel: "bge-reranker-v2-m3-free",
RerankerAPIKey: "",
AdminUsername: "admin",
AdminPassword: "admin-secret-password-123",
AuthRegistrationEnabled: true,
AuthRequireApproval: false,
MaxConcurrentAgents: 8,
}
if path == "" {
if _, err := os.Stat("configs/.env.dev"); err == nil {
path = "configs/.env.dev"
} else if _, err := os.Stat(".env"); err == nil {
path = ".env"
}
}
f, err := os.Open(path)
if err == nil {
defer f.Close()
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
}
key := strings.TrimSpace(parts[0])
val := strings.TrimSpace(parts[1])
if commentIdx := strings.Index(val, " #"); commentIdx != -1 {
val = strings.TrimSpace(val[:commentIdx])
}
switch key {
case "ENV":
cfg.Env = val
case "DATA_DIR":
cfg.DataDir = val
case "APP_PUBLISH_HOST", "HTTP_HOST":
cfg.HTTPHost = val
case "HTTP_PORT":
if p, err := strconv.Atoi(val); err == nil {
cfg.HTTPPort = p
}
case "AGENT_BACKEND":
cfg.AgentBackend = val
case "AGENT_TIMEOUT":
if d, err := time.ParseDuration(val); err == nil {
cfg.AgentTimeout = d
}
case "CURSOR_API_KEY":
cfg.CursorAPIKey = val
case "CURSOR_MODEL":
cfg.CursorModel = val
case "CURSOR_BIN":
cfg.CursorBin = val
case "CURSOR_BRIDGE_BIN":
cfg.CursorBridgeBin = val
case "PI_AGENT_BIN":
cfg.PiAgentBin = val
case "PI_AGENT_BRIDGE_BIN":
cfg.PiAgentBridgeBin = val
case "PI_AGENT_PROVIDER":
cfg.PiAgentProvider = val
case "PI_AGENT_MODEL":
cfg.PiAgentModel = val
case "PI_AGENT_RUNNER_URL":
cfg.PiAgentRunnerURL = val
case "KB_DB_HOST":
cfg.KbDbHost = val
case "KB_DB_PORT":
if p, err := strconv.Atoi(val); err == nil {
cfg.KbDbPort = p
}
case "KB_DB_USER":
cfg.KbDbUser = val
case "KB_DB_PASSWORD":
cfg.KbDbPassword = val
case "KB_DB_NAME":
cfg.KbDbName = val
case "CONTEXT_DB_HOST":
cfg.ContextDbHost = val
case "CONTEXT_DB_PORT":
if p, err := strconv.Atoi(val); err == nil {
cfg.ContextDbPort = p
}
case "CONTEXT_DB_USER":
cfg.ContextDbUser = val
case "CONTEXT_DB_PASSWORD":
cfg.ContextDbPassword = val
case "CONTEXT_DB_NAME":
cfg.ContextDbName = val
case "EMBEDDING_ENDPOINT_URL":
cfg.EmbeddingEndpointURL = val
case "EMBEDDING_PROVIDER":
cfg.EmbeddingProvider = val
case "EMBEDDING_MODEL":
cfg.EmbeddingModel = val
case "EMBEDDING_API_KEY":
cfg.EmbeddingAPIKey = val
case "RERANKER_ENDPOINT_URL":
cfg.RerankerEndpointURL = val
case "RERANKER_PROVIDER":
cfg.RerankerProvider = val
case "RERANKER_MODEL":
cfg.RerankerModel = val
case "RERANKER_API_KEY":
cfg.RerankerAPIKey = val
case "ADMIN_USERNAME":
cfg.AdminUsername = val
case "ADMIN_PASSWORD":
cfg.AdminPassword = val
case "AUTH_REGISTRATION_ENABLED":
cfg.AuthRegistrationEnabled = val == "true" || val == "1"
case "AUTH_REQUIRE_APPROVAL":
cfg.AuthRequireApproval = val == "true" || val == "1"
case "MAX_CONCURRENT_AGENTS":
if n, err := strconv.Atoi(val); err == nil && n > 0 {
cfg.MaxConcurrentAgents = n
}
case "ALLOW_IN_MEMORY":
cfg.AllowInMemory = val == "true" || val == "1"
case "CORS_ALLOWED_ORIGINS":
cfg.CORSAllowedOrigins = parseCSVOrigins(val)
case "VAULT_STATE_ROOT":
cfg.VaultStateRoot = val
case "VAULT_STORAGE_IDENTITY":
cfg.VaultStorageIdentity = val
case "VAULT_MASTER_KEY":
cfg.VaultMasterKey = val
case "FAV_DEGRADED_READ":
cfg.FAVDegradedRead = val == "true" || val == "1"
case "FAV_PUBLISHER_ENABLED":
cfg.FAVPublisherEnabled = val == "true" || val == "1"
}
}
}
// 允许系统环境变量覆盖
if v := os.Getenv("ENV"); v != "" {
cfg.Env = v
}
if v := os.Getenv("DATA_DIR"); v != "" {
cfg.DataDir = v
}
if v := os.Getenv("HTTP_HOST"); v != "" {
cfg.HTTPHost = v
} else if v := os.Getenv("APP_PUBLISH_HOST"); v != "" {
cfg.HTTPHost = v
}
if v := os.Getenv("HTTP_PORT"); v != "" {
if p, err := strconv.Atoi(v); err == nil {
cfg.HTTPPort = p
}
}
if v := os.Getenv("AGENT_BACKEND"); v != "" {
cfg.AgentBackend = v
}
if v := os.Getenv("AGENT_TIMEOUT"); v != "" {
if d, err := time.ParseDuration(v); err == nil {
cfg.AgentTimeout = d
}
}
if v := os.Getenv("CURSOR_API_KEY"); v != "" {
cfg.CursorAPIKey = v
}
if v := os.Getenv("CURSOR_MODEL"); v != "" {
cfg.CursorModel = v
}
if v := os.Getenv("CURSOR_BIN"); v != "" {
cfg.CursorBin = v
}
if v := os.Getenv("CURSOR_BRIDGE_BIN"); v != "" {
cfg.CursorBridgeBin = v
}
if v := os.Getenv("PI_AGENT_BIN"); v != "" {
cfg.PiAgentBin = v
}
if v := os.Getenv("PI_AGENT_BRIDGE_BIN"); v != "" {
cfg.PiAgentBridgeBin = v
}
if v := os.Getenv("PI_AGENT_PROVIDER"); v != "" {
cfg.PiAgentProvider = v
}
if v := os.Getenv("PI_AGENT_MODEL"); v != "" {
cfg.PiAgentModel = v
}
if v := os.Getenv("KB_DB_HOST"); v != "" {
cfg.KbDbHost = v
}
if v := os.Getenv("KB_DB_PORT"); v != "" {
if p, err := strconv.Atoi(v); err == nil {
cfg.KbDbPort = p
}
}
if v := os.Getenv("KB_DB_USER"); v != "" {
cfg.KbDbUser = v
}
if v := os.Getenv("KB_DB_PASSWORD"); v != "" {
cfg.KbDbPassword = v
}
if v := os.Getenv("KB_DB_NAME"); v != "" {
cfg.KbDbName = v
}
if v := os.Getenv("CONTEXT_DB_HOST"); v != "" {
cfg.ContextDbHost = v
}
if v := os.Getenv("CONTEXT_DB_PORT"); v != "" {
if p, err := strconv.Atoi(v); err == nil {
cfg.ContextDbPort = p
}
}
if v := os.Getenv("CONTEXT_DB_USER"); v != "" {
cfg.ContextDbUser = v
}
if v := os.Getenv("CONTEXT_DB_PASSWORD"); v != "" {
cfg.ContextDbPassword = v
}
if v := os.Getenv("CONTEXT_DB_NAME"); v != "" {
cfg.ContextDbName = v
}
if v := os.Getenv("EMBEDDING_ENDPOINT_URL"); v != "" {
cfg.EmbeddingEndpointURL = v
}
if v := os.Getenv("EMBEDDING_API_KEY"); v != "" {
cfg.EmbeddingAPIKey = v
}
if v := os.Getenv("EMBEDDING_MODEL"); v != "" {
cfg.EmbeddingModel = v
}
if v := os.Getenv("RERANKER_ENDPOINT_URL"); v != "" {
cfg.RerankerEndpointURL = v
}
if v := os.Getenv("RERANKER_API_KEY"); v != "" {
cfg.RerankerAPIKey = v
}
if v := os.Getenv("RERANKER_MODEL"); v != "" {
cfg.RerankerModel = v
}
if v := os.Getenv("ADMIN_USERNAME"); v != "" {
cfg.AdminUsername = v
}
if v := os.Getenv("ADMIN_PASSWORD"); v != "" {
cfg.AdminPassword = v
}
if v := os.Getenv("AUTH_REGISTRATION_ENABLED"); v != "" {
cfg.AuthRegistrationEnabled = v == "true" || v == "1"
}
if v := os.Getenv("AUTH_REQUIRE_APPROVAL"); v != "" {
cfg.AuthRequireApproval = v == "true" || v == "1"
}
if v := os.Getenv("MAX_CONCURRENT_AGENTS"); v != "" {
if n, err := strconv.Atoi(v); err == nil && n > 0 {
cfg.MaxConcurrentAgents = n
}
}
if v := os.Getenv("PI_AGENT_RUNNER_URL"); v != "" {
cfg.PiAgentRunnerURL = v
}
if v := os.Getenv("PI_AGENT_RUNNER_AUTH_TOKEN"); v != "" {
cfg.PiAgentRunnerAuthToken = v
}
if v := os.Getenv("RUNNER_AUTH_TOKEN"); v != "" {
cfg.PiAgentRunnerAuthToken = v
}
if v := os.Getenv("ALLOW_IN_MEMORY"); v != "" {
cfg.AllowInMemory = v == "true" || v == "1"
}
if v := os.Getenv("CORS_ALLOWED_ORIGINS"); v != "" {
cfg.CORSAllowedOrigins = parseCSVOrigins(v)
}
if v := os.Getenv("VAULT_STATE_ROOT"); v != "" {
cfg.VaultStateRoot = v
}
if v := os.Getenv("VAULT_STORAGE_IDENTITY"); v != "" {
cfg.VaultStorageIdentity = v
}
if v := os.Getenv("VAULT_MASTER_KEY"); v != "" {
cfg.VaultMasterKey = v
}
if v := os.Getenv("FAV_DEGRADED_READ"); v != "" {
cfg.FAVDegradedRead = v == "true" || v == "1"
}
if v := os.Getenv("FAV_PUBLISHER_ENABLED"); v != "" {
cfg.FAVPublisherEnabled = v == "true" || v == "1"
}
if v := os.Getenv("HTTP_ADDR"); v != "" && strings.HasPrefix(v, ":") {
if p, err := strconv.Atoi(strings.TrimPrefix(v, ":")); err == nil {
cfg.HTTPPort = p
cfg.HTTPHost = "0.0.0.0"
}
}
applyDSNOverrides(cfg)
applyCORSDefaults(cfg)
if err := validateProductionSecrets(cfg); err != nil {
return nil, err
}
if err := ValidateIndependentVault(cfg.VaultStateRoot, cfg.DataDir); err != nil {
return nil, err
}
return cfg, nil
}
// ValidateIndependentVault refuses vault roots that collide with runtime data/{env}.
// Empty root is allowed (FAV stays unconfigured / disabled).
func ValidateIndependentVault(root, dataDir string) error {
if strings.TrimSpace(root) == "" {
return nil
}
cr := filepath.Clean(root)
for _, env := range []string{"dev", "test", "main"} {
if strings.HasSuffix(cr, filepath.Join("data", env)) {
return fmt.Errorf("config: VAULT_STATE_ROOT must not be runtime data root %s", root)
}
}
if dataDir != "" {
cd := filepath.Clean(dataDir)
if cr == cd {
return fmt.Errorf("config: VAULT_STATE_ROOT must be independent from DATA_DIR")
}
}
return nil
}
func parseCSVOrigins(raw string) []string {
parts := strings.Split(raw, ",")
out := make([]string, 0, len(parts))
for _, p := range parts {
p = strings.TrimSpace(p)
if p != "" {
out = append(out, p)
}
}
return out
}
func applyCORSDefaults(cfg *Config) {
if cfg == nil || len(cfg.CORSAllowedOrigins) > 0 {
return
}
if cfg.Env == "dev" || cfg.Env == "test" {
cfg.CORSAllowedOrigins = []string{
"http://localhost:5173",
"http://127.0.0.1:5173",
"http://localhost:8080",
"http://127.0.0.1:8080",
"http://localhost:8081",
"http://127.0.0.1:8081",
}
}
}
func validateProductionSecrets(cfg *Config) error {
if cfg == nil || cfg.Env == "" || cfg.Env == "dev" || cfg.Env == "test" {
return nil
}
weak := map[string]string{
"KB_DB_PASSWORD": cfg.KbDbPassword,
"CONTEXT_DB_PASSWORD": cfg.ContextDbPassword,
"ADMIN_PASSWORD": cfg.AdminPassword,
}
defaults := []string{"shared_kb_secret_pass", "dev_secret_db_pass", "prod_highly_secure_db_pass", "admin-secret-password-123"}
for envKey, val := range weak {
if strings.TrimSpace(val) == "" {
return fmt.Errorf("%s must be set in %s environment", envKey, cfg.Env)
}
for _, d := range defaults {
if val == d {
return fmt.Errorf("%s uses insecure default value in %s environment", envKey, cfg.Env)
}
}
}
if cfg.AllowInMemory {
return fmt.Errorf("ALLOW_IN_MEMORY must be false in %s environment", cfg.Env)
}
if strings.TrimSpace(cfg.PiAgentRunnerURL) != "" && strings.TrimSpace(cfg.PiAgentRunnerAuthToken) == "" {
return fmt.Errorf("PI_AGENT_RUNNER_AUTH_TOKEN (or RUNNER_AUTH_TOKEN) must be set when PI_AGENT_RUNNER_URL is configured in %s environment", cfg.Env)
}
return nil
}
func applyDSNOverrides(cfg *Config) {
if dsn := os.Getenv("APP_DB_DSN"); dsn != "" {
if host, port, user, password, name, ok := parsePostgresDSN(dsn); ok {
cfg.ContextDbHost = host
cfg.ContextDbPort = port
cfg.ContextDbUser = user
cfg.ContextDbPassword = password
cfg.ContextDbName = name
}
}
if dsn := os.Getenv("POSTGRES_SHARED_KB_DSN"); dsn != "" {
if host, port, user, password, name, ok := parsePostgresDSN(dsn); ok {
cfg.KbDbHost = host
cfg.KbDbPort = port
cfg.KbDbUser = user
cfg.KbDbPassword = password
cfg.KbDbName = name
}
}
}
func parsePostgresDSN(dsn string) (host string, port int, user, password, dbName string, ok bool) {
if !strings.HasPrefix(dsn, "postgres://") {
return "", 0, "", "", "", false
}
rest := strings.TrimPrefix(dsn, "postgres://")
at := strings.LastIndex(rest, "@")
if at <= 0 {
return "", 0, "", "", "", false
}
creds := rest[:at]
hostPart := rest[at+1:]
slash := strings.Index(hostPart, "/")
if slash <= 0 {
return "", 0, "", "", "", false
}
hostPort := hostPart[:slash]
dbName = strings.SplitN(hostPart[slash+1:], "?", 2)[0]
userPass := strings.SplitN(creds, ":", 2)
if len(userPass) != 2 {
return "", 0, "", "", "", false
}
user = userPass[0]
password = userPass[1]
hostPortParts := strings.SplitN(hostPort, ":", 2)
host = hostPortParts[0]
port = 5432
if len(hostPortParts) == 2 {
if p, err := strconv.Atoi(hostPortParts[1]); err == nil {
port = p
}
}
return host, port, user, password, dbName, true
}
// KbDSN 返回 PostgreSQL 连接字符串
func (c *Config) KbDSN() string {
return "postgres://" + c.KbDbUser + ":" + c.KbDbPassword + "@" + c.KbDbHost + ":" + strconv.Itoa(c.KbDbPort) + "/" + c.KbDbName + "?sslmode=disable"
}
// ContextDSN 返回专属记忆与多级上下文 PostgreSQL 连接字符串
func (c *Config) ContextDSN() string {
return "postgres://" + c.ContextDbUser + ":" + c.ContextDbPassword + "@" + c.ContextDbHost + ":" + strconv.Itoa(c.ContextDbPort) + "/" + c.ContextDbName + "?sslmode=disable"
}