dsh-attachment-s3
文件与数据 活跃维护

dsh-attachment-s3

guaguasong/dsh-attachment-s3

附件存储场景的S3后端插件,支持内容寻址、一次写入的存储对象,读取时可做完整性校验,仅需执行一行插件命令即可完成安装部署。

0
Stars 标星
0
Forks 分支
0
Watchers 关注
0
Open Issues
TypeScript
主要语言
MIT
开源协议
56 KB
仓库大小
1 个月前
最后推送
一键安装扩展 / 插件指令
dsh plugin --profile web add github:guaguasong/dsh-attachment-s3
git clone https://github.com/guaguasong/dsh-attachment-s3.git
git clone git@github.com:guaguasong/dsh-attachment-s3.git
README.md main

@guaguasong/dsh-attachment-s3

English | 中文

S3 storage for the DeepSeek Harness attachment seam. It implements AttachmentStore — the same abstract service the in-box @deepseek-ai/dsh-attachment-local backend implements over DSH_HOME — so a deployment's session images live in a bucket instead of on the machine that admitted them. The seam takes exactly one provider, so this plugin replaces the local backend rather than joining it.

Nothing about the bucket is model-visible: the durable reference stays the opaque sha256: id already recorded in the session log, so moving between backends does not change what a transcript means.

Install

dsh plugin --profile <name> add @guaguasong/dsh-attachment-s3
export DSH_ATTACHMENT_S3_BUCKET=my-attachments
export DSH_ATTACHMENT_S3_REGION=us-east-1
dsh --profile <name>

The package declares "dsh": { "bundle": { "patch": "./cordis.patch.yml" } }, so dsh plugin appends it to the profile's bundle layer stack. Its patch disables the attachment-local row dsh-base inserts and adds the attachment-s3 row. Check both before booting:

dsh --profile <name> --dump-config | grep -A2 'id: attachment'

dsh plugin --profile <name> remove @guaguasong/dsh-attachment-s3 reverses the install and restores the local backend.

Environment

The bundle patch reads its values from the environment at mount. DSH_-prefixed names must come from the launching environment — export them or set them in the unit that starts dsh; the launcher refuses that prefix inside a .env file. Credential values are yours to name, so they can live in $DSH_HOME/.env.

Variable Config field
DSH_ATTACHMENT_S3_BUCKET bucket — required; unset fails the boot on this row rather than storing attachments elsewhere
DSH_ATTACHMENT_S3_REGION region
DSH_ATTACHMENT_S3_ENDPOINT endpoint — an S3-compatible service
DSH_ATTACHMENT_S3_FORCE_PATH_STYLE forcePathStyletrue enables it
DSH_ATTACHMENT_S3_PREFIX prefix
DSH_ATTACHMENT_S3_ACCESS_KEY_ID_REF accessKeyIdRef — the name of the variable holding the key, not the key
DSH_ATTACHMENT_S3_SECRET_ACCESS_KEY_REF secretAccessKeyRef
DSH_ATTACHMENT_S3_SESSION_TOKEN_REF sessionTokenRef

To pin configuration instead of reading the environment, restate the row in $DSH_HOME/profiles/<name>/cordis.patch.yml, which is applied after every bundle layer. An id-targeted patch replaces the row's whole config, so restate the fields you keep:

- id: attachment-s3
  name: '@guaguasong/dsh-attachment-s3'
  config:
    bucket: my-attachments
    region: us-east-1

Configuration

Field Default Meaning
bucket — (required) Bucket holding every attachment object.
region SDK resolution Bucket region.
endpoint AWS S3 Endpoint of an S3-compatible service.
forcePathStyle false Path-style addressing, required by most S3-compatible services.
prefix attachments/v1 Key prefix owning this deployment's objects.
accessKeyIdRef Environment-variable name holding the access key id.
secretAccessKeyRef Environment-variable name holding the secret access key.
sessionTokenRef Environment-variable name holding a session token.
maxImageBytes 5 MiB Maximum encoded bytes for one image.
maxImagesPerMessage 20 Maximum images in one submitted message.
maxMessageImageBytes 100 MiB Maximum aggregate encoded image bytes in one message.
maxImagePixels 40,000,000 Maximum intrinsic width × height for one image.
maxImageDimension 2000 Maximum intrinsic width and height, applied per side.

