Merge pull request 'Warn when a seeded model key does not become active' (#569) from fix/model-key-secret-overlay into main
Why: seeding a new model key left the old secret row in force.
Evidence: CI run 3341 jobs test, test-pg, and svc-realcombo succeeded on 34152f740f. Backend #205 is 11c337d3.
Impact: seed warns on fingerprint mismatch; backend gitlink keeps the adoption fix.
Refs: fix/model-key-secret-overlay
This commit was merged in pull request #569.
This commit is contained in:
File diff suppressed because one or more lines are too long
@@ -144,6 +144,9 @@ scripts/svc.sh <name> deploy [--kind agent|static] [--from 源] [--tag T]... [
|
||||
|
||||
**agent**:来源见 [§5](#5-命令参考部署来源);启动参数组装见 [§9 配置系统](#9-配置系统);
|
||||
健康后按 `--model-*` / `--light-*` / `--tavily-keys` 播种(幂等,设置存在 agent 数据目录)。
|
||||
密钥表 `secret_keys` 的生效行优先于设置明文;换 key 后以 `/api/secrets` 为准。
|
||||
当前后端在 `PUT /api/settings/system` 收到非掩码密钥时,会把它写成系统行的生效密钥。
|
||||
嵌入式库中生效密钥与环境变量指纹不一致时,播种后告警,不打印明文。
|
||||
|
||||
**static**:
|
||||
|
||||
|
||||
+1
-1
Submodule modules/backend updated: 7d6490feee...24bbb7b777
@@ -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() {
|
||||
|
||||
+80
-5
@@ -58,9 +58,13 @@ case "${1:-}" in
|
||||
[ -n "${AGENT_SYSTEM_DSN:-}" ] && printf 'AGENT_SYSTEM_DSN=%s\n' "$AGENT_SYSTEM_DSN"
|
||||
[ -n "${AGENT_SYSTEM_AUTH:-}" ] && printf 'AGENT_SYSTEM_AUTH=%s\n' "$AGENT_SYSTEM_AUTH"
|
||||
} > "$trace_dir/serve.txt"
|
||||
addr=""
|
||||
addr="" data=""
|
||||
while [ $# -gt 0 ]; do
|
||||
case "$1" in --addr) addr="$2"; shift 2 ;; *) shift ;; esac
|
||||
case "$1" in
|
||||
--addr) addr="$2"; shift 2 ;;
|
||||
--data) data="$2"; shift 2 ;;
|
||||
*) shift ;;
|
||||
esac
|
||||
done
|
||||
port="${addr##*:}"; host="${addr%:*}"
|
||||
trap 'kill "$child" 2>/dev/null; exit 0' TERM INT
|
||||
@@ -68,10 +72,10 @@ case "${1:-}" in
|
||||
# 请求体到 trace/put.txt 供播种断言(settings 播种验证,2026-10-11)。
|
||||
# HTTP 子进程轮询 ppid:父进程被 SIGKILL 后自行退出,避免与 recover 套件
|
||||
# 共用 1880x 端口时泄漏占用(与 test_svc_recover 桩同规)。
|
||||
python3 - "$port" "$host" "$trace_dir" <<'PYSRV' >/dev/null 2>&1 &
|
||||
import os, sys, threading, time
|
||||
python3 - "$port" "$host" "$trace_dir" "$data" <<'PYSRV' >/dev/null 2>&1 &
|
||||
import json, os, sqlite3, sys, threading, time
|
||||
from http.server import BaseHTTPRequestHandler, HTTPServer
|
||||
port, host, trace = int(sys.argv[1]), sys.argv[2], sys.argv[3]
|
||||
port, host, trace, data = int(sys.argv[1]), sys.argv[2], sys.argv[3], sys.argv[4]
|
||||
class H(BaseHTTPRequestHandler):
|
||||
def _ok(self, body=b'{"ok":true}'):
|
||||
self.send_response(200)
|
||||
@@ -88,7 +92,35 @@ class H(BaseHTTPRequestHandler):
|
||||
body = self.rfile.read(n).decode('utf-8', 'replace')
|
||||
with open(trace + '/put.txt', 'a') as f:
|
||||
f.write('AUTH=%s\nBODY=%s\n--\n' % (self.headers.get('Authorization') or '-', body))
|
||||
self._adopt(body)
|
||||
self._ok()
|
||||
def _adopt(self, body):
|
||||
# 模拟当前后端:系统设置里的非掩码主模型 key 成为系统行生效密钥。
|
||||
if os.environ.get('SVC_STUB_KEEP_OLD_SECRET') == '1':
|
||||
return
|
||||
db = os.path.join(data, 'agent-system.db')
|
||||
if not db or not os.path.exists(db):
|
||||
return
|
||||
try:
|
||||
incoming = json.loads(body)
|
||||
chat = incoming.get('models', {}).get('chat', {}).get('apiKey')
|
||||
except Exception:
|
||||
return
|
||||
if not chat:
|
||||
return
|
||||
con = sqlite3.connect(db, timeout=5)
|
||||
try:
|
||||
tables = {row[0] for row in con.execute("SELECT name FROM sqlite_master WHERE type='table'")}
|
||||
if 'secret_keys' not in tables:
|
||||
return
|
||||
con.execute("UPDATE secret_keys SET active=0 WHERE owner='' AND service='models.chat'")
|
||||
con.execute(
|
||||
"INSERT INTO secret_keys(id,owner,service,label,value,active,created_at) VALUES(?,?,?,?,?,1,?)",
|
||||
('key_seeded', '', 'models.chat', '播种', chat, '2026-10-15T00:00:00Z'),
|
||||
)
|
||||
con.commit()
|
||||
finally:
|
||||
con.close()
|
||||
def log_message(self, *args):
|
||||
pass
|
||||
ppid = os.getppid()
|
||||
@@ -759,6 +791,49 @@ class SvcDbRecoveryTests(unittest.TestCase):
|
||||
self.assertIn('"model": "stub-model"', put.split("BODY=", 1)[1])
|
||||
self.assertNotIn("apikey", self.cli_record()) # 认证关闭不铸 key
|
||||
|
||||
def plant_old_chat_secret(self, name, value):
|
||||
import sqlite3
|
||||
db = self.runtime(name) / "data" / "agent-system.db"
|
||||
con = sqlite3.connect(db)
|
||||
con.execute(
|
||||
"CREATE TABLE secret_keys(id TEXT, owner TEXT, service TEXT, label TEXT, value TEXT, active INTEGER, created_at TEXT)")
|
||||
con.execute(
|
||||
"INSERT INTO secret_keys VALUES('key_old','','models.chat','迁入',?,1,'2026-10-11T00:00:00Z')",
|
||||
(value,))
|
||||
con.commit()
|
||||
con.close()
|
||||
return db
|
||||
|
||||
def test_seed_replaces_old_system_secret(self):
|
||||
d = self.runtime("dev")
|
||||
self.seed_agent("dev", 18836,
|
||||
"--addr 127.0.0.1:18836 --data %s --plugins %s --auth auto"
|
||||
% (d / "data", d / "plugins"))
|
||||
db = self.plant_old_chat_secret("dev", "sk-old-secret")
|
||||
result = self.svc("dev", "start", env=self.model_env())
|
||||
self.assertIn("已播种设置:主模型", result.stdout)
|
||||
self.assertNotIn("生效密钥与环境变量不一致", result.stdout)
|
||||
self.assertNotIn("sk-old-secret", result.stdout)
|
||||
self.assertNotIn("sk-stub-model", result.stdout)
|
||||
import sqlite3
|
||||
active = sqlite3.connect(db).execute(
|
||||
"SELECT value FROM secret_keys WHERE owner='' AND service='models.chat' AND active=1"
|
||||
).fetchone()
|
||||
self.assertEqual(active[0], "sk-stub-model")
|
||||
|
||||
def test_seed_warns_when_old_secret_stays_active(self):
|
||||
d = self.runtime("dev")
|
||||
self.seed_agent("dev", 18837,
|
||||
"--addr 127.0.0.1:18837 --data %s --plugins %s --auth auto"
|
||||
% (d / "data", d / "plugins"))
|
||||
self.plant_old_chat_secret("dev", "sk-old-secret")
|
||||
result = self.svc("dev", "start", env=self.model_env(SVC_STUB_KEEP_OLD_SECRET="1"))
|
||||
self.assertIn("已播种设置:主模型", result.stdout)
|
||||
self.assertIn("生效密钥与环境变量不一致", result.stdout)
|
||||
self.assertIn("models.chat fp=", result.stdout)
|
||||
self.assertNotIn("sk-old-secret", result.stdout)
|
||||
self.assertNotIn("sk-stub-model", result.stdout)
|
||||
|
||||
|
||||
# ---- 13. rdm 部署源(下游组合仓 Agent-System-RDM):物化/产物/工作台/播种跳过 ----
|
||||
|
||||
|
||||
Reference in New Issue
Block a user