Back to home@roojay

dsh-stream-upload

DeepSeek Harness hybrid attachment plugin: native images plus bounded-memory workspace uploads

Stars
0
Language
JavaScript
Created
Sep 7, 2026
Updated
Sep 7, 2026
GitHub repo

Introduction

dsh-stream-upload

English | 中文

A dual-face DeepSeek Harness plugin for hybrid attachments from the Web composer. PNG/JPEG/WebP/GIF files are handed to the official durable image-attachment pipeline. Everything else — code, text, PDFs, DOCX/XLSX, archives — is streamed as raw bytes into the session workspace where the agent's own file tools already operate.

How it works

browser composer                          dsh host
┌─────────────────────┐                  ┌──────────────────────────────────────┐
│ 📎 button           │ image files       │ native draft image + attachment store│
│ (conversation.input │──────────────────▶│ stock preview / validation / model   │
│  .left slot entry)  │ other raw bytes   │ trust + quota → exclusive temp file  │
│ draft ← path insert │──────────────────▶│ sha256 → atomic publish in workspace │
└─────────────────────┘                  └──────────────────────────────────────┘
  • Native images: raster images use DSH's createDraftImagesaddImages path and stock attachment rail; this plugin never stores a duplicate image object.
  • Bounded-memory files: non-images stream socket→exclusive 0600 temp file while a full SHA-256 is calculated. Quotas pass before an atomic hard-link publish; aborted and rejected bodies leave no partial file.
  • Trust fence: /api/upload is a longer prefix than /api, so it wins dispatch and must re-implement the official browser fence itself — Host ∈ loopback ∪ connection.trustedHosts, sec-fetch-site ≠ cross-site, Origin authority match. Reverse-proxy deployments behind --trusted-host keep working unchanged; everything else gets 403.
  • Per-session isolation: a live session resolves its own workspace, storage roots and session directories are realpath-contained and cannot be symlinks, directories are 0700, and the API returns only workspace-relative references.
  • Lifecycle and quotas: full-SHA-256 content dedup, per-file/session/workspace caps, file count and free-space admission, bounded concurrency, request timeout and per-file TTL sweep.
  • Composer-native queue: a compact horizontal strip lives in the official conversation.input.dock immediately above the composer. It shows every selected file, byte-accurate progress for workspace uploads, native-image attachment state, size, completion or the retained failure reason. Active batches can be cancelled; successful/cancelled batches collapse after five seconds while failures remain until dismissed. Visual progress uses ARIA progress bars; state transitions use a polite live region. One borderless strip fills the composer's 780 px maximum width: a single attachment expands across the available row and the cancel/dismiss action stays inside the same visual surface. Multiple attachments share the row and scroll horizontally only after reaching their compact minimum width.
  • Composer integration: the button is a conversation.input.left slot entry. Every successful non-image upload appends one CommonMark resource link, [display name](<workspace/relative/path>), through the official inputActions.setDraft provide-channel face — no DOM scraping anywhere.

Install

This package uses ESM and has no prepare script, so installing it from Git does not require allowBuilds. Pin DSH to @deepseek-ai/dsh@0.1.2-rc.1.

dsh plugin --profile web add github:roojay/dsh-stream-upload#v0.4.0

Install from npm (once published):

dsh plugin --profile web add dsh-stream-upload@0.4.0

Install from a GitHub Release tarball:

dsh plugin --profile web add https://github.com/roojay/dsh-stream-upload/releases/download/v0.4.0/dsh-stream-upload-0.4.0.tgz

Install from a local directory:

cd /absolute/path/to/dsh-stream-upload
pnpm install --ignore-scripts
dsh plugin --profile web add "$PWD"

Restart the Web process after installation:

systemctl restart dsh   # or however your deployment restarts `dsh web`

Remove the plugin:

dsh plugin --profile web remove dsh-stream-upload

Config (cordis.patch.yml row)

keydefaultmeaning
maxBytes99614720 (95 MiB)hard cap for one non-image upload body; stays below Cloudflare's 100 MB request ceiling
allowedExtensions[]lowercase allowlist; empty = all allowed
uploadDirName.dsh-uploadsworkspace-relative storage directory
uploadTtlMs604800000 (7 d)stale upload lifetime
sweepIntervalMs3600000 (1 h)sweep interval; 0 disables
maxConcurrentUploads2concurrent upload bodies
maxSessionBytes1073741824 (1 GiB)retained bytes per session
maxTotalBytes2147483648 (2 GiB)retained bytes below one workspace upload root
maxFilesPerSession100retained files per session
minFreeBytes536870912 (512 MiB)filesystem free-space reserve
requestTimeoutMs120000idle request timeout

Upload feedback

The UI follows the established status-bar/file-card pattern: queued → uploading/attaching → complete, failed or cancelled. The paperclip remains in conversation.input.left; the status strip is a separate conversation.input.dock entry at order 30, sharing one isolated observable store per session. Ordinary files use XMLHttpRequestUpload.progress because the browser Fetch API does not expose portable upload-byte progress; the host route and raw streaming protocol are unchanged. Success is not inferred from reaching 100% of sent bytes: the chip becomes complete only after the server returns a validated workspace-relative reference.

Limits / roadmap

  • v1 stores binaries as-is: reading PDF/DOCX content needs an agent-side tool or conversion step (see ecosystem plugins like markitdown converters if you want automatic document→Markdown).
  • No folder upload or global drop handler; use the paperclip. The smaller surface avoids directory traversal, recursive quota and conflict problems.
  • Uploads are whole-request transfers, not resumable chunks. Cancelling removes the host .part file; retrying a failed file currently means selecting it again.

Tests

npm test   # node --test test/*.test.js — fence scenarios, transport, sweeper