harness - 目录结构与挂载机制
HarnessEngine.of(workspace, harnessHome) 的两个参数,定义了马具的两个基准位置:
workspace:工作区。Agent 默认的当前工作目录(文件查找、读写、命令执行都以它为根)。harnessHome:马具主目录(默认.solon/,例:.soloncode/)。马具的运行态数据都落在这里。
1、马具主目录(harnessHome)
主目录下按用途划分为若干子目录,按需自动创建/读取:
| 子目录 | 用途 |
|---|---|
sessions/ | 会话与 Todo 任务清单的持久化 |
skills/ | 技能(SKILL.md) |
agents/ | 子代理定义(xxx.md) |
commands/ | 自定义命令(xxx.md) |
memory/ | 心智记忆 |
download/ | 下载缓存 |
channels/ | IM 通道绑定数据 |
对应的派生路径是 harnessHome + "<子目录>/" 的字符串拼接,可通过 engine.getHarnessHome()、engine.getHarnessSessions()、engine.getHarnessSkills()、engine.getHarnessAgents()、engine.getHarnessCommands()、engine.getHarnessMemory()、engine.getHarnessDownload()、engine.getHarnessChannels() 读取。
注意:派生目录只是约定位置,不等于"已经挂载"。若要让技能、子代理从这个目录被加载,还需要为它注册相应类型的挂载(见下文)。
2、挂载机制(Mount)
除了主目录里的约定位置,还可以通过"挂载"把外部目录接入马具,让技能、子代理来自全局共享库或第三方目录。
自 4.1.1 起,挂载由两部分共同表达(旧类型 MountDir 已删除):
Mount:挂载的元数据与策略(别名、类型、启用/可写/可见)。MountSource:内容来源,负责真正的读取与写入能力。本地目录用FileMountSource,Classpath/Jar 等虚拟来源用ClasspathMountSource。
类型由 MountType 区分:
| 类型 | 说明 |
|---|---|
SKILLS | 技能挂载(发现并加载 SKILL.md) |
AGENTS | 子代理挂载(发现并加载子代理 md) |
FILES | 文件挂载(只读,或可写) |
HarnessEngine engine = HarnessEngine.of("work", ".soloncode/")
.sessionProvider(sessionProvider)
.mountAdd(Mount.builder()
.alias("@global-agents") // 虚拟别名,可省略 @,注册时会自动补齐
.description("全局共享子代理") // 可选;会出现在引导词的挂载点清单里
.type(MountType.AGENTS) // 挂载类型
.source(FileMountSource.of( // 内容来源(本地目录)
Paths.get(System.getProperty("user.home"), ".soloncode/agents")))
.primary(true) // 主挂载标记
.writeable(false) // 是否可写(默认只读)
.build())
.mountAdd(Mount.builder()
.alias("@global-skills")
.type(MountType.SKILLS)
.source(FileMountSource.of("team/skills")) // 相对工作区的路径
.build())
.build();
Mount.builder() 的字段说明:
| 字段 | 默认值 | 描述 |
|---|---|---|
alias | / | 虚拟别名。注册时自动补齐 @;但不得为 @ 或 @workspace,且不能包含 /、\ 或空白字符 |
type | FILES | 挂载类型(SKILLS / AGENTS / FILES) |
source | / | 内容来源(MountSource)。未设置时 build() 直接抛 IllegalArgumentException |
description | null | 挂载描述,用于引导词中的挂载点清单 |
primary | false | 是否为主挂载的标记 |
enabled | true | 是否启用;禁用的挂载在目录扫描与逻辑路径解析时都不可用 |
writeable | false | 是否可写(挂载默认只读) |
visible | true | 是否在展示层出现。它只影响引导词里的挂载清单,不影响挂载的启用、解析与运行时使用 |
路径语法说明:
- 本地来源直接用
FileMountSource.of(String|Path)(内部即Paths.get(...))。 ~/(用户目录相对)与./(工作区相对)的展开由MountManager.parseRealPath(String)负责,例如new MountManager(workspace).parseRealPath("~/.soloncode/agents/");需要该语法时先展开再交给FileMountSource。- 工作区本身也以
FileMountSource的形式存在于MountManager中,其保留别名为@workspace。
3、内置挂载(@harness-agents)
引擎构造时会自动注册一个内置挂载:
- 别名
@harness-agents,类型AGENTS,primary=true、enabled=true、writeable=false; - 来源是 classpath 的
META-INF/solon/ai/harness/agents/(ClasspathMountSource.of(HarnessEngine.class.getClassLoader(), ...)); - 其中包含 5 个内置子代理:
bash、explore、general、git-summary、plan(git-summary的 front matter 声明了hidden: true)。
该别名受到保护:HarnessEngine.Builder.mountAdd(...)、engine.addMount(...) 与 engine.removeMount(...) 命中 @harness-agents 时都会抛 IllegalArgumentException("Built-in mount cannot be replaced/removed"),即既不能被同名覆盖,也不能被移除。
由于它的类型是 AGENTS:
- 不会出现在引导词的
<mount_list>中(该清单按enabled && visible && type != AGENTS过滤); - 不进入 OS 级沙盒的文件系统白名单(该白名单按
enabled && type != AGENTS过滤)。
也就是说,子代理挂载属于"能力来源",不参与文件工具的路径白名单。
4、路径别名(@别名)
挂载后,可在工具调用中直接用 @别名 引用其内容(如 @global-skills/xxx),由马具自动转换到真实来源路径;工作区内的访问则使用相对路径(如 src/app.java)。
引导词的挂载点清单会为每个挂载声明 shell 可达性(shell 属性):
shell | 含义 |
|---|---|
shell="true" | 本地来源(FileMountSource),可用逻辑路径 cd/执行 |
shell="materialized" | 可物化的虚拟来源(如 classpath),首次引用时物化到用户级缓存后执行;同时给出 scheme |
shell="false" | 其余纯虚拟来源,只能使用文件工具 |
5、挂载物化缓存(shell="materialized")
Classpath/Jar 这类虚拟来源没有本地目录,无法直接在 bash 里 cd。为此 MountSource 提供了物化能力:capabilities().isMaterializable() 声明能力,materialize(path, targetDirectory) 落地内容,getFingerprint() 提供来源指纹;物化由 CLI 侧的 MountMaterializer 执行。
- 缓存根目录:
~/.soloncode/cache/mount(相对用户主目录;user.home不可用时回退到工作区)。 - 目录结构:
<cacheRoot>/<alias>/<指纹哈希>。 - 完整性:目录内写有
.ready(物化完成标记)与.manifest(完整性摘要);落盘采用唯一 staging 目录 + 原子 rename,读者要么看到完整快照,要么触发重物化。 - 生命周期:缓存跨工作区复用,不随
TerminalTalent关闭而删除(Talent 关闭后仍可命中)。 - 安全:文件工具层拒绝直接读写该缓存目录(mandatory deny,同时在 files 与 dirs 两个口径登记);启用 OS 级沙盒时该目录进入 allowRead 且进入 denyWrite;未启用 OS 沙盒时由 manifest 在下一次命中时发现普通文件改动并重建。
FileMountSource恒不参与物化(能力位为 false),它本身就是本地来源。
6、运行时动态管理挂载
engine.addMount(Mount.builder()
.alias("@team-skills")
.type(MountType.SKILLS)
.source(FileMountSource.of("team/skills"))
.build()); // 添加挂载(同别名会替换)
engine.hasMount("@team-skills"); // 是否存在
engine.getMount("@team-skills"); // 取挂载对象
engine.refreshMount("@team-skills"); // 刷新(清理该挂载的运行时定义并重新扫描其目录)
engine.refreshMount(null); // 刷新全部挂载(清自定义代理 + 全量刷新技能/子代理目录)
engine.removeMount("@team-skills"); // 移除(同时清理该挂载关联的运行时定义)
行为细节:
addMount:若别名已是@harness-agents抛异常;同别名替换时先按别名清理旧的运行时定义,再注册并刷新SKILLS/AGENTS目录。removeMount:内置别名抛异常;否则先清理该别名关联的定义,再移除挂载。refreshMount(alias):清理该挂载关联的定义并刷新对应目录;refreshMount(null)清除全部自定义代理(保留内置)并整体刷新两个目录。primary仅是一个标记,不提供删除保护。
7、读取挂载内容
engine.getMounts(); // 全部挂载(Collection<Mount>)
engine.getSkills(); // 全部技能(Collection<SkillDescriptor>)
engine.getSkillsByMount("@global-skills"); // 按挂载筛选技能
engine.getAgents(); // 全部子代理(Collection<AgentDescriptor>)
engine.getAgentsByMount("@global-agents"); // 按挂载筛选子代理