Skip to content

fix: 修复skill_exec未注入Skill专属环境变量的问题 (#359) - #360

Open
kinolo wants to merge 1 commit into
trpc-group:mainfrom
kinolo:fix/skill-exec-repository-env
Open

kinolo wants to merge 1 commit into
trpc-group:mainfrom
kinolo:fix/skill-exec-repository-env

Conversation

@kinolo

@kinolo kinolo commented Oct 10, 2026

Copy link
Copy Markdown

当 Skill 的环境变量仅通过 repository.skill_run_env() 提供时,skill_run 能读取变量,而 skill_exec 会遗漏注入。本次将已有注入逻辑抽为共享内部 helper,并让 skill_exec 在构造执行环境前调用。

保留显式 env 优先、宿主非空值优先、空键和空白值过滤、禁止键过滤,以及 repository 钩子异常时继续执行的行为。helper 接收已解析的 repository,skill_exec 不额外调用 resolver。同步补充中英文 Skill 文档。

验证:

  • 新增两个入口的参数化回归测试,覆盖 repository 注入、显式覆盖、宿主覆盖、显式覆盖宿主、空白宿主值和显式空值。
  • 新增空键/空白值过滤、全部现有禁止键(含大小写处理)和钩子异常测试。
  • pytest tests/skills tests/code_executors/local/test_local_ws_runtime.py tests/code_executors/test_base_workspace_runtime.py:803 passed。
  • pytest tests/skills/tools/test_skill_run.py tests/skills/tools/test_skill_exec.py:59 passed。
  • 修改的 Python 文件通过 YAPF、flake8 和 git diff --check。
  • 使用 [BUG] skill_exec does not apply repository.skill_run_env() #359 中的真实本地子进程脚本验证:repository-only 条件下两个入口均输出 injected;显式参数和宿主变量对照结果保持一致。

Fixes #359

PR type: type/bug

RELEASE NOTES: 修复通过 skill_exec 执行 Skill 命令时,repository 配置的 Skill 专属环境变量未生效的问题。

@codecov

codecov Bot commented Oct 10, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
⚠️ Please upload report for BASE (main@8ce3612). Learn more about missing BASE report.

Additional details and impacted files
@@            Coverage Diff             @@
##             main        #360   +/-   ##
==========================================
  Coverage        ?   87.51446%           
==========================================
  Files           ?         543           
  Lines           ?       52733           
  Branches        ?           0           
==========================================
  Hits            ?       46149           
  Misses          ?        6584           
  Partials        ?           0           

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@kinolo kinolo changed the title skills: apply repository environment to skill_exec fix: 修复skill_exec未注入Skill专属环境变量的问题 (#359) Oct 10, 2026
Share skill-specific environment injection between skill_run and skill_exec,
preserving explicit and host environment precedence, blocked-key filtering,
and repository error handling. Add regression tests and document the behavior.

Fixes trpc-group#359

RELEASE NOTES: Skill-specific environment variables are now available when commands run through skill_exec.
@kinolo
kinolo force-pushed the fix/skill-exec-repository-env branch from 9e4059d to e689756 Compare October 10, 2026 16:15
@helloopenworld

Copy link
Copy Markdown
Contributor

AI Code Review

审查结论

通过

审查范围:8ce3612..9e4059d(6 文件,+158/-15)。变更内容:将 SkillRunTool 内联的 skill 环境注入逻辑提取为模块级函数 _apply_skill_run_env(_skill_run.py:119-135),并在 SkillExecTool._run_async_impl 中新增调用(_skill_exec.py:451),使 skill_exec 会话也能获得仓库注入的 Skill 专属环境变量;同步新增 2 个测试文件的回归用例并更新中英文文档。

计划符合性:提取重构经与父提交逐行比对,与原内联逻辑(键 strip、显式 env 优先、宿主非空保护、blocked 键过滤、异常吞噬)完全等价,skill_run 行为零变化;skill_exec 注入点位于 _build_exec_env 之前、ENV_SKILL_NAME 键先于注入占位,顺序正确,repository 复用已解析实例无重复解析;文档描述与实现逐条核对一致(显式 env 优先、宿主非空保护、空白与 blocked 键忽略、钩子异常时命令继续)。三条测试覆盖注入分支矩阵(显式/host/空白/blocked 大小写/异常/双工具端到端),测试构造与生产 SkillToolSet 模式一致,可真实跑通。

主要风险集中在被复用的注入函数与 skill_exec 路径既有的环境构造之间的交互,共报告 3 条:1) 仓库注入键可覆盖 WORKSPACE_DIR/SKILLS_DIR/WORK_DIR/OUTPUT_DIR/RUN_DIR 五个工作区基础键(基础键在注入后合并且 user 优先,宿主通常无同名键故放行),导致运行目录错位、产物收集失效——MODERATE,本次变更使该冲突面首次对 skill_exec 可达;2) 检查用 strip 后键、调用方 env 键未 strip,空白键可绕过显式 env 优先级并产生同键双形式并存——MODERATE;3) v.strip() 无类型防护,非字符串值中断整批注入且无日志——LOW。被拒绝的候选:异常吞噬与 blocked 键清单缺失(BASH_ENV 等)均为既有行为且本变更零攻击面扩大;cube/container 宿主环境不一致属既有路径限制(该两运行时不支持 skill_exec 的 start_program,注入不生效也不破坏流程);default repository 的 visibility ValueError 被吞与文档声明一致;测试对宿主 LD_* 键的静默退化已由本机验证排除。未发现 SEVERE 级缺陷,变更满足需求且实现质量良好;建议修正上述 3 条后合入,均不阻塞。

