---
title: "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` | 文件挂载（只读，或可写） |

```java
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、运行时动态管理挂载

```java
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、读取挂载内容

```java
engine.getMounts();                           // 全部挂载（Collection<Mount>）
engine.getSkills();                           // 全部技能（Collection<SkillDescriptor>）
engine.getSkillsByMount("@global-skills");    // 按挂载筛选技能
engine.getAgents();                           // 全部子代理（Collection<AgentDescriptor>）
engine.getAgentsByMount("@global-agents");    // 按挂载筛选子代理
```
