tancheng33/dsh-spill-s3
S3-compatible spill backend for DeepSeek Harness: oversized tool output goes to object storage (AWS S3, MinIO, R2) instead of the agent's local disk
已收录
0
Dev
Bundle 已验证
功能介绍
spill seam 的 S3 兼容后端:超长工具输出写入对象存储(S3、MinIO、R2),会话前缀经哈希、键不可猜测,而不是落在 agent 的本地磁盘上。
适合
- 需要让超长工具输出脱离 Agent 主机并长期存在的分布式或容器化 DSH 部署。
- 已使用 AWS S3、MinIO、R2 或其他 S3 兼容对象存储共享产物的团队。
- 需要逐次上传解析凭证、哈希会话前缀和不可猜测对象键的运维人员。
不适合
- 本机私有 spill 存储已满足访问与持久性需求的单机工作流。
- 没有预先创建的 S3 兼容存储桶及相应凭证的环境;插件不会创建存储桶。
- 需要内置检索、搜索、删除、分片上传或客户端加密的工作流。
README
dsh-spill-s3
| English | 中文 |
An S3-compatible backend for the DeepSeek Harness spill storage seam (ctx.spillStore). Oversized tool output goes to object storage — AWS S3, MinIO, Cloudflare R2, or any S3-compatible server — instead of the disk of whichever machine happened to run the agent.
Why
When a tool result is too large for the model’s context, the harness spills it: the full text is persisted and the model gets a locator plus retrieval guidance. The shipped backend, @deepseek-ai/dsh-spill-local, writes that text to a private directory on the local host. That is the right default, and the wrong deployment for a team:
dsh-spill-local |
dsh-spill-s3 |
|
|---|---|---|
| Where artifacts live | a private dir on the agent’s host | your bucket |
| A headless/containerized run | artifacts die with the container | artifacts outlive it |
| Another engineer wants to look | shell onto that host | already has bucket access |
| Encryption at rest / lifecycle / retention | whatever the host does | whatever your bucket policy says |
This plugin swaps the substrate, not the policy. It implements the one method the seam declares — saveText — and leaves retention (@deepseek-ai/dsh-output-retention) and tool-result replacement (@deepseek-ai/dsh-spill-policy) exactly where they already live.
Install
dsh plugin --profile <name> add dsh-spill-s3
The bundle patch disables spill-local as it inserts this row: ctx.spillStore accepts exactly one implementation per context, and loading a second throws cordis’ duplicate-service error.
bucket ships empty on purpose — the row fails validation until you set it. Silently defaulting a bucket name is how spilled output ends up somewhere nobody intended. Set it in your profile’s 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 virtual-host style
accessKeyIdRef: AWS_ACCESS_KEY_ID
secretAccessKeyRef: AWS_SECRET_ACCESS_KEY
sessionTokenRef: AWS_SESSION_TOKEN
serverSideEncryption: AES256
retrieval: cli
presignExpiresSeconds: 3600
timeoutMs: 30000
A patch replaces a row’s whole config, so restate every key you want to keep.
MinIO / R2 / self-hosted
- id: spill-s3
config:
endpoint: http://127.0.0.1:9000
region: us-east-1 # any consistent value; it only scopes the signature
bucket: agent-spill
forcePathStyle: true # required on a bare IP — no wildcard DNS
serverSideEncryption: '' # some servers reject the header
# …restate the rest
Configuration
| Key | Default | Meaning |
|---|---|---|
endpoint |
https://s3.us-east-1.amazonaws.com |
Service endpoint origin. |
region |
us-east-1 |
Region in the SigV4 credential scope. S3-compatible servers accept any consistent value. |
bucket |
(required) | Destination bucket. It must already exist — this plugin never creates one. |
prefix |
dsh-spill |
Key prefix. Artifacts land under <prefix>/session-<hash>/. |
forcePathStyle |
true |
host/bucket/key addressing. Required by MinIO and bare-IP endpoints; set false for AWS virtual-host style. |
accessKeyIdRef |
AWS_ACCESS_KEY_ID |
Credential reference — a name, not a value. |
secretAccessKeyRef |
AWS_SECRET_ACCESS_KEY |
Reference for the secret key. |
sessionTokenRef |
AWS_SESSION_TOKEN |
Reference for an STS session token. Ignored when unset. |
serverSideEncryption |
AES256 |
x-amz-server-side-encryption value. Empty sends no header. |
retrieval |
cli |
How the model is told to read an artifact: cli, presigned, or locator-only. |
presignExpiresSeconds |
3600 |
Presigned URL lifetime (1..604800). |
timeoutMs |
30000 |
Upload timeout. |
Credentials are references, not values
accessKeyIdRef names a credential; the value is resolved per upload through ctx.credentials, falling back to the process environment when no credential provider is mounted. Nothing secret belongs in cordis.patch.yml.
Because resolution is per operation (the seam’s own contract), a rotated key reaches the next upload without restarting anything. Pair it with a central secret store — e.g. dsh-credentials-vault — and the agent host never holds a long-lived AWS key at all.
Choosing a retrieval mode
| Mode | The model is told | Cost |
|---|---|---|
cli (default)
|
run aws s3 cp s3://…
|
needs the AWS CLI and credentials on the machine running commands |
presigned |
fetch this URL | puts a bearer URL in the model’s context and the durable session log |
locator-only |
ask the user | safest; the model cannot self-serve |
presigned is genuinely useful — it makes a spilled artifact readable with a plain web_fetch — but a presigned URL is a bearer credential with a lifetime. It is opt-in for that reason.
Key layout
<prefix>/session-<sha256(sessionId)[0:16]>/<18 random hex>-<safe name>
-
The session id is hashed. Bucket keys are visible to every principal with
s3:ListBucketand are copied into inventories, access logs, and analytics pipelines. Per-session grouping survives; the id does not leak. - The random component precedes the name. It satisfies the seam’s collision-free requirement and makes a key unguessable to someone holding only prefix-level read. Putting it first also keeps a prefix listing from clustering by tool name.
-
The suggested name is sanitized, never trusted.
[A-Za-z0-9._-]is kept, everything else collapses to-, runs of two or more dots become-(so..never appears in a derived key), and leading dots/dashes are stripped. The seam callssuggestedName“a hint, never a path”, and this treats it as one.
Design notes
No AWS SDK. The signing is ~150 lines of node:crypto against the published SigV4 contract. @aws-sdk/client-s3 is tens of megabytes of transitive dependencies for a single PUT, and a spill backend that inflates every install is a spill backend nobody mounts. The trade — owning a signer — is bounded because SigV4 is a stable wire format, and it is checked against a real server (below) rather than only against itself.
The signature covers the real payload digest, not UNSIGNED-PAYLOAD. The body is already in memory, and a signed digest makes the stored artifact tamper-evident in transit.
saveText rejects on a storage failure, per the seam contract — it never returns a locator for an object that was not written. The spill policy treats a rejection as best-effort and keeps the inline result, so a bucket outage degrades to today’s behavior instead of losing output. Failures carry a machine-readable kind: network (never reached the server), http (the server refused, with status), or config.
Cancellation is chained. The caller’s AbortSignal and the configured timeout both abort the in-flight request, so a spill cannot hold a cancelled tool result open.
Tests
50 tests, including four that run against a real S3-compatible server. Unit tests pin the shape of a signature; only a live server proves it is correct — a self-consistent but wrong signer would pass every unit test and fail every upload.
npm test # unit tests only
# with a live server (verifies signing, presigned GET, and percent-encoded keys)
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
Limitations
-
saveTextonly. The seam declares one method, and this implements exactly it. There is no retrieval, search, or delete API here — retrieval is whatretrievalHintdescribes, and deletion belongs to your bucket’s lifecycle policy. - The bucket must exist. Creating buckets requires privileges a spill backend should not hold.
-
No multipart upload. Spilled tool output is a single
PUT. Objects beyond the 5 GiB single-PUTlimit are not supported; the harness’s own output caps make that unreachable in practice. -
isolation-style claims. Server-side encryption is whatever your bucket and theserverSideEncryptionheader negotiate. This plugin does not encrypt client-side, and does not claim to.
License
MIT
常见问题常见问题
在启用了 DSH 的终端中执行已验证命令 dsh plugin --profile default add github:tancheng33/dsh-spill-s3。命令会解析公开 package 元数据,并保持插件与本页展示的目录身份一致。
兼容性以页面上展示的 bundle 与 profile 状态为准。如果某个 profile 尚未检测到,请先保持禁用,并在生产启用前阅读仓库文档。
GitHub 链接和 activity 元数据是 release 与维护状态的来源。新版本发布后重新查看本页,确认目录已经观察到最新版本。