@epoch-agent/server 0.9.0 → 0.10.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -115,6 +115,7 @@ tarball 里真的有那些字节。
115
115
  | `POST /api/mcp/config/apply` | `{revision}` → 把盘上那份搬进这个进程(**立刻生效**,见下) |
116
116
  | `POST /api/skills/import/preview` | 「将导入什么」,不写字节(方案 42 §六,**进程级**,见下) |
117
117
  | `POST /api/skills/import` | 从本机目录导入技能(**收路径不收字节**,见下) |
118
+ | `POST /api/skills/stage` | 把手里那一份字节交上来 → 一条本机路径(**回环绑定才给**,见下) |
118
119
  | `POST /api/skills/remove` | `{name}` → 删掉一份用户级技能(**不可撤销;只有用户级删得掉**,见下) |
119
120
  | `POST /api/roles` | 新建一个身份,写进 `~/.epoch/agents/`(**回环绑定才给**,见下) |
120
121
  | `GET /api/plugins` | 装着的 + 市场里有什么 + `pendingRestart`(**进程级**,方案 59 E2,见下) |
@@ -463,6 +464,30 @@ POST /api/sessions/abc/permission?lang=zh → 错误消息也跟着这个语
463
464
  400/403/404 **刻意相反**:`empty` 那一档带着 `issues`,而那串诊断就是用户唯一
464
465
  看得懂「为什么没导进来」的东西,`{error:{code,message}}` 塞不下它。
465
466
 
467
+ #### `POST /api/skills/stage` —— 上面那句话的**反面**(2026-09-13)
468
+
469
+ 上面那条说「收路径不收字节」,而那句话把用户堵在原地:他手里的技能就在**他这台**上。
470
+ 这一条就是那条路 —— 字节走网线过去落地、解开,回来一条**服务端本机路径**,
471
+ 接着走上面那两条。
472
+
473
+ ⚠️ **它没有推翻上面那节判据**,三条缺一不可:
474
+
475
+ 1. **落点在扫描范围之外。** 字节落进 `~/.epoch/skill-drops/`,而 `scanSkillDirs`
476
+ 扫的是 `~/.epoch/skills/`。**传上来 ≠ 装上了** —— 这一发结束时 system prompt
477
+ 里一个字都没变;
478
+ 2. **给人的那道闸门一格没动。** 出口是一条路径,下游仍是 `import/preview` →
479
+ `import`,连 `token` 那条防 TOCTOU 的缝都照旧;
480
+ 3. **它吃一道 403。** `--host 0.0.0.0` 那一档下直接拒,而且**在读 body 之前**
481
+ (body 上限 50 MiB,漏一格就是让 LAN 上的人一发一发地灌内存)。
482
+
483
+ ⚠️ 那句 403 的措辞**不能抄身份 / 插件那两条**:技能不起子进程(插件那句是假话),
484
+ 而且**这一档下导入本身没关,只是不收上传** —— 用户把文件放到那台机器上、
485
+ 把路径填进同一屏的输入框照样导得进来。那半句出路必须说出来。
486
+
487
+ ⚠️ **这一层一个技能都不装,也一个都不验。**「里面有没有技能」由紧接着那一次
488
+ `import/preview` 答(它说得出「找到三份、其中一份名字不合法」)。所以拖一个空文件夹
489
+ 进来**这一发会成功**,然后下一屏说「一份 SKILL.md 都没找到」—— 那是对的分工。
490
+
466
491
  #### `POST /api/skills/remove` —— **能加就得能删**(2026-09-01)
467
492
 
468
493
  起因是一句话:这一栏原来只有「导入技能」,拿掉一份得让用户自己去
@@ -958,6 +983,35 @@ agent」),前者只能命中这个进程已经加载好的那张角色表。
958
983
  ⚠️ 拒绝走 **200 + `{ok:false, refusal, message}`**,只有闸才是 4xx ——
959
984
  前者界面要原地显示那句话,不是弹一个网络错误。
960
985
 
986
+ ### 输入框附件(2026-09-11)
987
+
988
+ 输入框收下的**非图片**文件走这条路。两发请求:
989
+
990
+ ```
991
+ POST /api/sessions/:id/attachments?name=<文件名> 裸字节 → {name, bytes}
992
+ POST /api/sessions/:id/messages {message, attachments: [name, ...]}
993
+ ```
994
+
995
+ - **请求体是裸字节**(`application/octet-stream`),不是 multipart:这条路上只有
996
+ 一个文件、没有别的字段,而 multipart 要在服务端引一个解析器,换来的只是把一个
997
+ 查询参数搬进 body。文件名走 `?name=` 而不是请求头 —— HTTP 头是 latin-1,
998
+ 而文件名可以是任意 Unicode(`合同-终稿.pdf`);
999
+ - ⚠️ **没有大小上限,那是拍过的决定**。字节**边收边写盘**(`streamBodyToFile`),
1000
+ 峰值内存是一个 chunk。护栏换成另外两道:认证(同别的 API),以及
1001
+ `~/.epoch/attachments/` 那本 512MB 的总账(7 天 / 从最旧的会话删起)。
1002
+ ⚠️ 两道都**拦不住「当前会话里传一个 2GB 的文件」** —— 当前会话目录不参与驱逐;
1003
+ - ⚠️ **回执里那个 `name` 可能和你传的不一样**(净化过、撞名加了 `-1`)。发消息和
1004
+ 界面显示都必须用它 —— 用原名的话服务端在会话目录里找不到那个文件;
1005
+ - **`attachments` 里是名字不是路径**,这是这条链上最要紧的收窄:路径由引擎算
1006
+ (`EpochRuntime.attachments`),浏览器就没有任何办法指向那个目录之外。
1007
+ 认不出来的名字**不是 400**,是一个 `omitted: 'unreadable'` 的部件加一条收据;
1008
+ - **带了附件时空消息合法**(「拖一份 PDF、一个字不打」是这条路上最自然的一次操作)。
1009
+ 附件为空时照旧 400。
1010
+
1011
+ 产出的 `EpochFilePart` 和 `@文件` 是**同一种部件**、走同一本 200KB 的预算账、
1012
+ 收据进同一个 `files` 数组。两者的分工和三道边界写在
1013
+ [src/attachments.ts](src/attachments.ts) 的文件头。
1014
+
961
1015
  ### 逐条专家(方案 57)
962
1016
 
963
1017
  `POST /api/sessions/:id/messages` 上多一个可选字段 `role` ——「**这一条**用哪个专家」。