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