spill seam 的 S3 兼容后端:超长工具输出写入对象存储(S3、MinIO、R2),会话前缀经哈希、键不可猜测,而不是落在 agent 的本地磁盘上。
安装
# GitHub 源码(首次需按提示配置 allowBuilds 构建授权后重试)
dsh plugin --profile web add github:tancheng33/dsh-spill-s3
装任何插件都等于在你的机器上跑第三方代码,权限和你本人一样大——能读你的文件、用你的凭据、访问网络,工具审批管不到它。GitHub 来源的插件还会在安装时执行构建脚本。请只安装可信来源,并尽量锁定 commit(github:owner/repo#sha)。
README
English | 中文
DeepSeek Harness spill 存储 seam(ctx.spillStore)的 S3 兼容后端。超长工具输出写进对象存储——AWS S3、MinIO、Cloudflare R2 或任意 S3 兼容服务——而不是碰巧跑了这个 agent 的那台机器的本地磁盘。
为什么
当一次工具结果大到放不进模型上下文时,harness 会把它 spill 掉:全文落盘,模型拿到一个 locator 和取回指引。官方后端 @deepseek-ai/dsh-spill-local 把这段文本写进本机的一个私有目录。这个默认值是对的,但对团队部署是错的:
dsh-spill-local |
dsh-spill-s3 |
|
|---|---|---|
| 产物在哪 | agent 所在主机的私有目录 | 你的 bucket |
| headless / 容器化运行 | 产物随容器一起消失 | 产物比容器活得久 |
| 同事想看一眼 | 得 ssh 到那台机器 | 他本来就有 bucket 权限 |
| 静态加密 / 生命周期 / 保留策略 | 取决于那台主机 | 取决于你的 bucket policy |
这个插件换的是存储介质,不是策略。它只实现 seam 声明的那一个方法 saveText,保留策略(@deepseek-ai/dsh-output-retention)和工具结果替换(@deepseek-ai/dsh-spill-policy)依然留在原处。
安装
dsh plugin --profile <name> add dsh-spill-s3
bundle patch 在插入本行时会禁用 spill-local:ctx.spillStore 每个 context 只接受一个实现,加载第二个会抛 cordis 的重复服务错误。
bucket 故意留空——不填这一行就通不过校验。默认一个 bucket 名字,正是 spill 产物流到没人预期的地方的典型原因。在你自己 profile 的 cordis.patch.yml 里设置:
- id: spill-s3
config:
endpoint: https://s3.us-east-1.amazonaws.com
region: us-east-1
bucket: my-agent-spill
prefix: dsh-spill
forcePathStyle: false # AWS 虚拟主机寻址
accessKeyIdRef: AWS_ACCESS_KEY_ID
secretAccessKeyRef: AWS_SECRET_ACCESS_KEY
sessionTokenRef: AWS_SESSION_TOKEN
serverSideEncryption: AES256
retrieval: cli
presignExpiresSeconds: 3600
timeoutMs: 30000
patch 会替换一行的整个 config,所以覆盖这一行时要把想保留的键全部重写一遍。
MinIO / R2 / 自建
- id: spill-s3
config:
endpoint: http://127.0.0.1:9000
region: us-east-1 # 任意值,只要一致;它只参与签名 scope
bucket: agent-spill
forcePathStyle: true # 裸 IP 必须开——没有泛域名解析
serverSideEncryption: '' # 有些服务会拒绝这个头
# …其余键照样重写
配置
| 键 | 默认值 | 含义 |
|---|---|---|
endpoint |
https://s3.us-east-1.amazonaws.com |
服务端点 origin。 |
region |
us-east-1 |
SigV4 凭证 scope 里的 region。S3 兼容服务接受任意一致的值。 |
bucket |
(必填) | 目标 bucket,必须已存在——本插件从不创建 bucket。 |
prefix |
dsh-spill |
键前缀。产物落在 <prefix>/session-<hash>/ 下。 |
forcePathStyle |
true |
host/bucket/key 寻址。MinIO 和裸 IP 端点必须开;AWS 虚拟主机风格设 false。 |
accessKeyIdRef |
AWS_ACCESS_KEY_ID |
凭证引用——是名字,不是值。 |
secretAccessKeyRef |
AWS_SECRET_ACCESS_KEY |
secret key 的引用。 |
sessionTokenRef |
AWS_SESSION_TOKEN |
STS 会话令牌的引用。未配置时忽略。 |
serverSideEncryption |
AES256 |
x-amz-server-side-encryption 的值。留空则不发这个头。 |
retrieval |
cli |
告诉模型怎么读取产物:cli、presigned、locator-only。 |
presignExpiresSeconds |
3600 |
预签名 URL 有效期(1..604800)。 |
timeoutMs |
30000 |
上传超时。 |
凭证是引用,不是值
accessKeyIdRef 给的是一个凭证名字;值在每次上传时通过 ctx.credentials 解析,没挂凭证 provider 时回落到进程环境变量。任何密钥都不该出现在 cordis.patch.yml 里。
因为解析是逐次操作进行的(这是 seam 自己的契约),轮换后的密钥不需要重启就能用在下一次上传上。配合中心化密钥存储——例如 dsh-credentials-vault——agent 主机上根本不需要放长期 AWS 密钥。
怎么选 retrieval
| 模式 | 告诉模型 | 代价 |
|---|---|---|
cli(默认) |
执行 aws s3 cp s3://… |
需要执行命令的机器上有 AWS CLI 和凭证 |
presigned |
去 fetch 这个 URL | 会把一个 bearer URL 写进模型上下文和持久会话日志 |
locator-only |
去问用户 | 最安全;模型无法自助取回 |
presigned 确实好用——一个普通的 web_fetch 就能读回产物——但预签名 URL 本质上是一张有有效期的通行凭证。正因如此它是显式开启的。
键的结构
<prefix>/session-<sha256(sessionId)[0:16]>/<18 位随机 hex>-<安全名>
- session id 被哈希。 bucket 的键对任何有
s3:ListBucket的主体都可见,还会被复制进 inventory、访问日志和分析管道。按会话分组的能力保留了,id 不外泄。 - 随机段放在名字前面。 它满足 seam 对"不碰撞"的要求,也让只有前缀级读权限的人猜不到键。放在前面还能避免前缀列表按工具名聚簇。
- suggestedName 只做净化,从不信任。 保留
[A-Za-z0-9._-],其余折叠成-,两个及以上连续的点变成-(所以派生键里永远不会出现..),开头的点和横线被剥掉。seam 明确说suggestedName是"提示,绝不是路径",这里就按提示处理。
设计说明
不引入 AWS SDK。 签名是约 150 行基于 node:crypto 的实现,对照公开的 SigV4 契约写的。@aws-sdk/client-s3 为了一个 PUT 要带进来几十兆的传递依赖,而一个会让每次安装都变胖的 spill 后端,没人会挂。代价——自己维护一个签名器——是有界的,因为 SigV4 是稳定的线格式;而且它是对着真实服务器验证的(见下),不是只跟自己自洽。
签名覆盖真实的载荷摘要,不是 UNSIGNED-PAYLOAD。反正 body 已经在内存里了,签过的摘要能让存储产物在传输中可验篡改。
saveText 在存储失败时 reject,符合 seam 契约——它绝不会为一个没写成功的对象返回 locator。spill policy 把 reject 当作尽力而为、保留内联结果,所以 bucket 故障会退化成今天的行为,而不是丢输出。失败带机器可读的 kind:network(没到服务器)、http(服务器拒绝,含状态码)、config。
取消是链式的。 调用方的 AbortSignal 和配置的超时都会中止在途请求,所以一次 spill 不会把已取消的工具结果拖住。
测试
50 个测试,其中 4 个跑在真实的 S3 兼容服务器上。单元测试只能钉住签名的形状;只有实盘服务器能证明它是对的——一个自洽但错误的签名器能通过所有单元测试,然后每次上传都失败。
npm test # 仅单元测试
# 带实盘服务器(验证签名、预签名 GET、需转义的键)
docker run -d --name minio -p 19000:9000 \
-e MINIO_ROOT_USER=dshtest -e MINIO_ROOT_PASSWORD=dshtest12345 \
cgr.dev/chainguard/minio:latest server /data
DSH_SPILL_S3_TEST_ENDPOINT=http://127.0.0.1:19000 npm test
限制
- 只有
saveText。 seam 只声明了一个方法,这里就只实现这一个。没有取回、搜索、删除 API——取回是retrievalHint描述的事,删除属于你 bucket 的生命周期策略。 - bucket 必须已存在。 创建 bucket 需要的权限不该由一个 spill 后端持有。
- 不做分片上传。 spill 的工具输出是单次
PUT。超过单次PUT5 GiB 上限的对象不支持;harness 自身的输出上限让这在实践中不可达。 - 不做客户端加密。 服务端加密取决于你的 bucket 和
serverSideEncryption头协商的结果。本插件不做客户端加密,也不声称做。
许可证
MIT
链接
同类插件
strukto-ai/mirage#dsh★ 3453
把文件系统与 bash 提供者换成 mirage 虚拟工作区:文件工具与 shell 命令作用于挂载的资源(RAM、S3、Redis、Slack、Gmail、Notion、Postgres)而非宿主磁盘,支持按挂载点设置读/写/执行模式、按命令选择沙箱(进程内 monty、pyodide、quickjs;远程 docker、e2b、daytona),并可在虚拟终端中安装 CLI(git、gh、slack、linear、ntn、gws,或自行注册的程序树)作为命令头词。
hust-open-atom-club/oh-dsh★ 203
社区发行版:TUI、桌面端与 Web UI 统一体验,分层安装、一步到位。
Jayden-X-L/forkprobe★ 66
同一任务并行试跑多个技能,对比结果选出最优。
vlln/plugin-registry★ 44
插件生态基建:浏览器面板管理官方 repository 插件(0 patch)+ make-dsh-plugin 插件开发引导技能。
forrestchang/dsh-multica-runtime★ 38
让 dsh 运行时跑在 Multica 上。
omdsh-dev/dsh-plugin-check★ 21
插件健康检查:扫描清单协议/patch 格式/构建陷阱,零依赖只读。