Warn when a seeded model key does not become active

Why: Settings plaintext loses to secret_keys, so replacing the env key did not change the key a conversation actually sent.
Evidence: python3 -m unittest tests.test_svc.SvcDbRecoveryTests.test_seed_replaces_old_system_secret tests.test_svc.SvcDbRecoveryTests.test_seed_warns_when_old_secret_stays_active tests.test_svc.SvcDbRecoveryTests.test_seed_puts_without_credentials_when_auth_off (SELF).
Impact: Embedded databases now compare the active system secret fingerprint after seeding, and the backend gitlink carries the adoption fix.
Refs: fix/model-key-secret-overlay
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-10-15 10:33:28 +00:00
co-authored by Cursor
parent e5994cd8ae
commit 265bcdcbba
5 changed files with 152 additions and 24 deletions
+45
View File
@@ -581,6 +581,9 @@ dotenv 格式:KEY=VALUE,支持 # 注释、成对引号、export 前缀;只
SVC_TAVILY_KEYS Tavily 密钥池
SVC_SEED_OWNER 播种目标账号(缺省取库内首个管理员)
播种幂等:设置持久化在 agent 数据目录,播过一次后续启动无需再传。
密钥表 secret_keys 的生效行优先于设置明文。换 key 后以 /api/secrets 为准;
播种写入系统设置时,当前后端会把非掩码密钥同步成系统行的生效密钥。
嵌入式库里生效密钥与环境变量指纹不一致时,启动会告警且不打印明文。
认证开启(PG 档默认)时播种自动铸临时密钥(name=svc-seed,用后即回收),
不依赖手动到设置页配置。
@@ -3564,11 +3567,53 @@ for u in users:
[ -n "${SVC_MINIMAX_BASE:-}${SVC_MINIMAX_KEY:-}${SVC_MINIMAX_CHAT:-}${SVC_MINIMAX_FAST:-}" ] && did="${did:+$did、}MiniMax 备用"
[ -n "${CLI_TAVILY:-${SVC_TAVILY_KEYS:-}}" ] && did="${did:+$did、}Tavily"
c_ok "[$ENV_NAME] 已播种设置:$did(持久化于 $ENV_DATA,后续启动无需再传)"
verify_seeded_secrets
else
c_warn "[$ENV_NAME] 设置播种未成功:${resp:-<无响应>}(认证开启时需库内已有账号以铸临时密钥,或用 --seed-owner 指定;也可在设置页手动配置)"
fi
}
# verify_seeded_secrets:嵌入式库里,系统行生效密钥要和环境变量是同一把。
# 只比较 SHA-256 前 8 位,不一致就告警。没有库、没有密钥表或还没有生效行时跳过。
verify_seeded_secrets() {
local db="${ENV_DATA:-}/agent-system.db"
[ -f "$db" ] || return 0
local chat="${CLI_MKEY:-${SVC_MODEL_KEY:-}}" light="${CLI_LKEY:-${SVC_LIGHT_KEY:-}}"
[ -n "$chat$light" ] || return 0
command -v python3 >/dev/null 2>&1 || return 0
local report
report="$(python3 - "$db" "$chat" "$light" <<'PY'
import hashlib, sqlite3, sys
db, chat, light = sys.argv[1:4]
con = sqlite3.connect("file:%s?mode=ro" % db, uri=True)
tables = {row[0] for row in con.execute("SELECT name FROM sqlite_master WHERE type='table'")}
if "secret_keys" not in tables:
raise SystemExit(0)
def active(service):
row = con.execute(
"SELECT value FROM secret_keys WHERE owner=? AND service=? AND active=1 ORDER BY created_at DESC LIMIT 1",
("", service),
).fetchone()
return row[0] if row else None
def fp(value):
return hashlib.sha256(value.encode()).hexdigest()[:8]
parts = []
for service, expect in (("models.chat", chat), ("models.lightweight", light)):
if not expect:
continue
got = active(service)
if got is None or fp(got) == fp(expect):
continue
parts.append("%s fp=%s expected=%s" % (service, fp(got), fp(expect)))
if parts:
print("; ".join(parts))
PY
)" || return 0
if [ -n "$report" ]; then
c_warn "[$ENV_NAME] 播种后的生效密钥与环境变量不一致:$report。密钥表优先于设置明文,换 key 后以 /api/secrets 为准"
fi
}
# seed_settings_db:rdm 产物不能铸 API key 时,把同一份配置合并进嵌入式系统设置。
# 库还不存在时不创建,避免在空数据目录里写出半套表。
seed_settings_db() {