modestoma
dsh-webfile
S3 & FTP file tools for DeepSeek Harness: 8 agent tools (list, stat, mkdir, delete, move, copy, download, upload) — approval-gated mutations, transfer jobs with progress & cancel.
- Stars
- 0
- Language
- TypeScript
- Created
- Aug 15, 2026
- Updated
- Aug 15, 2026
Introduction
dsh-webfile
S3 & FTP file tools for DeepSeek Harness: 8 agent tools (list, stat, mkdir, delete, move, copy, download, upload) — approval-gated mutations, transfer jobs with progress & cancel.
Let your agent browse and manage files on S3 (including MinIO and compatible object stores) and FTP/FTPS directly in conversation:
- Read-only pass-through:
webfile_list/webfile_statnever require approval; - Per-call approval for mutations: the other 6 tools ask the user before every call — a rejection means zero side effects;
- Transfers run as background jobs: download/upload stream progress into the Jobs panel and can be cancelled at any time;
- Credentials by reference only: secret values never enter config or the session log; rotations take effect on the next call.
Install
dsh plugin --profile <name> add dsh-webfile
The package declares dsh.bundle, so installing auto-mounts it into the profile's layer stack — no manual rows needed. To upgrade:
dsh plugin --profile <name> update
Configuration
Override the plugin config by id in the profile's cordis.patch.yml ($DSH_HOME/profiles/<name>/cordis.patch.yml):
- id: dsh-webfile
config:
connections:
prod-logs:
protocol: s3
endpoint: https://oss.example.com # omit for AWS public cloud
region: cn-north-1
bucket: prod-logs
pathStyle: true # required for MinIO
accessKeyRef: OSS_ACCESS_KEY # credential reference name, see below
secretKeyRef: OSS_SECRET_KEY
legacy-ftp:
protocol: ftp
host: ftp.example.com
port: 21
userRef: FTP_USER
passwordRef: FTP_PASSWORD
tls: explicit # none | explicit | implicit
maxTransferBytes: 2147483648 # per-file transfer cap, default 2 GiB
multipartThresholdBytes: 67108864 # S3 multipart threshold, default 64 MiB
Connection fields
| Field | Protocol | Notes |
|---|---|---|
protocol | both | s3 or ftp (required) |
endpoint | s3 | Custom endpoint (MinIO etc.); omit for AWS public cloud |
region | s3 | Region (required) |
bucket | s3 | Default bucket |
pathStyle | s3 | Path-style addressing; MinIO needs true |
accessKeyRef / secretKeyRef / sessionTokenRef | s3 | Credential reference names, see below |
host | ftp | Server address (required) |
port | ftp | Default 21; 990 for implicit FTPS |
userRef / passwordRef | ftp | Credential reference names; omit for anonymous login |
tls | ftp | none / explicit / implicit (required) |
passive | ftp | Default true; only passive mode is supported |
Patch semantics
- Target by id: patch rows without an
idare skipped with a warning; - Whole-value replace:
configshallow-replaces the entire value; omitted fields fall back to schema defaults; - Do not re-mount: never
insertanother row with the same name in the user layer — configure through id overrides only; - Disable:
- id: dsh-webfile / disabled: trueturns the plugin off for that profile; - Hot reload: long-lived surfaces watch this file — saving replays the patch transactionally via HMR, no restart needed; a parse failure fails loud and keeps the last good tree.
Credentials
The config only holds credential reference names (environment-variable names, POSIX identifiers). Secret values live in $DSH_HOME/.credentials.yaml:
OSS_ACCESS_KEY: AKIAxxxxxxxx
OSS_SECRET_KEY: xxxxxxxxxxxx
FTP_USER: logbot
FTP_PASSWORD: xxxxxxxx
- Four-layer precedence: process environment > credentials file > project
.env> user.env; the process environment is read-only and shadows same-named file entries; - Hot publish: the file is watched (100 ms debounce) — an external edit takes effect on the next tool call, zero-restart rotation;
- Format: strictly a
reference: string valuemap (not dotenv syntax); values must be non-empty; file permissions must be0600; - Secret values never enter the config surface or the session log.
Tools
| Tool | Parameters | Approval | Background job |
|---|---|---|---|
webfile_list | connection · remotePath · maxEntries (default 200, cap 1000) · nextToken | none | no |
webfile_stat | connection · remotePath | none | no |
webfile_mkdir | connection · remotePath | required | no |
webfile_delete | connection · remotePath · recursive (default false) | required | no |
webfile_move | connection · sourcePath · targetPath · overwrite (default false) | required | no |
webfile_copy | connection · sourcePath · targetPath · overwrite (default false) | required | no |
webfile_download | connection · remotePath · localPath (optional; defaults to mirroring under the workspace root) | required | yes |
webfile_upload | connection · localPath · remotePath · overwrite (default false) | required | yes |
Semantics
- S3 has no real directories: directories are key-prefix aggregations;
mkdirwrites a zero-bytekey/marker;statsynthesizes a directory for prefixes that have children but no object of their own. - List pagination: truncated results return
truncated: trueplus anextToken(S3ContinuationTokenpassthrough) — pass it back to continue; FTP has no server-side continuation, so retry with a largermaxEntries. - Overwrite protection:
overwritedefaults tofalse; an existing target raisesWEBBUF_EXISTSwith a hint to enable it. - Recursion protection:
deleterefuses non-empty directories withWEBBUF_DIR_NOT_EMPTY;recursive: truedeletes the whole subtree. - Transfer cap:
maxTransferBytes(default 2 GiB) is checked before the operation starts; oversized transfers raiseWEBBUF_TOO_LARGEwith zero remote side effects. - S3 large uploads: at or above
multipartThresholdBytes(default 64 MiB) uploads use multipart with monotonic progress. - Cancellation: killing a transfer job aborts the stream, removes the local half-file, and settles the job as
killed. - S3 directory move/copy: per-object copy (+delete) — large directories are slow and an interruption can leave a half-moved state, which the tool description and approval reason state explicitly.
- FTP symlinks: surfaced as
type=link; recursive delete removes the link itself and never follows it; copy refuses symlinks outright.
Security notes
- Per-call authorization: every mutation asks independently — no session-wide exemptions;
allowed-onceis the only grant. - Fail closed: with no approval channel (e.g. pure headless without an answerer) or with session policy
approval/policy: never, mutation tools are always denied — never silently allowed. - Session log: tool arguments (connection id, remotePath, …) enter the session log with the call — mind paths that are sensitive; secret values never do.
- S3
CopyObjectcannot be aborted: server-side copies, once issued, cannot be cancelled — relevant for large directory move/copy. - Downloads land through
ctx.fsinto the workspace, governed by the DSH file sandbox policy.
Error codes
| Code | Meaning |
|---|---|
WEBBUF_UNKNOWN_CONNECTION | Connection id not configured (message lists the available ids) |
WEBBUF_CREDENTIAL_MISSING | A declared credential reference has no value (message names the variable) |
WEBBUF_NOT_FOUND | Path does not exist |
WEBBUF_EXISTS | Target exists and overwrite: false |
WEBBUF_DIR_NOT_EMPTY | Non-empty directory and recursive: false |
WEBBUF_PATH_TRAVERSAL | Path contains .. or is absolute |
WEBBUF_TOO_LARGE | Exceeds maxTransferBytes |
WEBBUF_PROTOCOL_ERROR | Protocol-level error (e.g. FTP refusing to follow a symlink) |
Compatibility
- Node:
^22.19.0 || >=24.0.0(aligned with DeepSeek Harness). - S3: any object store implementing ListObjectsV2 / HeadObject / GetObject / PutObject / DeleteObject(s) / CopyObject / multipart (AWS, MinIO, and S3-compatible services).
- FTP/FTPS: MLSD-capable servers yield the fullest listings (precise timestamps); older LIST-only servers work but lack time fields and infer directory types from permissions; both explicit and implicit TLS are supported.
- One short-lived connection per operation in v1 (no pooling) — extremely high-frequency workloads are a later milestone.
Development
The committed manifest and lockfile use registry packages so local development and CI resolve the same dependency graph:
pnpm install
pnpm check # build + lint + tests
For cross-repository development, use pnpm link <package-dir>... to replace selected packages in node_modules without editing package.json or pnpm-lock.yaml.
Commit convention: <type>(<scope>): <summary>. The lefthook commit-msg hook enforces local commits after pnpm exec lefthook install; CI applies the same rule to pull request titles.
Releasing: fully automated via semantic-release — every merge to main analyzes the conventional commit history, bumps the version (fix → patch, feat → minor, e.g. 0.1.1 → 0.2.0), publishes to npm, pushes a version tag, and files a GitHub Release whose notes are generated from the commits. The source manifest intentionally remains at 0.0.0; the Git tag is the version record.
The first publish needs a granular automation token in the NPM_TOKEN GitHub Actions secret. Once the package exists, configure npm Trusted Publishing for modestoma/dsh-webfile and workflow ci.yml, then delete the secret and its workflow environment entry. Later publishes use short-lived OIDC credentials and carry npm provenance attestations.
License
MIT © 2026 modesto