agentscope-ai / agentscope-ai/agentscope-java

[Bug]:CompositeFilesystem 的路由前缀会劫持同名的宿主挂载点(/knowledge、/skills…),静默读不到文件

Aperta
#2,690 0 commenti 0 reazioni 0 assegnatari Vedi su GitHub
area/core/memory bug
Lingua principale
Java
Stelle
5.6k
Fork
1.3k
Merge medio
4g 12h
PR unite (30g)
77

Descrizione

> Labels 建议:`bug` / `harness` / `filesystem`

## 环境

| | |
|---|---|
| agentscope-harness | 2.0.2 |
| store | `agentscope-extensions-mysql` 2.0.2(`JdbcStore` → MySQL 8.0.21) |
| 场景 | **非 sandbox**,`CompositeFilesystem` + 远程 store,K8s 2 副本 |
| 宿主挂载 | `/repos`(源码,只读)、`/knowledge`(知识库,只读)、`/skills`(技能,只读)、`/workspace`(emptyDir,可写) |

## 现象

系统提示词里明确告诉模型「知识库根目录 = `/knowledge`,需用全路径访问」。模型照做发出 `read_file("/knowledge/services.md")`,**读不到**。

没有异常、没有 WARN,只是返回一个失败结果 —— 从日志上看不出任何异常。

## 根因

`CompositeFilesystem.routeForPath()` 在匹配前会**剥掉前导斜杠**,因此**宿主绝对路径也会参与路由前缀匹配**:

```java
// CompositeFilesystem.java:100-128
private RouteResult routeForPath(String path) {
String matchPath = stripLeadingSlash(path); // :106 "/knowledge/x.md" → "knowledge/x.md"
for (RouteEntry entry : sortedRoutes) {
...
if (matchPath.startsWith(normalizedPrefix)) { // :122 命中 "knowledge/" 路由
String suffix = matchPath.substring(normalizedPrefix.length());
String backendPath = suffix.isEmpty() ? "/" : "/" + suffix; // :124
return new RouteResult(entry.backend(), backendPath, entry.prefix());
}
}
return new RouteResult(defaultBackend, path, null);
}
```

于是 `/knowledge/services.md` 被转发到 `knowledge/` 这条路由:

- 上层 = `RemoteFilesystem`(`knowledge` 段),我们从不往这里写 → 空
- 下层 = `LocalFilesystem(workspace/knowledge, SANDBOXED)`,workspace 是 emptyDir → 空

**读取失败。而宿主机上 `/knowledge/services.md` 明明存在**,只是永远不会被访问到 —— 那需要落到 defaultBackend、由它的 `PathPolicy` 白名单放行。

`RemoteFilesystemSpec.toFilesystem()` 默认注册的 10 条路由里,`knowledge/`、`skills/`、`subagents/`、`plans/`、`memory/` 都可能与宿主挂载点撞名。我们的 `/repos` 之所以没事,纯粹因为没有叫 `repos/` 的路由。

## 复现

1. 用 `RemoteFilesystemSpec` + 任意 `BaseStore` 构建 agent
2. 在宿主机上准备 `/knowledge/services.md`,并把 `/knowledge` 加进可读根(我们是自建 `PathPolicy`;官方 spec 下 defaultBackend 是 `UNRESTRICTED`,理论上任何绝对路径都该放行)
3. 调用 `filesystem.read(rc, "/knowledge/services.md", 0, 10)`
4. 预期读到内容;实际失败 —— 请求被转发到了远程 `knowledge` 段

## 影响

- **静默失败**:无异常、无日志,只有"文件读不到"这一个现象,排查成本高
- 任何把只读卷挂在与路由前缀同名路径上的部署都会中招,而这几个名字(knowledge、skills、plans)恰恰是最自然的命名
- 我们最终只能**弃用 `knowledge/` 这条路由**(我们从不往那儿写,弃用无损),让 `/knowledge/**` 正常落 defaultBackend

## 建议

匹配前区分「宿主绝对路径」与「workspace 相对路径」,例如任一:

- 只对**不以 `/` 开头**的路径做路由匹配;以 `/` 开头的先尝试 defaultBackend,失败再回退路由(会改变现有语义,需评估)
- 提供 `RemoteFilesystemSpec#excludeHostRoots(Path...)`,匹配时显式排除这些前缀
- 至少在路由未命中任何文件、而该路径在宿主机上存在时打一条 WARN,把「疑似被路由劫持」暴露出来

## 相关

可能与 [#2683](https://github.com/agentscope-ai/agentscope-java/issues/2683) 相关,但根因不同:那条是 workspace **内部** `/knowledge` 的路径解析;本条是**外部宿主挂载点**与路由前缀撞名。

Guida per i contributori

Apri la guida per i contributori

Valutazione

Questa issue non è ancora stata valutata.

Ricevi le nuove issue nella tua casella

Un breve riepilogo di issue GitHub adatte ai principianti.