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:
@@ -7,45 +7,50 @@
|
||||
- 权威仓:ssh://git@git-ssh.u1680832.nyat.app:31411/weichen/Agent-System-Dev.git;目标分支:main;R0(initial_base_sha)= 919f75cf7b39ca357e212a08a0ba5ba3d9a79b51
|
||||
- 工作区:/home/weichen/projects/Agent-System-Dev-wt-fix-model-key-secret-overlay(worktree);分支:fix/model-key-secret-overlay
|
||||
|
||||
- 授权与交付终点:<本次允许的动作与终点;未授权事项写明>
|
||||
- 交付模式:manual(前序人工决策,智能体 Review/合入);其他阶段明确授权后可记为 authorized-continuous
|
||||
- 授权与交付终点:修复 8788 对话 401。允许改后端密钥迁移/播种、401 诊断、svc 播种校验与文档;交付终点是后端 PR 与 Dev gitlink PR 合入 main。不改生产密钥,不清理 genui-smoke / u7-creator 的遗留密钥行。
|
||||
- 交付模式:authorized-continuous(用户要求按已确认方案连续实现到合入)
|
||||
- 合入执行:agent(已授权交付范围内,Review 通过、冲突解决且当前 HEAD 的 CI 通过后合入)
|
||||
- 自动推进授权:无;授权范围:无;授权来源:无;授权终点:无;停止条件:无(未授权)
|
||||
- 当前阶段:需求拆解;当前增量:I1(待定义)
|
||||
- 自动推进授权:有;授权范围:阶段 1–9,本任务卡所列需求与方案;授权来源:用户消息「Implement the plan as specified... Don't stop until you have completed all the to-dos」(2026-10-15);授权终点:后端与 Dev 的 PR 合入 main;停止条件:CI 失败、冲突无法安全解决、或需要改动方案外的密钥/生产数据
|
||||
- 当前阶段:实施与自检;当前增量:I1
|
||||
- 流程依据:仓库根目录 policy/delivery.md(归档后仍按仓库根目录定位)
|
||||
|
||||
## 目标与非目标
|
||||
|
||||
<预期结果、受影响入口/模块/契约;明确不做什么。>
|
||||
对话使用的模型密钥以 secret_keys 的生效行为准。环境变量播种的新 key 必须成为系统行生效密钥,不能被旧行盖住。401 要带出来源层级和脱敏指纹。
|
||||
|
||||
不做:不直接改 8788 的数据库文件;不删除其他账号的遗留密钥;不更换 .env.dev。
|
||||
|
||||
## 关联方向与规划
|
||||
|
||||
- 适用状态:待核对(无适用规划时说明原因)
|
||||
- 主记录路径/版本:待核对(路径按仓库根目录定位,归档后不变;只引用唯一正文)
|
||||
- 对应里程碑:待核对
|
||||
- 适用状态:无单独长期规划。这是 8788 实测 401 的修复,方案在本次任务卡。
|
||||
- 主记录路径/版本:本任务卡
|
||||
- 对应里程碑:不适用
|
||||
|
||||
## 需求分析
|
||||
|
||||
| ID | 描述 | 项目符合度及依据 | 优先级 | 验收条件 | 依赖 | 结论 |
|
||||
|---|---|---|---|---|---|---|
|
||||
| R1 | 待填写 | 待评估 | 待定 | 待定义 | 待确认 | 待评审 |
|
||||
| R1 | 已有生效密钥与设置明文不同时,迁移保留明文并告警 | 符合:secret_keys 优先于设置明文 | P0 | 旧行仍生效,明文仍在,日志含服务名且不含明文 | 无 | 纳入本增量 |
|
||||
| R2 | 系统设置写入非掩码密钥时,同步为系统行生效密钥;相同值不追加行 | 符合:PUT /api/settings/system 是播种入口 | P0 | 旧生效值被新值替换,label 为播种;再写同一值不增加行 | R1 | 纳入本增量 |
|
||||
| R3 | 401 的 run.failed 带密钥来源和脱敏指纹 | 符合:错误要能定位用了哪一层 key | P1 | 文本含 source 与 sha256 前 8 位,不含明文 | R2 | 纳入本增量 |
|
||||
| R4 | 播种后核对嵌入式库生效密钥指纹,并写明密钥表优先 | 符合:svc 播种是换 key 的运维入口 | P1 | 不一致时告警且不含明文;一致时旧行被替换 | R2 | 纳入本增量 |
|
||||
|
||||
## 方案与计划
|
||||
|
||||
- 方案引用:待填写(含对象版本)
|
||||
- 实施计划引用:待填写(含增量与需求 ID)
|
||||
- 测试计划引用:待填写(含需求 ID 与验收条件)
|
||||
- 方案:迁移按生效密钥比较;系统设置写入调用 AddSecret;401 附 source 与指纹;svc 只比较指纹。对象版本:后端 origin/main c7b4ad7 之上。
|
||||
- 实施计划:I1 后端 R1–R3,I2 Dev 仓 R4 与 gitlink。
|
||||
- 测试计划:settings 迁移/播种测试、runtime 401 指纹测试、tests/test_svc.py 两条播种用例。
|
||||
|
||||
## 阶段评审
|
||||
|
||||
| 阶段 | 对象版本 | 结论 | 评审人 | 时间 | 依据 |
|
||||
|---|---|---|---|---|---|
|
||||
| 需求拆解 | 待定 | 未评审 | — | — | — |
|
||||
| 需求评估 | 待定 | 未评审 | — | — | — |
|
||||
| 需求表确认 | 待定 | 未评审 | — | — | — |
|
||||
| 方案设计 | 待定 | 未评审 | — | — | — |
|
||||
| 实施计划 | 待定 | 未评审 | — | — | — |
|
||||
| 实施与自检 | 待定 | 未评审 | — | — | — |
|
||||
| 需求拆解 | 方案确认稿 | 通过 | 用户 | 2026-10-15 | 连续实现授权,范围即本任务卡 R1–R4 |
|
||||
| 需求评估 | 方案确认稿 | 通过 | 用户 | 2026-10-15 | 同上 |
|
||||
| 需求表确认 | 方案确认稿 | 通过 | 用户 | 2026-10-15 | 同上 |
|
||||
| 方案设计 | 方案确认稿 | 通过 | 用户 | 2026-10-15 | 同上 |
|
||||
| 实施计划 | 方案确认稿 | 通过 | 用户 | 2026-10-15 | 同上 |
|
||||
| 实施与自检 | 进行中 | 未评审 | — | — | 局部测试见证据表 |
|
||||
| 正式测试与验收 | 待定 | 未评审 | — | — | — |
|
||||
| 代码 Review | 待定 | 未评审 | — | — | — |
|
||||
| 合入 | 待定 | 未评审 | — | — | — |
|
||||
|
||||
@@ -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