发现的问题

中等

trpc_agent_sdk/skills/tools/_skill_run.py:119-135

问题: _apply_skill_run_env 的拦截检查只覆盖调用方 env(inputs.env + SKILL_NAME)与宿主 os.environ 中已存在的键,但 WORKSPACE_DIR/SKILLS_DIR/WORK_DIR/OUTPUT_DIR/RUN_DIR 这五个工作区基础键是在注入之后才由 _build_exec_env(_skill_exec.py:307-308)或 local runner 的 _build_program_env(_local_ws_runtime.py:563-567)生成并合并,且合并逻辑均为 user/env 优先、基础键仅补齐缺失。仓库 skill_run_env 返回同名键时会被直接注入并覆盖这些运行目录变量——本次变更把 skill_run_env 引入 skill_exec 路径,使该冲突面首次对该调用路径可达(此前 skill_exec 从不调用该钩子)。

触发条件: skill 配置 env: 或自定义 BaseSkillRepository.skill_run_env 返回任一工作区基础键名(loader 的 _build_skill_run_env 只过滤 7 个 blocked 键,不拦这 5 个,见 _skill_loader.py:468-484),且 agent 宿主进程恰好没有同名环境变量(宿主通常不导出这些键,os.environ.get(k) 为空即放行),无需 LLM 配合构造。

实际影响: 子进程收到的 $OUTPUT_DIR/$RUN_DIR/$WORK_DIR 指向配置值而非真实工作区目录:_build_exec_env 已用 os.makedirs 创建真实 run_dir,注入后 $RUN_DIR 错位并遗留孤儿目录;按 $OUTPUT_DIR 语义写出的产物与按 ws.path/DIR_OUT 收集的 _prepare_outputs/_AUTO_EXPORT_PATTERN 不一致,产物静默丢失或错位。

修正方向: 在 _apply_skill_run_env 内对 WORKSPACE_ENV_DIR_KEY、ENV_SKILLS_DIR、ENV_WORK_DIR、ENV_OUTPUT_DIR、ENV_RUN_DIR(均已在 code_executors/_constants.py 定义)加与 _BLOCKED_SKILL_ENV_KEYS 同等的放行排除;同时补充一条 repo env 含基础键名时断言真实目录值不被覆盖的回归测试。

中等

trpc_agent_sdk/skills/tools/_skill_run.py:123-133

问题: 注入循环对 skill_run_env 返回的键先做 k = k.strip(),随后的 if k in env、os.environ.get(k)、blocked 检查与最后写入 env[k] = v 均使用 strip 后的键,但调用方 env(SkillRunInput.env/ExecInput.env)与宿主环境的键未 strip——pydantic dict[str,str] 无任何键约束(_skill_run.py:335、_skill_exec.py:118),LLM 输入的 env 含首尾空白键(如 "TEST_SKILL_ENV ")时,k in env 检查放行,注入值写入 strip 后键,两种键形式并存。

触发条件: 模型/上游在 env 参数中传入含首尾空白的键(如 {"TEST_SKILL_ENV ": "explicit"}),同时 skill_run_env 返回该键的 strip 形式。skill_exec 路径下 _build_exec_env 以 os.environ.copy() 起步,空白键(如 " RUN_DIR ")不被基础键去重逻辑(if k not in extra)拦截,同键两种形式同时进入子进程环境。

实际影响: 子进程同时看到 RUN_DIR 与 " RUN_DIR " 两个环境变量:后者携带注入值(正确值被空转浪费),前者继承宿主旧值可能指向上一工作区/任意目录,skill 脚本按 $RUN_DIR/$OUTPUT_DIR 操作时行为错位且难以排查;显式 env 优先级保障被空白键静默绕过。

修正方向: 在 _apply_skill_run_env 内将调用方 env 的键统一 strip 后再做存在性检查(或对两侧键做同一归一化后比较),并补充空白键组合的回归测试。