The admission limits carry the same defaults as the local backend, so switching backends does not change which images a deployment accepts. Object policy a bucket already owns — default encryption, storage class, lifecycle — is left to the bucket.

Configuration carries credential references, never values, matching the harness credential seam. Each is resolved per request, through ctx.credentials when a credential provider is loaded and through the process environment otherwise, so a rotated secret reaches the next request without a restart. Declare both key references or neither: a half-declared pair fails at load instead of silently signing as the SDK's ambient identity. Declaring neither is the normal deployment on an instance role.

How it stores

<prefix>/objects/<first two hex digits>/<sha256 hex>

One immutable object per distinct image, Content-Type set to the verified media type, the SHA-256 sent as the object checksum, and the intrinsic width/height recorded as object metadata. The session log records sha256:<hex> — an opaque id, never a key or a URL. The v1 segment fences a future incompatible layout off from stored objects.

  • Write-once. Uploads are conditional (If-None-Match: *), so concurrent writers of identical bytes never overwrite each other. The writer that loses the race verifies what is already stored instead of republishing it: the recorded checksum settles it without moving bytes, and an object stored without one is read back and compared, because a same-length substitution would otherwise publish a reference no later read can return.
  • Verified reads. A read requests exactly the byte range the reference claims, re-hashes the bytes, and re-derives the image header before returning, so a bucket-side substitution surfaces as ATTACHMENT_CORRUPT instead of reaching a model request.
  • Failure codes. Admission keeps the seam's caller-correctable codes (IMAGE_TOO_LARGE, IMAGE_TYPE_MISMATCH, IMAGE_TOO_MANY_PIXELS, IMAGE_DIMENSION_TOO_LARGE, INVALID_IMAGE); storage failures surface as ATTACHMENT_WRITE_FAILED, ATTACHMENT_READ_FAILED, ATTACHMENT_NOT_FOUND, or ATTACHMENT_CORRUPT, each carrying its cause.

S3-compatible services

The backend needs four things from a bucket: a conditional write that refuses a taken key, an object checksum it can send and read back, ranged reads, and a distinguishable "no such key". AWS S3 provides all four; other services vary. Probe one before pointing a deployment at it:

PROBE_ENDPOINT=https://s3.example.com PROBE_REGION=us-east-1 PROBE_BUCKET=<bucket> \
AWS_ACCESS_KEY_ID=... AWS_SECRET_ACCESS_KEY=... pnpm run probe

It writes and deletes one small object and reports what the service does with each behavior. A service that ignores the conditional write still works: the key is the digest of the bytes, so an overwrite writes exactly what is already there, and the writer publishing the reference is the one that wrote it — storage still deduplicates, only the upload repeats. A service that rejects the conditional write or the checksum header cannot run this backend as it stands.

Development

pnpm install       # runs `prepare`, which builds lib/
pnpm run test      # unit tests, including the real AWS SDK against a loopback S3-compatible service
pnpm run typecheck
pnpm run build
pnpm run test:e2e  # real bucket; self-skips without DSH_S3_E2E_BUCKET

pnpm run test needs no bucket and no network: tests/support/fake-s3.ts serves the S3 requests the SDK actually sends, so signing, conditional writes, range reads, and status classification are exercised for real. The e2e suite reads DSH_S3_E2E_BUCKET and optionally DSH_S3_E2E_REGION, DSH_S3_E2E_ENDPOINT, DSH_S3_E2E_FORCE_PATH_STYLE, and DSH_S3_E2E_PREFIX; it writes under a random per-run prefix and deletes what it wrote.

Publishing runs prepublishOnly first: clean, typecheck, the full suite, then build.

Known Limitations and Deferred Work

  • No retention or deletion. Objects are written and never removed; the seam has no retention policy on either backend. Bucket lifecycle rules are the only collector, and expiring an object a session still references turns that attachment into ATTACHMENT_NOT_FOUND.
  • Images only. The seam's version-one surface carries PNG, JPEG, WebP, and GIF.
  • One bucket per deployment. Routing sessions or workspaces to different buckets would need a routing layer this package does not have.
  • Full-object transfers. Reads buffer the whole image in memory, bounded by maxImageBytes.
  • Sharing needs more than attachments. Session logs stay wherever the profile's persistence backend puts them — by default $DSH_HOME/sessions, which is machine-local. A bucket makes attachments durable and centrally managed; it does not by itself make a session readable from another machine.