敏感信息入库 + 会话占位符引用 + 入站自动侦测 + 执行边界注入,真值全程不进
模型上下文/不落库/不进命令字符串(根治"用户贴apikey→每轮上行供应商"泄露面)。
核心 secret_vault.py:
- AES 密文入库(appPublic.aes + UTF-8包装 + AES1$版本前缀)。注:ahserver 的
password_encode 实测是 RC4 薄封装(globalEnv.py:61),非 AES,故直接用 aes 模块。
aes.py 第42/43行密文侧 iso-8859-1 必须保留(实测200组密文utf-8解码100%失败),
只在调用方对明文侧做 UTF-8→latin-1 字节保真包装(中文/emoji/私钥 9/9 往返一致)。
- 占位符 @@sec:NAME@@(避开 sqlor ${X}$ 与 ahserver ArgsConvert 同语法冲突,
也避开裸 $VAR 被 bash 就地展开)。
- 侦测三通道:已知前缀16种 / 关键词邻域+熵≥3.5 / 不做裸串侦测(防吞哈希数据)。
中文冒号「apikey:」支持;_JUNK_VALUE_RE 拦占位示例值;低熵弱口令不误吞。
- 执行边界:占位符→$PIPELINE_SEC_NAME,真值走 subprocess env=(不用 bwrap
--setenv——值进argv同机任意用户ps可读;env=落/proc/pid/environ仅属主可读)。
- 越权防护:get_secret_row(own_only=) 读写范围分离 + _assert_own 代码级二次校验
(防任意机构成员覆盖/删除机构共享凭据)。写操作显式 COMMIT(多worker)。
- 审计只记 name/type,禁记值。
三路接入:
- gateway._secret_inbound_gate: run_message Step 0.5(所有上下文解析之前),
progress 事件回显动作(体感不变但告知已自动保存)。
- agent_loop_v2: _t_secret_tool 六工具 + _t_run_command 执行边界(前台/后台分支
之前) + _t_process 出站擦洗 + prompt 铁律(仅有 user_id 时注入)。
- agent_loop(v1角色agent): 只读三件(list/use/detect,无人值守禁自主增删凭据);
身份=项目真人owner(resolve_project_owner,已处理 agent.pm 自动立项反查);
run_shell 执行边界 + 出站擦洗。
- _run_shell/bg_jobs.start_bg_job 加 secret_env 参数(无secret时env=None零改动)。
本地测试 124 断言全绿(含三个挂调实锤陷阱回归:子串碰撞/占位符别名劫持/
未知占位符吞空 + 跨文件工具名同步 + 越权防护)。表定义 m0037(pipeline-app)。
306 lines
22 KiB
Markdown
306 lines
22 KiB
Markdown
# pipeline_service
|
||
|
||
产线执行引擎 —— 任务调度、步骤执行、人工任务交互、LLM 桥接。
|
||
|
||
## 功能
|
||
|
||
- **任务执行**:DAG 步骤调度与状态机
|
||
- **人工任务**:审批/输入等待与交互
|
||
- **LLM 桥接**:统一 LLM 调用接口
|
||
- **Agent Loop**:AI Agent 多轮任务执行
|
||
- **意图分类**:自然语言意图识别
|
||
- **产物管理**:步骤输入输出存储
|
||
|
||
## 数据表
|
||
|
||
| 表 | 说明 |
|
||
|---|------|
|
||
| pipeline_tasks | 任务实例 |
|
||
| pipeline_task_steps | 步骤执行记录 |
|
||
| pipeline_artifacts | 步骤产物 |
|
||
| pipeline_human_tasks | 人工任务 |
|
||
| pipeline_step_types | 步骤类型注册 |
|
||
| pipeline_session_settings | 会话设置(多 tab 项目上下文) |
|
||
| pipeline_conversations | 平台会话消息(回放缓存;2026-09-10 归籍本模块——唯一写入方 agent_loop_v2._save_turn,此前是无 models 定义的孤儿表) |
|
||
| pipeline_agent_instances | 角色 agent 实例注册(项目×角色→稳定实例 id;唯一写入方 agent_instance.py;m0002 建表未进 models,2026-09-10 归籍) |
|
||
| pipeline_agent_settings | 用户 agent 偏好(每用户一行 uk_user:默认模型/当前项目指针;写入方 agent_loop_v2、set_agent_model.dspy、workspace.py;2026-09-10 归籍) |
|
||
| pipeline_project_agents | 项目角色 agent 配置(驾驶舱 v1 start_agent 链路;v2 已走 pipeline_agent_instances,保留至 v1 链路删除决议;2026-09-10 归籍) |
|
||
| pipeline_user_secrets | 用户敏感信息库(2026-09-17,m0037 建表):AES 密文 + 指纹去重 + org/user 双维归属;写入方 secret_vault.py(agent 工具 + gateway 入站自动侦测);唯一索引 (org_id,user_id,name) |
|
||
|
||
## 安装
|
||
|
||
```bash
|
||
cd pkgs/pipeline-service && pip install .
|
||
```
|
||
|
||
## 核心模块
|
||
|
||
| 文件 | 职责 |
|
||
|------|------|
|
||
| `executor.py` | 任务/步骤调度引擎 |
|
||
| `storage.py` | 数据库读写 |
|
||
| `llm_bridge.py` | LLM API 调用 |
|
||
| `agent_loop.py` | AI Agent 多轮执行 |
|
||
| `human.py` | 人工任务处理 |
|
||
| `intent_classifier.py` | 意图识别 |
|
||
| `state.py` | 状态机 |
|
||
| `step_registry.py` | 步骤类型注册表 |
|
||
| `agent_loop_v2.py` | Agent Executor v2(会话 agent 执行引擎) |
|
||
| `workspace.py` | 项目/通用会话工作空间目录管理 |
|
||
| `sdlc_ability.py` | SDLC 产线能力包(工具定义 + handler) |
|
||
| `skill_live.py` | 技能实时发布/管理(publish_skill_live + manage_skill_live 多租户隔离) |
|
||
| `bg_jobs.py` | run_command 后台任务(状态文件化 .bg/,跨 worker 可见) |
|
||
| `subagents.py` | 后台并行子 agent 委派(.sub/,spawn/steer/stop/result) |
|
||
| `diagram_gate.py` | 文档配图形态硬门禁(2026-09-15):交付文档/标书章节中的图必须 invoke_model t2i/i2i 真图,确定性检测 mermaid/plantuml/ASCII 伪图;deliver 双路径拦截 + pipeline-bidding write/review_chapter 复用;豁免=文中如实标注「配图缺失:平台无可用文生图模型」 |
|
||
| `secret_vault.py` | 用户敏感信息库(2026-09-17):AES 密文入库 + 会话占位符引用 `@@sec:NAME@@` + 入站自动侦测入库;真值只在执行边界以子进程 env 注入,永不进模型上下文/落库/进命令字符串;gateway 入站门禁 + v1 角色 agent(项目 owner 身份)+ v2 会话 agent 三路接入 |
|
||
|
||
## 通用会话产线隔离(2026-09-05,7 层纵深防御)
|
||
|
||
规范:通用会话(generic=True,agent_chat_generic.dspy)= 纯通用 agent,
|
||
不挂任何产线工具/技能/角色/记忆,不得看到或操作任何产线的项目。
|
||
|
||
隔离层(缺一层即泄漏,详见技能 cockpit-agent-patterns):
|
||
|
||
1. **工具注册表**:ToolRegistry 每 executor 独立实例(禁全局单例,防跨会话残留)
|
||
2. **默认产线回退**:generic 会话 pipeline_id 空不回退 DEFAULT_ABILITY_ID
|
||
3. **产线能力包**:_execute_ability_tool 对 generic 直接拒绝
|
||
4. **工具 schema**:load_agent_config(pipeline-core)generic 时剔除 project/shell 类工具
|
||
5. **执行层兜底**:_dispatch_sdlc_tool 对 generic 拒绝项目管理工具(别名归一后拦截)
|
||
6. **工作空间**:generic 会话文件根 = `_general/{user_id}` 专属目录(generic_workspace_dir),
|
||
_resolve_ws_path 越界保护圈死;run_command 剔除(shell 无法圈禁);
|
||
resolve_upload_dir(pipeline-core)generic=True 上传落同一专属目录
|
||
7. **历史回放**:_load_history / _t_session_search 对 generic 只读写
|
||
`pipeline_id='' OR IS NULL` 的自身历史(与存库端 store_pl='' 对称),
|
||
禁止按 created_by 全量回放(会带出用户各产线对话中的项目信息)
|
||
|
||
## Hermes 能力对齐:写入侧 + 运行时侧(2026-09-10)
|
||
|
||
四个 handler 在 `agent_loop_v2.py`,工具定义在 pipeline-core `agent_config.py`
|
||
(GENERAL_TOOLS)。全部经测试机服务端直调 31/31 + HTTP 端到端 5/5 实测通过。
|
||
|
||
### 1. memory 工具(持久记忆写入,`_t_memory`)
|
||
- 写入门禁(代码强制,多机构多用户):scope 白名单 user/project/pipeline,
|
||
global/org 种子域禁写;org_id/user_id 强制注入会话真实身份(忽略 LLM 传值);
|
||
无 user_id(无人值守)拒写;remove 只能删本人条目(org+user 双过滤)。
|
||
- 记忆注入改可见性版(`_build_system_prompt` 传 org_id/user_id → store.visible_to
|
||
过滤),修了旧「按域叠加」会带出他机构归属记忆的隐患。
|
||
|
||
### 2. manage_skill 工具(技能增改删,`skill_live.manage_skill_live`)
|
||
- 五动作 create/patch/write_file/remove_file/delete,隔离全在 skill_live:
|
||
只落本租户 orgs/{org}(org≠0)或 users/{uid}(org 0 降级/个人);
|
||
- **fork-on-write**:改 global 原版先整目录继承拷贝到本租户再改(同名覆盖仅本租户
|
||
生效);**校验通过才 fork**(旧「先 fork 后校验」失败会留副本遮蔽原版,已修);
|
||
- org+user 双副本同步改(user 遮蔽 org,防改了被遮蔽那份);产线/角色/项目层拒改;
|
||
delete 只删本租户副本(global 删不到,删后重新可见);子文件限四目录 + realpath 双检。
|
||
|
||
### 3. run_command background + process 工具(`bg_jobs.py`,`_t_run_command`/`_t_process`)
|
||
- 状态文件化 workspace/.bg/{job_id}/(meta.json+output.log),跨 worker/进程可见;
|
||
- 沙箱档位与前台一致(generic 强制 strict bwrap,无 bwrap 拒绝,绝不降级裸 shell);
|
||
- start_new_session 独立进程组 + 超时 SIGTERM 整组(不留孤儿);心跳 stale 判活。
|
||
|
||
### 4. delegate_subtask background + subagent 工具(`subagents.py`,`_t_delegate_subtask`/`_t_subagent`)
|
||
- 后台并行(同 workspace 上限 3),状态文件化 .sub/{sid}/;深度限制 1(子禁再委派);
|
||
- 子会话 session_isolation='none':不读父历史、不写 pipeline_conversations(防污染回放);
|
||
- steer/stop 走文件传递,子 agent 每轮 tool-loop 边界 `subagents.heartbeat` 消费;
|
||
result.txt 流式落盘,stop/崩溃即部分结果(绝不空手)。
|
||
|
||
## 第二批 Hermes 能力对齐:五级工具作用域 + patch_file + 原生视觉(2026-09-10)
|
||
|
||
### 五级作用域工具解析接线(`_resolve_tool_scopes`,解析逻辑在 core `tool_sources.py`)
|
||
- `_init_components` 第 8 步:调 core 五级解析(global/org/pipeline/role/project),
|
||
registry 重建为解析后工具集;capability 工具(技能 frontmatter 声明)合成
|
||
ToolDefinition 注册,执行路由 `capability_tools.exec_capability_tool`。
|
||
- **执行层门禁**(`_execute_tool` 第 0 步):`_allowed_tools` 非 None 时,作用域外
|
||
工具名(幻觉/越权)直接拒绝并回可行动提示(列出本会话可用工具)。解析失败
|
||
降级 `_allowed_tools=None` 不拦截(回退旧行为,不阻断会话)。
|
||
- 隔离走查修复(capability 两层语义):global 层 all/ 概念技能的 capability 声明
|
||
**不作为注入依据**(只当映射手册),否则 generic 会话拿到 ~70 个产线工具击穿
|
||
七层隔离;注入只认 org/pipeline/role/project/user 层技能的声明。
|
||
|
||
### patch_file 工具(`_t_patch_file`)
|
||
old_string 唯一性校验(0/多次拒绝,replace_all 放行)+ 原子写 + `_resolve_ws_path`
|
||
越界防护 + UTF-8 校验(二进制拒绝)。
|
||
|
||
### 原生视觉(多模态消息链)
|
||
- `run(user_input, image_parts)`:image_parts 非空时首条用户消息构造 OpenAI 多模态
|
||
content 数组(text + image_url data URL);llm_bridge→llm_v1 端点→inference 原样透传。
|
||
- `_call_llm` 降级兜底:含图调用失败 → `_degrade_images_if_needed` 剥离图片就地改写
|
||
消息(文本 + 系统说明「如实告知用户当前模型看不了图,建议切视觉模型或 invoke_model
|
||
i2t」)重试一次——绝不假装看过图。只降级一次。
|
||
- `_estimate_tokens`/`_summarize` 兼容 list content(`_content_as_text` 归一,图片按
|
||
1000 token 估)。
|
||
- `gateway.run_message(image_paths)`:build_image_parts 构造 + 超限 notes 注入 content。
|
||
- 实测:中性文件名红蓝两图,agent 正确答「第一张红色、第二张蓝色」,degrade 0 次。
|
||
|
||
## deliver 代码真实性三重门禁(2026-09-16 pbls M1a 空壳事故根治)
|
||
|
||
**事故形状**:develop agent 声称「已用 write_file 落盘 17141 字符、G1~G6 严格退出码门禁」,
|
||
实测交付的 verify_gate.py 只有 9 行纯注释 1225 字节——LLM 把「对文件的描述」当文件内容写入;
|
||
且 git 证据由 agent 自称(实为引擎代收口)。QC 抓到时代价已是 4 轮烧光 + fault + pause。
|
||
|
||
三层确定性门禁(全部引擎层,不靠 LLM 自觉):
|
||
|
||
1. **git 收口引擎化**(38efb3e):deliver 时引擎代为 add+commit 本任务写入的
|
||
apps/modules 仓库,收口核验记录(改了哪些仓库/几个文件/commit hash)回填交付件正文——
|
||
agent 无法再自称「已提交」伪造 git 证据。
|
||
2. **代码空壳硬门禁 `_validate_stub_code`**(0d02ac6):deliver 时本任务 write_file
|
||
实写的每个 `.py` 必须 ast 可解析且有真实可执行语句(纯注释/pass/纯 docstring/
|
||
语法错误一律拒绝,`__init__.py`/`conftest.py` 豁免),`.json` 必须可解析;
|
||
`files` 参数先验后写(`_validate_code_files_params`),防空壳落盘后被收口变既成事实。
|
||
拒绝时回填可行动 FAIL(指出文件+原因),agent 当轮重写——空壳死在 deliver 入口,
|
||
不再烧 QC 轮次。native + 文本兜底两条 deliver 路径都挂载。
|
||
3. **产出文件机械核验段**:引擎自动计算每个代码产出文件的实测字节/行数/语法/语句数,
|
||
以「产出文件机械核验(引擎自动计算,非 agent 声明)」标题回填交付件正文——
|
||
「声称 17141 实测 1225」类矛盾一眼可见,QC 拿到引擎级证据底座,
|
||
不再依赖自己 run_shell 逐个取证(取证失败盲判 0 分退也在烧重试预算,实测 9 轮里 3 轮是盲审)。
|
||
|
||
配套技能(pipeline-core 7bfa8d6):develop 角色「代码真实性铁律」(write_file 必须完整正文、
|
||
大文件拆小分批、声称数字须与引擎核验一致、勿自称 commit/push);QC review-develop
|
||
「优先读引擎回填两段核验当权威证据;声称 vs 实测矛盾直接按造假退;取证失败禁盲判 0 分,
|
||
改 read_file 逐项或 ask_question 冒泡」。
|
||
|
||
**通用规则**:质量门禁的判定证据必须来自引擎机械计算(字节/行数/ast/commit hash),
|
||
不能来自 agent 的自我声明;QC 尺子要配「取证失败 ≠ 判 0 分」的出路,否则盲审烧轮次。
|
||
|
||
|
||
|
||
## LLM 超时预算统一治理(2026-09-16 用户裁定:单点收敛,禁散落)
|
||
|
||
**事故背景**:09-05 提取、09-14 develop、09-16 QC 三次事故同一根因——调用方忘传
|
||
timeout → payload 无 _timeout → inference 按供应商端点配置(百炼 120s)掐断长生成;
|
||
且超时公式散落在各产线各角色调用点(570/330/+60/-60/min+60 各自为政),按角色
|
||
逐个修必然复发(09-14 只修 develop native,09-16 QC 复发实锤)。
|
||
|
||
**统一架构(全平台只有两处单点)**:
|
||
1. `llm_bridge._resolve_budget(timeout, default)`:timeout 语义=一次调用【总预算】
|
||
(0=平台缺省 chat 300 / 生成类 900;封顶 900 下限 60)→ 返回
|
||
(payload._timeout=总预算, 客户端 aiohttp=总预算+60)。四个公开入口
|
||
(llm_call/llm_call_msgs/llm_call_msgs_native/llm_infer)全走它,
|
||
payload._timeout 恒传——忘传 timeout 也结构性安全。
|
||
2. `pipeline-llm inference._post_upstream` deadline:attempt 超时=剩余预算,
|
||
总时长≈预算(消灭 3× 放大→客户端先断连→模糊 TimeoutError→误判永久错误链);
|
||
预算耗尽(<15s)不再发起必然超时 attempt;端点配置 timeout 不再掐断 attempt
|
||
(仅直连未传 _timeout 时与 _TOTAL_TIMEOUT 取大兜底)。
|
||
|
||
**三层预算对齐铁律**:调用方总预算 T → payload._timeout=T → 客户端=T+60 →
|
||
外层 wait_for(若有)≥ T+60。agent_loop:510→570→600 实证对齐。
|
||
验证特征串:inference 日志「预算=Ns」;llm_call_trace status_code=0 计数。
|
||
新增 LLM 调用点只需(可选)声明总预算,禁止自算客户端等待。
|
||
|
||
## 用户敏感信息库(secret_vault,2026-09-17)
|
||
|
||
**要解决的问题**:用户在会话里直接贴 apikey/token 时,明文会进模型上下文 → 随每轮 API
|
||
调用完整上行供应商 → 落 `pipeline_conversations` 永久留存 → session_search 捞回再上行。
|
||
(Hermes 侧同源分析实测:一个 5 轮任务的 key 上行 3~5 次,含重试可翻倍。)
|
||
|
||
**设计不变量(写死在代码,不靠 LLM 自觉)**
|
||
|
||
1. **真值只以子进程环境变量形式存在**,绝不拼进命令字符串。占位符 `@@sec:NAME@@`
|
||
在执行边界转成 `$PIPELINE_SEC_NAME`,值经 `subprocess env=` 注入。
|
||
→ 命令原文可安全落库/进上下文;模型抄回占位符是**预期行为**(不像脱敏成 `***`
|
||
会让模型把占位符抄进命令导致任务全断)。
|
||
2. **env 注入走 `env=`,不用 bwrap `--setenv`**:后者把值放进 bwrap 的 argv,同机任意
|
||
用户 `ps aux` / `/proc/<pid>/cmdline` 可读(world-readable);`env=` 落在
|
||
`/proc/<pid>/environ`,仅属主与 root 可读。bwrap 未用 `--clearenv`,默认继承自身环境,
|
||
所以 `env=` 的值能进沙箱内。
|
||
3. **未知占位符原样保留 + 拒绝执行**,绝不替换成空串。空串会静默改变命令语义
|
||
(实测 `rm -rf ${TARGET_DIR}$/` → `rm -rf /`)。
|
||
4. **读写权限范围分离**(`get_secret_row(own_only=)`):读/用路径含机构共享条目
|
||
(`user_id=''`,共享凭据就是给成员用的);写路径(save/delete/set_status)只匹配本人
|
||
条目 + `_assert_own` 代码级二次校验。否则任意机构成员可覆盖/删除全机构共用凭据。
|
||
5. **入站侦测只吃高置信度**:已知凭据前缀(16 种)或关键词邻域 + 熵 ≥3.5。
|
||
中低置信度只提示不自动入库——误报会把哈希/base64 数据块当凭据吞掉,静默毁数据,
|
||
比漏报更糟。**不做无上下文裸串侦测**(同理)。
|
||
|
||
**占位符语法为什么是 `@@sec:NAME@@`**
|
||
|
||
不能用 `${X}$`:sqlor 参数化(`sor.py:78` `sqlvp='${', sqlvs='}$'`)与 ahserver
|
||
`baseProcessor.py:193` 的 `ArgsConvert("${","}$")` 都用这个语法,复用会被误展开。
|
||
也不能用裸 `$VAR`:bash 会就地展开,绕过执行边界。
|
||
|
||
**加密:AES(appPublic.aes),不是 password_encode**
|
||
|
||
`ahserver.globalEnv.password_encode/decode`(:255-261)实测是 **RC4** 薄封装
|
||
(`:61 from appPublic.rc4 import password, unpassword`),换过去等于没换。
|
||
真正的 AES 在 `appPublic.aes`(AES-ECB + PKCS7 + base64),与 DB 连接密码同套。
|
||
|
||
⚠️ `aes.py` 内有**两处方向相反**的 iso-8859-1,只有一处需要包装:
|
||
|
||
| 位置 | 方向 | 结论 |
|
||
|---|---|---|
|
||
| `aes.py:42-43,46-47` | 密文 bytes → str | **必须** iso-8859-1。实测 200 组 AES 密文用 utf-8 解码失败 200 组(100%,首字节 0xef 即 invalid continuation byte)——密文是二进制,不符合 UTF-8 规范。历史正确设计,**不要动** |
|
||
| `aes.py:29,37` | 明文 str → bytes | iso-8859-1 表示不了非 ASCII。实测「密码是中文Abc123」「🔑key123」直接 UnicodeEncodeError |
|
||
|
||
解法在**调用方**包装(不改 aes.py——appPublic 是多宿主共享基础模块,改它影响 DB 密码
|
||
等全部调用方):`str → UTF-8 bytes → 按 latin-1 逐字节还原成等长 str 传入`。latin-1 是
|
||
字节保真映射(0x00-0xFF 一一对应),`aes.py:29` 再 `encode('iso-8859-1')` 即还原成原始
|
||
UTF-8 字节流 = 字节保真空操作。出库反向还原。包装后实测 9/9 往返一致(ASCII / GitHub
|
||
PAT / 中文 / emoji / 中英混合+符号 / 私钥含换行 / 512 长值 / `p@$$w0rd!#%^&*()` /
|
||
含 `@@sec:` 语法的值),密文零明文残留。
|
||
|
||
密文带版本前缀 `AES1$`:base64 密文无固定形态特征,无法像 RC4 的 `QUZVcX` 靠前缀嗅探
|
||
判断是否已加密;显式版本标记同时为算法迁移留路。`looks_encrypted()` 同时识别 `AES1$`
|
||
与历史 `QUZVcX`(幂等加密防多层叠加 + 向后兼容读取 llm 表等存量 RC4 密文)。
|
||
|
||
**三路接入**
|
||
|
||
| 路径 | 位置 | 身份来源 | 工具集 |
|
||
|---|---|---|---|
|
||
| 入站门禁 | `gateway.run_message` Step 0.5(所有上下文解析**之前**) | `get_user()` | 自动侦测→入库→替换,progress 事件回显动作 |
|
||
| v2 会话 agent | `agent_loop_v2._t_secret_tool` + `_t_run_command` + `_t_process` | `self.user_id`(作用域解析里被 generic 置空的那份**不能用**) | 全六件 |
|
||
| v1 角色 agent | `agent_loop._exec_agent_tool` + `run_shell` 分支 | 项目真人 owner(`rag_client.resolve_project_owner`,已处理 `created_by='agent.pm'` 自动立项反查) | 只读三件(list/use/detect)——无人值守下 LLM 自主增删凭据风险高于收益 |
|
||
|
||
入站门禁必须在 Step 0.5:净化后的 content 才是该进模型上下文与落库的版本,
|
||
明文一旦进 executor 就会每轮上行供应商。
|
||
|
||
出站净化两处:`_t_run_command`(前台)+ `_t_process`(包一层 `_t_process_raw`,
|
||
后台 output.log 可能回显凭据)。执行边界必须在前台/后台分支**之前**,否则后台命令不受保护。
|
||
|
||
**前端(AgentIO 钥匙图标)**
|
||
|
||
bricks 是多应用共用框架,**不硬编码任何宿主路径或占位符格式**(对齐 `model_dataurl`
|
||
既有设计)。宿主通过 opts 注入 `secret_dataurl` / `secret_format` / `secret_title` /
|
||
`secret_tip`;未传 `secret_dataurl` 时钥匙图标根本不渲染 → 其他应用零影响。
|
||
|
||
- `UiText.insertAtCursor(text)`:光标位置插入、有选区则替换选区。两个时序坑照抄
|
||
`handle_enter`/`handle_tab_indent` 既有处理:程序改 `dom_element.value` 不触发 input
|
||
事件(须手动同步 `this.value`);`focus()` 后浏览器可能重置 selectionStart
|
||
(用 `schedule_once` 0.5ms 后重设)。**不 dispatch('changed')**——UiText 也用于 Form,
|
||
多发一次会触发脏值/校验联动。
|
||
- 弹窗 API:`PopupWindow` 无 `close()` 也无 `'closed'` 事件,正确是 `dismiss()` +
|
||
`'dismissed'`(popup.js:475/481)。
|
||
- 样式走全局 css class(`.secret-picker-row` 等)——bricks 内联嵌套 style 对象无效。
|
||
- 6 处 AgentIO 入口全部注入:`pipeline-core/wwwroot/agent/index.ui`(静态 + 动态建 tab
|
||
脚本各一处)、`agent_generic/index.ui`、`mobile_agent.ui`(2 处)、
|
||
`pipeline-sdlc/wwwroot/index.ui`、`index_cockpit.ui`。
|
||
- 后端 `pipeline-core/wwwroot/api/secret_options.dspy`:身份取自 `get_user()`/`get_userorgid()`,
|
||
**不接受前端传 user_id/org_id**(否则改参数即可枚举他人凭据名称);双保险剔除
|
||
`encrypted_value`/`fingerprint`;未登录返 `success:false`(区分「没登录」与「没数据」)。
|
||
RBAC 已被 `rp.json` 的 `/pipeline_core/**`(logined) 覆盖,无需额外注册。
|
||
|
||
**性能(实测,本地 20KB 文本)**
|
||
|
||
`tokenize` 0.003ms / `detokenize` 0.016ms / 4KB 侦测 0.64ms / 用户消息侦测 0.034ms。
|
||
|
||
**三个挂调实锤的实现陷阱(勿回退,有回归测试)**
|
||
|
||
| 陷阱 | 症状 | 修复 |
|
||
|---|---|---|
|
||
| 子串碰撞 | 短 key `key-Ab3x` 与长 key 共存时按插入序替换 → 长 key 被吃掉前缀、尾部明文裸露进上下文 | `_sorted_secrets` 按值长度**降序**替换 |
|
||
| 占位符别名劫持 | secret 的值本身长得像占位符 → 二次 token 化改写既有占位符 | 入库拒绝值内含 `@@sec:`(`_PLACEHOLDER_IN_VALUE`)+ tokenize 幂等 |
|
||
| 未知占位符吞空 | `ArgsConvert(default='')` 把缺失变量替换成空串 → 命令语义反转 | `detokenize` 原样保留 + 回报未知名单;执行边界拒绝执行 |
|
||
|
||
另两个自产 bug:`_PLACEHOLDER_RE` 用 `{1,62}`(最少 2 字符)与 `_NAME_RE` 长度下界不一致
|
||
→ 单字符名存得进库但识别不出(未知占位符漏报);`guess_type` 名称关键词优先 →
|
||
`openai_style_key` 里的 `key` 把精确类型降级成 `api_key`。均已修 + 加同步断言。
|
||
|
||
**测试**:`/tmp/test_secret_vault.py` 124 断言全绿(11 组:AES 加解密/子串碰撞/别名劫持/
|
||
未知占位符/侦测/名称归一/端到端/性能/DB 层/跨文件工具名同步/越权防护)。
|
||
桩 `CapturingSor` **必须真实模拟 WHERE 过滤**——第一版对任何 SELECT 都返回全部 rows,
|
||
无视 SQL 条件,导致「零写操作」断言在过滤正确时反而 FAIL(假阳性)。
|
||
测试桩失真 = 测试证明不了任何事。
|
||
|
||
**部署**:`dist/` 在 bricks 的 .gitignore 里(构建产物不入库)→ 部署必须在测试机
|
||
`cd pkgs/bricks && bash build.sh` 重建,不能靠 git pull(同「bricks 追平必须连 dist 重建」)。
|
||
建表走 m0037 + `create_tables.py`(自动扫描 `pkgs/*/models`,索引按列集合幂等判重)。
|