背景
当前 skill 生态中,部分 skill(如 $Code)运行时会在硬编码的路径下写持久化文件(例如 ~/code/memory.md),但这些路径不在 Agent 的 writable_roots 中,导致 tool.path.outside_workspace 拒绝。
现状分析
权限模型
- 内建可写根:
~/.agents、~/.tiy、~/.cache、/tmp、$TMPDIR
- 全局混合模型:read / write / edit / search / list / find 共用同一组
writable_roots
- Path 解析器只认绝对路径或相对 workspace 的路径,不展开
~、${VAR}
Prompt 注入
SkillsProvider 只暴露每个 skill 的 SKILL.md 文件路径,没有 stateDir、没有兼容规则、没有"本轮激活的是哪个 skill"的结构化信息。
痛点
$Code skill 要求读写 ~/code/memory.md,但该路径不在任何 writable_roots 中。按当前设计,每遇到一个这样的 skill 就要手动给用户开白名单——不可持续。
方案
核心思路:不继续给未知目录开 writable_roots,而是把 skill 自有状态统一收敛到 ~/.tiy/skills-state/<skill-id>/(~/.tiy 已是内建可写根),再做 legacy 路径识别与定向重写。
1. 数据模型扩展
SkillRecordDto(src-tauri/src/model/extensions.rs)新增字段:
pub state_dir: String, // ~/.tiy/skills-state/<sanitized-id>/
pub legacy_state_paths: Vec<String>, // 从 SKILL.md 启发式提取,如 "~/code"
前端类型同步。
2. Skill 加载时生成 state 信息
parse_skill_markdown 中:
- 生成
state_dir = ~/.tiy/skills-state/<id>/
- 启发式扫描:从
SKILL.md body 提取 ~/xxx、$HOME/xxx、mkdir -p ~/xxx 等模式
- 排除已有权限的路径(skill 自身目录、
/tmp、workspace)
3. Prompt 注入升级
SkillsProvider::collect 输出每个 skill 的 state 目录 + 兼容规则:
- Code: Coding workflow... (file: .../SKILL.md, state: ~/.tiy/skills-state/code-1.0.4/)
并增加兼容指引:
### Skill state rules
- Skill installation directories are READ-ONLY. Never modify files inside a skill's own directory.
- Each skill has a dedicated writable state directory listed above. Write all skill-owned persistent files there.
- If a SKILL.md references legacy paths like ~/code/memory.md, treat them as examples. Use the skill's state directory instead.
4. 工具层兼容拒绝与重写
当 file 工具返回 tool.path.outside_workspace 时:
if 被拒绝的路径 ∈ 某个已启用 skill 的 legacy_state_paths:
返回 enhanced error + suggested redirect
不自动静默重写,但给 agent 足够信息自行修正。
5. 后续可选增强
- 支持
skill://state/<skill-id>/memory.md 伪协议路径
- 按当前激活 skill 动态追加 state root 到
additional_roots
改造清单(最小可行版)
| 文件 |
改动 |
src-tauri/src/model/extensions.rs |
SkillRecordDto 加 state_dir、legacy_state_paths |
src/shared/types/extensions.ts |
SkillRecord 同步加字段 |
src-tauri/src/extensions/mod.rs |
parse_skill_markdown:生成 state_dir,启发式扫描 legacy 路径 |
src-tauri/src/core/prompt/providers.rs |
SkillsProvider::collect:输出 state 目录 + 兼容规则 |
src-tauri/src/core/policy_engine.rs / tool_gateway.rs |
outside 拒绝时检查 legacy_state_paths,返回 suggested redirect |
优点
- 不需要用户手动加路径到
writable_roots
- 不需要预测每个 skill 会用什么路径
~/.tiy 已是内建可写根,无需额外权限
- 统一、可审计、可按 skill 隔离
背景
当前 skill 生态中,部分 skill(如
$Code)运行时会在硬编码的路径下写持久化文件(例如~/code/memory.md),但这些路径不在 Agent 的writable_roots中,导致tool.path.outside_workspace拒绝。现状分析
权限模型
~/.agents、~/.tiy、~/.cache、/tmp、$TMPDIRwritable_roots~、${VAR}Prompt 注入
SkillsProvider只暴露每个 skill 的SKILL.md文件路径,没有stateDir、没有兼容规则、没有"本轮激活的是哪个 skill"的结构化信息。痛点
$Codeskill 要求读写~/code/memory.md,但该路径不在任何writable_roots中。按当前设计,每遇到一个这样的 skill 就要手动给用户开白名单——不可持续。方案
核心思路:不继续给未知目录开
writable_roots,而是把 skill 自有状态统一收敛到~/.tiy/skills-state/<skill-id>/(~/.tiy已是内建可写根),再做 legacy 路径识别与定向重写。1. 数据模型扩展
SkillRecordDto(src-tauri/src/model/extensions.rs)新增字段:前端类型同步。
2. Skill 加载时生成 state 信息
parse_skill_markdown中:state_dir = ~/.tiy/skills-state/<id>/SKILL.mdbody 提取~/xxx、$HOME/xxx、mkdir -p ~/xxx等模式/tmp、workspace)3. Prompt 注入升级
SkillsProvider::collect输出每个 skill 的 state 目录 + 兼容规则:并增加兼容指引:
4. 工具层兼容拒绝与重写
当 file 工具返回
tool.path.outside_workspace时:不自动静默重写,但给 agent 足够信息自行修正。
5. 后续可选增强
skill://state/<skill-id>/memory.md伪协议路径additional_roots改造清单(最小可行版)
src-tauri/src/model/extensions.rsSkillRecordDto加state_dir、legacy_state_pathssrc/shared/types/extensions.tsSkillRecord同步加字段src-tauri/src/extensions/mod.rsparse_skill_markdown:生成 state_dir,启发式扫描 legacy 路径src-tauri/src/core/prompt/providers.rsSkillsProvider::collect:输出 state 目录 + 兼容规则src-tauri/src/core/policy_engine.rs/tool_gateway.rs优点
writable_roots~/.tiy已是内建可写根,无需额外权限