Files
AgentLab/backend/app/capabilities/secrets.py
ThearchyHelios 941723b17c 端到端验收修复与指标出具:
- 身份与工具:登录页建第一个管理员(认人接口的 401 说明能不能建);搭建者有只读的人和组名录,选人给候选并核对名字;授权三行带标题;端点请求头名校验(空名、非法名存不进、发不出时说清原因);写入能力在工具页试运行一律演练、不发送,HTTP 状态照实说成败;写记忆不算只读、查询能力只能选同一端点上的只读能力;审批与遮罩的说法和实际规则一致;开登录时不要署名、导入和发布记登录的人;使用者不显示推演等只给搭建者的操作,403 按是否开登录说话;登录页只认一次人
- 流程与实例:图里用到的岗位(派给谁、超时升级给谁、发送确认派给谁)全部派生、没绑人的发布时报错;写外部系统的参数按能力声明的类型收拢后再算哈希和发送,收不成就不发;发起人的实例页只看这一步的状态,处理人才给待办卡;会签进度带登录核实和时间,升级后照实写规则;实例状态写流程的结论(已退回、未确认、等待核实写入结果),核实生效后不再挂失败原因;流程应用首页不派生最近封存运行的结果块,看板没数据时是安静的空态;样例⑩的金额带类型和下限、待办标题带金额,表单上下限前后端一起生效
- 看板与证据:定时任务模板加 ISO 周、上一 ISO 周、本月、上月;出处链穿过原样解析查询结果的整形节点,追不到原始数据的照实说停在哪里,样例⑨改用 cell();应用首页和版本历史都能做成应用;指标块单位不重复;版本列表带退役说明;定时任务页头跟着刷新;删视图要确认、新视图能起名;编辑器属性用中文名、术语带解释
- 指标出具(用户 2026-10-15 拍板):契约只写「指标来自」、上游没有报告撰写节点、成果里没有模型写的话时按指标出具——必需指标齐、指标输入追到原始数据、成果字段里的数回指得上才完整出具;受管级要求每个指标静态地追到原始数据(governed.metric_untraced);出具横幅和应用印章写明按指标
- 受管工作流降为已发布只给管理员(Q8 顺延,服务端同样拦);工作流接口带当前发布版的级别,画布上改过之后也认得出受管工作流
2026-10-15 07:15:37 +08:00

82 lines
4.3 KiB
Python
Raw Permalink 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.
"""「名字 → 密钥值」的字典(请求头、环境变量):存库加密、接口只回掩码、提交掩码值表示保持原值(A6 第 6 条)。
自定义工具的 headers、MCP 的 env 和 headers、端点各环境的 headers 都按这一套:界面拿到的永远是掩码,用户只改了别的
字段、原样提交回来时,掩码对上原值的那一项保持原值——不然每保存一次,密钥就被掩码本身覆盖掉。
"""
from __future__ import annotations
import re
from typing import Any
from app.core import crypto
#: HTTP 请求头名能用的字符(RFC 9110 的 token)
_HEADER_NAME = re.compile(r"^[!#$%&'*+\-.^_`|~0-9A-Za-z]+$")
def seal(values: dict[str, Any] | None, previous: dict[str, Any] | None = None) -> dict[str, str]:
"""提交上来的字典 → 存库的样子:值加密;正好等于原值掩码的那一项保持原值(原值是升级前的明文也一样加密)。"""
before = previous or {}
out: dict[str, str] = {}
for key, value in (values or {}).items():
text = "" if value is None else str(value)
old = before.get(key)
if old and text and text == crypto.mask(old):
text = old
out[str(key)] = crypto.encrypt(text) or ""
return out
def header_problem(values: dict[str, Any] | None, *, check_values: bool = True) -> str | None:
"""请求头(名字 → 明文值)哪里不合法,没问题交回 None。名字不能为空、只能是 HTTP 的 token:存成 {"": ""} 时,这一组
头的每个请求都在发出之前就失败,报出来的只是一句 LocalProtocolError。值不能换行、只能是可打印的英文字符(中文、全角
字符编不进请求头);值里写了 {{ 参数 }} 模板的(自定义工具)由调用方关掉值的检查。"""
for name, value in (values or {}).items():
key = str(name)
if not key.strip():
return "请求头名称不能为空"
if not _HEADER_NAME.match(key):
return f"请求头名称「{key}」不合法:只能用英文字母、数字和 - _ . 这类符号,不能有空格、冒号或中文"
if check_values and any(not (" " <= ch <= "~" or ch == "\t") for ch in str(value or "")):
return f"请求头「{key}」的值不合法:只能用英文字母、数字、空格和常用符号,不能换行,也不能用中文或全角字符"
return None
def masked(values: dict[str, Any] | None) -> dict[str, str]:
"""给接口返回的样子:每个值都换成掩码。"""
return {str(k): crypto.mask(v) if v else "" for k, v in (values or {}).items()}
def reveal(values: dict[str, Any] | None) -> dict[str, str]:
"""真正发请求、起进程时用的明文。密钥换过(解不开)的值给空串:带着密文去请求只会让对方回一句认证失败。"""
return {str(k): (crypto.decrypt(v) or "") if v else "" for k, v in (values or {}).items()}
def is_sealed(values: dict[str, Any] | None) -> bool:
"""每个非空值都已经是密文。启动时据此判断存量明文要不要加密一遍。"""
return all(not v or (isinstance(v, str) and v.startswith(crypto._PREFIX)) for v in (values or {}).values())
async def seal_existing() -> int:
"""启动时把存量的明文加密一遍:自定义工具的请求头、MCP 的 env 和请求头。encrypt 本身幂等(已经是密文的原样交回),
重复执行不改任何东西。交回改了几行。"""
from sqlalchemy import select
from app.db.base import SessionLocal
from app.db.models import CustomTool, McpServer
changed = 0
async with SessionLocal() as session:
for tool in (await session.execute(select(CustomTool))).scalars():
headers = (tool.config or {}).get("headers")
if isinstance(headers, dict) and not is_sealed(headers):
tool.config = {**(tool.config or {}), "headers": seal(headers)}
changed += 1
for server in (await session.execute(select(McpServer))).scalars():
if not is_sealed(server.env) or not is_sealed(server.headers):
server.env, server.headers = seal(server.env), seal(server.headers)
changed += 1
if changed:
await session.commit()
return changed