较低

trpc_agent_sdk/skills/tools/_skill_run.py:123-125

问题: 注入循环对值调用 v.strip()(第 125 行)时未做类型防护;BaseSkillRepository.skill_run_env 是抽象接口(_repository.py:125),自定义实现可返回含非字符串值(None/数字/任意对象)的 dict,此时抛 AttributeError 被外层 except Exception: pass 整体吞掉,同一技能其余 env 键全部静默丢失且无任何日志。

触发条件: 任何自定义 BaseSkillRepository.skill_run_env 返回含非 str 值的 dict(树内 loader _build_skill_run_env 保证 str,但该钩子是公开扩展点且无类型强制);skill_exec 路径此前从不调用该钩子,属本次变更新引入的可达面,skill_run 旧路径同样受影响但本次将其复制到了第二个工具。

实际影响: 注入在首个非法值处中断,技能所需凭据(如 api_key)部分或全部缺失,命令因缺环境变量失败而错误信息与真实原因无关,难以排查;已写入的前几个键造成不确定的部分注入状态。

修正方向: 对值(与键)做 str(v).strip() 强制转换(与 loader _build_skill_run_env 的 str(key)/str(val) 做法一致),或对非字符串项 continue 跳过,避免单键毁掉整批注入。

Comment on lines +119 to +135
def _apply_skill_run_env(repository: BaseSkillRepository, skill_name: str, env: dict[str, str]) -> None:
"""Inject skill-specific env without overriding explicit or non-empty host values."""
try:
skill_env: dict[str, str] = repository.skill_run_env(skill_name)
for k, v in skill_env.items():
k = k.strip()
if not k or not v.strip():
continue
if k in env: # don't override explicit tool-call env
continue
if os.environ.get(k, "").strip(): # don't override host env
continue
if k.upper() in _BLOCKED_SKILL_ENV_KEYS:
continue
env[k] = v
except Exception: # pylint: disable=broad-except
pass

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

问题: _apply_skill_run_env 的拦截检查只覆盖调用方 env(inputs.env + SKILL_NAME)与宿主 os.environ 中已存在的键,但 WORKSPACE_DIR/SKILLS_DIR/WORK_DIR/OUTPUT_DIR/RUN_DIR 这五个工作区基础键是在注入之后才由 _build_exec_env(_skill_exec.py:307-308)或 local runner 的 _build_program_env(_local_ws_runtime.py:563-567)生成并合并,且合并逻辑均为 user/env 优先、基础键仅补齐缺失。仓库 skill_run_env 返回同名键时会被直接注入并覆盖这些运行目录变量——本次变更把 skill_run_env 引入 skill_exec 路径,使该冲突面首次对该调用路径可达(此前 skill_exec 从不调用该钩子)。

触发条件: skill 配置 env: 或自定义 BaseSkillRepository.skill_run_env 返回任一工作区基础键名(loader 的 _build_skill_run_env 只过滤 7 个 blocked 键,不拦这 5 个,见 _skill_loader.py:468-484),且 agent 宿主进程恰好没有同名环境变量(宿主通常不导出这些键,os.environ.get(k) 为空即放行),无需 LLM 配合构造。

实际影响: 子进程收到的 $OUTPUT_DIR/$RUN_DIR/$WORK_DIR 指向配置值而非真实工作区目录:_build_exec_env 已用 os.makedirs 创建真实 run_dir,注入后 $RUN_DIR 错位并遗留孤儿目录;按 $OUTPUT_DIR 语义写出的产物与按 ws.path/DIR_OUT 收集的 _prepare_outputs/_AUTO_EXPORT_PATTERN 不一致,产物静默丢失或错位。

修正方向: 在 _apply_skill_run_env 内对 WORKSPACE_ENV_DIR_KEY、ENV_SKILLS_DIR、ENV_WORK_DIR、ENV_OUTPUT_DIR、ENV_RUN_DIR(均已在 code_executors/_constants.py 定义)加与 _BLOCKED_SKILL_ENV_KEYS 同等的放行排除;同时补充一条 repo env 含基础键名时断言真实目录值不被覆盖的回归测试。

Comment on lines +123 to +133
for k, v in skill_env.items():
k = k.strip()
if not k or not v.strip():
continue
if k in env: # don't override explicit tool-call env
continue
if os.environ.get(k, "").strip(): # don't override host env
continue
if k.upper() in _BLOCKED_SKILL_ENV_KEYS:
continue
env[k] = v

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

