diff --git a/pipeline_core/agent_config.py b/pipeline_core/agent_config.py index 62ac889..b16e531 100644 --- a/pipeline_core/agent_config.py +++ b/pipeline_core/agent_config.py @@ -449,6 +449,57 @@ GENERAL_TOOLS = [ required=["table"], category="data", ), + # ── 用户敏感信息库(2026-09-17):凭据密文入库 + 占位符引用 + 入站自动侦测 ── + # 实现在 pipeline_service.secret_vault(handler 签名 (sor, params, ctx))。 + # 安全不变量:agent 全程只见元数据与占位符,真值只在执行边界以子进程环境变量注入。 + ToolDefinition( + name="list_secrets", + description="列出当前用户/机构可用的敏感信息(apikey/token/密码/私钥)。只返回元数据(名称/类型/前缀/长度/使用次数),绝不返回明文值。需要用凭据执行命令前先调这个查有哪些", + parameters={"all": "可选:传 1 时含已停用条目"}, + required=[], + category="secret", + ), + ToolDefinition( + name="save_secret", + description="把一份敏感信息密文入库。用户说「把 X 存为 Y」「保存这个 apikey/token/密码」时调用;用户直接在消息里贴明文时平台入站门禁会自动侦测入库,无需你再调。入库后返回占位符 @@sec:NAME@@ 与环境变量名 $PIPELINE_SEC_NAME", + parameters={ + "value": "明文值(仅本次调用使用,落库即 RC4 加密)", + "name": "可选:变量名(大写字母开头,只含大写字母/数字/下划线;缺省按类型自动派生)", + "label": "可选:显示名称", + "secret_type": "可选:类型(缺省自动识别,如 github_token/api_key/db_password)", + "remark": "可选:备注(用途说明)", + }, + required=["value"], + category="secret", + ), + ToolDefinition( + name="use_secret", + description="从敏感信息库选一个插入当前会话(返回占位符 @@sec:NAME@@ 与环境变量名,不返回明文)。用户说「用我的 github token」「插入那个 apikey」时调用", + parameters={"name": "变量名(先用 list_secrets 查可用名称)"}, + required=["name"], + category="secret", + ), + ToolDefinition( + name="delete_secret", + description="删除一条敏感信息(仅本人/本机构条目)", + parameters={"name": "变量名", "id": "可选:记录ID(与 name 二选一)"}, + required=[], + category="secret", + ), + ToolDefinition( + name="set_secret_status", + description="启用/停用一条敏感信息。停用后不再注入执行环境但保留记录(凭据泄露疑似时先停用)", + parameters={"name": "变量名", "status": "active(启用)或 disabled(停用)"}, + required=["name", "status"], + category="secret", + ), + ToolDefinition( + name="detect_secret", + description="只侦测文本中的疑似敏感信息但不入库(返回前4字符+类型+置信度+熵)。用户问「这段有没有泄露风险」时调用", + parameters={"text": "待检测文本"}, + required=["text"], + category="secret", + ), ] diff --git a/wwwroot/agent/index.ui b/wwwroot/agent/index.ui index 364ec60..b4aa399 100644 --- a/wwwroot/agent/index.ui +++ b/wwwroot/agent/index.ui @@ -18,7 +18,7 @@ "event": "click", "actiontype": "script", "target": "self", - "script": "var tp=bricks.getWidgetById('session_tabs',bricks.app);if(!tp)return;var sid='s'+Date.now()+'_'+Math.floor(Math.random()*100000);var n=tp.opts.items.length+1;tp.open_tab({name:'session_'+sid,label:'会话 '+n,removable:true,content:{widgettype:'VBox',options:{css:'filler',width:'100%',height:'100%'},subwidgets:[{widgettype:'urlwidget',options:{url:'/pipeline_core/api/agent_menus.dspy?session_id='+sid+'&pipeline_id=sdlc_general',method:'GET'}},{widgettype:'AgentIO',options:{css:'filler',margin:'0 24px 24px 24px',url:'/pipeline_core/api/agent_chat.dspy?session_id='+sid+'&pipeline_id=sdlc_general',model_dataurl:'/pipeline_core/api/agent_model_options.dspy?session_id='+sid+'&pipeline_id=sdlc_general',model_cwidth:14,placeholder:'输入你的需求...'}}]}});" + "script": "var tp=bricks.getWidgetById('session_tabs',bricks.app);if(!tp)return;var sid='s'+Date.now()+'_'+Math.floor(Math.random()*100000);var n=tp.opts.items.length+1;tp.open_tab({name:'session_'+sid,label:'会话 '+n,removable:true,content:{widgettype:'VBox',options:{css:'filler',width:'100%',height:'100%'},subwidgets:[{widgettype:'urlwidget',options:{url:'/pipeline_core/api/agent_menus.dspy?session_id='+sid+'&pipeline_id=sdlc_general',method:'GET'}},{widgettype:'AgentIO',options:{css:'filler',margin:'0 24px 24px 24px',url:'/pipeline_core/api/agent_chat.dspy?session_id='+sid+'&pipeline_id=sdlc_general',model_dataurl:'/pipeline_core/api/agent_model_options.dspy?session_id='+sid+'&pipeline_id=sdlc_general',model_cwidth:14,placeholder:'输入你的需求...',secret_dataurl:'/pipeline_core/api/secret_options.dspy',secret_format:'@@sec:{name}@@',secret_title:'选择敏感信息',secret_tip:'插入敏感信息'}}]}});" }] } ] @@ -52,7 +52,11 @@ "url": "/pipeline_core/api/agent_chat.dspy?session_id=default_sdlc_general&pipeline_id=sdlc_general", "model_dataurl": "/pipeline_core/api/agent_model_options.dspy?session_id=default_sdlc_general&pipeline_id=sdlc_general", "model_cwidth": 14, - "placeholder": "输入你的需求..." + "placeholder": "输入你的需求...", + "secret_dataurl": "/pipeline_core/api/secret_options.dspy", + "secret_format": "@@sec:{name}@@", + "secret_title": "选择敏感信息", + "secret_tip": "插入敏感信息" } } ] diff --git a/wwwroot/agent_generic/index.ui b/wwwroot/agent_generic/index.ui index 3919a17..c670c3f 100644 --- a/wwwroot/agent_generic/index.ui +++ b/wwwroot/agent_generic/index.ui @@ -31,7 +31,11 @@ "url": "/pipeline_core/api/agent_chat_generic.dspy", "model_dataurl": "/pipeline_core/api/agent_model_options.dspy", "model_cwidth": 14, - "placeholder": "输入你的需求...(纯通用模式)" + "placeholder": "输入你的需求...(纯通用模式)", + "secret_dataurl": "/pipeline_core/api/secret_options.dspy", + "secret_format": "@@sec:{name}@@", + "secret_title": "选择敏感信息", + "secret_tip": "插入敏感信息" } } ] diff --git a/wwwroot/api/secret_options.dspy b/wwwroot/api/secret_options.dspy new file mode 100644 index 0000000..dddc815 --- /dev/null +++ b/wwwroot/api/secret_options.dspy @@ -0,0 +1,39 @@ +# secret_options.dspy — AgentIO「插入敏感信息」选择窗的数据源(2026-09-17) +# +# 前端:AgentInput 的 opts.secret_dataurl 指向此处(bricks 通用框架不硬编码宿主路径, +# 由 pipeline-core 的 agent/index.ui 注入)。 +# 返回:{"success":true, "secrets":[{name,label,secret_type,prefix_hint,length_hint, +# use_count,remark,status}, ...]} +# +# 安全铁律(与 secret_vault.list_secrets 同源): +# - **绝不返回 encrypted_value 或明文**。只给元数据:名称/类型/前缀4字符/长度/使用次数。 +# 前缀4字符够人类辨认是哪个凭据,拼不出可用值(GitHub PAT 前缀是公开固定值,零熵)。 +# - 身份取自登录态(get_user/get_userorgid),**不接受前端传 user_id/org_id**—— +# 否则任意用户改参数即可枚举他人凭据名称(元数据本身也是信息)。 +# - 未登录返回 success:false,不返回空列表(区分「没登录」与「没数据」,便于前端提示)。 + +import json + +uid = await get_user() +if not uid: + return json.dumps({"success": False, "error": "未登录"}, ensure_ascii=False) + +org_id = (await get_userorgid()) or '' +show_all = str((params_kw or {}).get('all', '') or '').strip() in ('1', 'true', 'True') + +from sqlor.dbpools import DBPools +from pipeline_service import secret_vault + +async with DBPools().sqlorContext('pipeline') as sor: + rows = await secret_vault.list_secrets(sor, org_id=org_id, user_id=uid, + only_active=not show_all) + await sor.sqlExe("COMMIT", {}) + +# 双保险:即使上游误带出密文字段,这里再剔一次(返回体是安全边界) +_safe = [] +for r in rows: + item = {k: v for k, v in r.items() + if k not in ('encrypted_value', 'fingerprint')} + _safe.append(item) + +return json.dumps({"success": True, "secrets": _safe}, ensure_ascii=False, default=str) diff --git a/wwwroot/mobile_agent.ui b/wwwroot/mobile_agent.ui index 2ace543..f87a461 100644 --- a/wwwroot/mobile_agent.ui +++ b/wwwroot/mobile_agent.ui @@ -17,7 +17,11 @@ "url": "/pipeline_core/api/agent_chat.dspy?pipeline_id={{pl_id}}&session_id=m_{{pl_id}}", "model_dataurl": "/pipeline_core/api/agent_model_options.dspy?capabilities=chat&pipeline_id={{pl_id}}&session_id=m_{{pl_id}}", "model_cwidth": 14, - "placeholder": "输入你的需求..." + "placeholder": "输入你的需求...", + "secret_dataurl": "/pipeline_core/api/secret_options.dspy", + "secret_format": "@@sec:{name}@@", + "secret_title": "选择敏感信息", + "secret_tip": "插入敏感信息" } } ] @@ -77,7 +81,11 @@ "url": "/pipeline_core/api/agent_chat.dspy", "model_dataurl": "/pipeline_core/api/agent_model_options.dspy?capabilities=chat", "model_cwidth": 14, - "placeholder": "输入你的需求..." + "placeholder": "输入你的需求...", + "secret_dataurl": "/pipeline_core/api/secret_options.dspy", + "secret_format": "@@sec:{name}@@", + "secret_title": "选择敏感信息", + "secret_tip": "插入敏感信息" } }, {