Solon v4.1.1

harness - 目录结构与挂载机制

</> markdown
2026年10月8日 上午9:14:24

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,且不能包含 /、\ 或空白字符
typeFILES挂载类型(SKILLS / AGENTS / FILES)
source/内容来源(MountSource)。未设置时 build() 直接抛 IllegalArgumentException
descriptionnull挂载描述,用于引导词中的挂载点清单
primaryfalse是否为主挂载的标记
enabledtrue是否启用;禁用的挂载在目录扫描与逻辑路径解析时都不可用
writeablefalse是否可写(挂载默认只读)
visibletrue是否在展示层出现。它只影响引导词里的挂载清单,不影响挂载的启用、解析与运行时使用

路径语法说明:

  • 本地来源直接用 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");    // 按挂载筛选子代理