问题: 注入循环对 skill_run_env 返回的键先做 k = k.strip(),随后的 if k in env、os.environ.get(k)、blocked 检查与最后写入 env[k] = v 均使用 strip 后的键,但调用方 env(SkillRunInput.env/ExecInput.env)与宿主环境的键未 strip——pydantic dict[str,str] 无任何键约束(_skill_run.py:335、_skill_exec.py:118),LLM 输入的 env 含首尾空白键(如 "TEST_SKILL_ENV ")时,k in env 检查放行,注入值写入 strip 后键,两种键形式并存。

触发条件: 模型/上游在 env 参数中传入含首尾空白的键(如 {"TEST_SKILL_ENV ": "explicit"}),同时 skill_run_env 返回该键的 strip 形式。skill_exec 路径下 _build_exec_env 以 os.environ.copy() 起步,空白键(如 " RUN_DIR ")不被基础键去重逻辑(if k not in extra)拦截,同键两种形式同时进入子进程环境。

实际影响: 子进程同时看到 RUN_DIR 与 " RUN_DIR " 两个环境变量:后者携带注入值(正确值被空转浪费),前者继承宿主旧值可能指向上一工作区/任意目录,skill 脚本按 $RUN_DIR/$OUTPUT_DIR 操作时行为错位且难以排查;显式 env 优先级保障被空白键静默绕过。

修正方向: 在 _apply_skill_run_env 内将调用方 env 的键统一 strip 后再做存在性检查(或对两侧键做同一归一化后比较),并补充空白键组合的回归测试。

Comment on lines +123 to +125
for k, v in skill_env.items():
k = k.strip()
if not k or not v.strip():

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

问题: 注入循环对值调用 v.strip()(第 125 行)时未做类型防护;BaseSkillRepository.skill_run_env 是抽象接口(_repository.py:125),自定义实现可返回含非字符串值(None/数字/任意对象)的 dict,此时抛 AttributeError 被外层 except Exception: pass 整体吞掉,同一技能其余 env 键全部静默丢失且无任何日志。

触发条件: 任何自定义 BaseSkillRepository.skill_run_env 返回含非 str 值的 dict(树内 loader _build_skill_run_env 保证 str,但该钩子是公开扩展点且无类型强制);skill_exec 路径此前从不调用该钩子,属本次变更新引入的可达面,skill_run 旧路径同样受影响但本次将其复制到了第二个工具。

实际影响: 注入在首个非法值处中断,技能所需凭据(如 api_key)部分或全部缺失,命令因缺环境变量失败而错误信息与真实原因无关,难以排查;已写入的前几个键造成不确定的部分注入状态。

修正方向: 对值(与键)做 str(v).strip() 强制转换(与 loader _build_skill_run_env 的 str(key)/str(val) 做法一致),或对非字符串项 continue 跳过,避免单键毁掉整批注入。

@helloopenworld

Copy link
Copy Markdown
Contributor

AI Code Review

审查结论

通过

审查范围:8ce3612..e689756(PR #359「修复skill_exec未注入Skill专属环境变量的问题」),共 6 个文件、+158/-15 行,涉及 _skill_run.py 抽取 _apply_skill_run_env 帮助函数、_skill_exec.py 在 _build_exec_env 前新增注入调用、2 个测试文件与中英文档。

计划符合性:变更准确实现计划目标——skill_exec 现在与 skill_run 一样调用 repository.skill_run_env 注入 Skill 专属环境变量;抽取函数与旧内联块逐字等价(git show 基提交确认),run 路径行为零变化;exec 路径 extra_env 由 dict(inputs.env) 起步、注入回填后经 _build_exec_env 以 host 副本打底再 update,优先级(显式 call env > 宿主非空值 > 钩子值)在 local/container/cube 三种运行时合并语义下均与文档宣称一致;空键、空白值、LD_PRELOAD 等禁用键被忽略,钩子异常被吞掉不阻塞执行。

主要风险核查:无正确性、安全、兼容性缺陷。注入仅经 env 传递不参与命令构造;阻塞键经 k.upper() 归一后命中白名单,与 loader/parser 侧一致;无新增攻击面。测试方面:12 个参数化用例 + 3 个单元测试经调用图全链路静态验证可正确运行(import 全部可解析、skill_stager property 实例属性遮蔽可替换 stage_skill、位置参数索引与调用点相符、monkeypatch 生效路径正确),覆盖显式覆盖、宿主优先、空白值、空显式值、密钥过滤、异常吞没等边界;文档声明与实现逐条相符。次要候选(值保留前导空白、RUN_DIR 基键差异、EDITOR/VISUAL 与 editor_text 冲突)均为先前已有或文档一致行为,非本次引入,按证据验证协议排除。

门禁结论:PASSED,无高置信可操作问题。

发现的问题

未发现需要反馈的代码问题。

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[BUG] skill_exec does not apply repository.skill_run_env()

2 participants