2026-08-08 11:43:26 +08:00

pipeline_service

产线执行引擎 —— 任务调度、步骤执行、人工任务交互、LLM 桥接。

功能

  • 任务执行DAG 步骤调度与状态机
  • 人工任务:审批/输入等待与交互
  • LLM 桥接:统一 LLM 调用接口
  • Agent LoopAI 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.pym0002 建表未进 models2026-09-10 归籍)
pipeline_agent_settings 用户 agent 偏好(每用户一行 uk_user默认模型/当前项目指针;写入方 agent_loop_v2、set_agent_model.dspy、workspace.py2026-09-10 归籍)
pipeline_project_agents 项目角色 agent 配置(驾驶舱 v1 start_agent 链路v2 已走 pipeline_agent_instances保留至 v1 链路删除决议2026-09-10 归籍)
pipeline_user_secrets 用户敏感信息库2026-09-17m0037 建表AES 密文 + 指纹去重 + org/user 双维归属;写入方 secret_vault.pyagent 工具 + gateway 入站自动侦测);唯一索引 (org_id,user_id,name)

安装

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-17AES 密文入库 + 会话占位符引用 @@sec:NAME@@ + 入站自动侦测入库;真值只在执行边界以子进程 env 注入,永不进模型上下文/落库/进命令字符串gateway 入站门禁 + v1 角色 agent项目 owner 身份)+ v2 会话 agent 三路接入

通用会话产线隔离2026-09-057 层纵深防御)

规范通用会话generic=Trueagent_chat_generic.dspy= 纯通用 agent 不挂任何产线工具/技能/角色/记忆,不得看到或操作任何产线的项目。

隔离层(缺一层即泄漏,详见技能 cockpit-agent-patterns

  1. 工具注册表ToolRegistry 每 executor 独立实例(禁全局单例,防跨会话残留)
  2. 默认产线回退generic 会话 pipeline_id 空不回退 DEFAULT_ABILITY_ID
  3. 产线能力包_execute_ability_tool 对 generic 直接拒绝
  4. 工具 schemaload_agent_configpipeline-coregeneric 时剔除 project/shell 类工具
  5. 执行层兜底_dispatch_sdlc_tool 对 generic 拒绝项目管理工具(别名归一后拦截)
  6. 工作空间generic 会话文件根 = _general/{user_id} 专属目录generic_workspace_dir _resolve_ws_path 越界保护圈死run_command 剔除shell 无法圈禁); resolve_upload_dirpipeline-coregeneric=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 URLllm_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 收口引擎化38efb3edeliver 时引擎代为 add+commit 本任务写入的 apps/modules 仓库,收口核验记录(改了哪些仓库/几个文件/commit hash回填交付件正文—— agent 无法再自称「已提交」伪造 git 证据。
  2. 代码空壳硬门禁 _validate_stub_code0d02ac6deliver 时本任务 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 7bfa8d6develop 角色「代码真实性铁律」write_file 必须完整正文、 大文件拆小分批、声称数字须与引擎核验一致、勿自称 commit/pushQC 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 native09-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 deadlineattempt 超时=剩余预算, 总时长≈预算(消灭 3× 放大→客户端先断连→模糊 TimeoutError→误判永久错误链 预算耗尽(<15s)不再发起必然超时 attempt端点配置 timeout 不再掐断 attempt (仅直连未传 _timeout 时与 _TOTAL_TIMEOUT 取大兜底)。

三层预算对齐铁律:调用方总预算 T → payload._timeout=T → 客户端=T+60 → 外层 wait_for若有≥ T+60。agent_loop510→570→600 实证对齐。 验证特征串inference 日志「预算=Ns」llm_call_trace status_code=0 计数。 新增 LLM 调用点只需(可选)声明总预算,禁止自算客户端等待。

用户敏感信息库secret_vault2026-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-readableenv= 落在 /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:193ArgsConvert("${","}$") 都用这个语法,复用会被误展开。 也不能用裸 $VARbash 会就地展开,绕过执行边界。

加密AESappPublic.aes不是 password_encode

ahserver.globalEnv.password_encode/decode:255-261实测是 RC4 薄封装 :61 from appPublic.rc4 import password, unpassword),换过去等于没换。 真正的 AES 在 appPublic.aesAES-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:29encode('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 分支 项目真人 ownerrag_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.valuefocus() 后浏览器可能重置 selectionStart (用 schedule_once 0.5ms 后重设)。不 dispatch('changed')——UiText 也用于 Form 多发一次会触发脏值/校验联动。
  • 弹窗 APIPopupWindowclose() 也无 '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.uimobile_agent.ui2 处)、 pipeline-sdlc/wwwroot/index.uiindex_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,索引按列集合幂等判重)。

Description
No description provided
Readme 1.4 MiB
Languages
Python 99.5%
Shell 0.5%