dshbase

Plugin directory / Developer / dsh-rust-sdk

dsh-rust-sdk

Unverified 42ch-dev

3 contributors

View on GitHub ↗ ← Back to plugin directory

10Stars
0Forks
0Open issues
—Language
—Last push
Cross-platformPlatform

What it does

Rust SDK for DSH(Deepseek Harness)

Our take

No short evidence report

There is no checkable per-level test record on this page. Stars, the catalog test field, a web-only flag, and whether the plugin needs an external account or service are not shown here as a pass or a failure. The catalog badge stays as published.

“Unverified” means our automated CI has not yet installed this plugin. Feature descriptions and version compatibility are the author’s claims. This is not a security audit and not an endorsement of third-party code.

Plugin author? Get the “Verified” label — submit your own evidence (screenshots, logs, or a short demo) and we'll review and flip the badge.

Submit verification evidence ↗

README

deepseek-harness-sdk

English | 中文

License
Language
crates.io

See the CHANGELOG for the release history.

Rust client SDK for the DeepSeek Harness
(DSH) runtime. The runtime is the dsh CLI booted under a named profile —
this crate spawns it as a subprocess (dsh --profile sdk, the profile
default) and speaks its stdio JSON-RPC 2.0 protocol. One crate, two layers:
the high-level Python-parity API (DeepSeekHarness / Session::run /
RunResult) and the low-level protocol client (HarnessClient).

The crate is the design twin of the official
Python SDK, sharing the
same runtime peer, wire protocol, and layering; the Python SDK surface is the
alignment baseline for every public type and error. The TypeScript SDK's
divergences are documented (notably RunResult, see below), as are the
crate's own deliberate divergences from both references (see
Deliberate divergences).

This crate is a pure client. It contains no agent, LLM, or persistence
logic — the spawned runtime process does all of that. The runtime is
bring-your-own: this crate never downloads, bundles, or ships one (see
Runtime acquisition).

Installation

cargo add deepseek-harness-sdk

or in Cargo.toml:

[dependencies]
deepseek-harness-sdk = "*"

Pick the version that suits you (cargo search deepseek-harness-sdk or the
crates.io page shows the
latest). While the crate is on a pre-release line, a bare
cargo add deepseek-harness-sdk may not resolve to the newest pre-release —
request it explicitly (e.g. cargo add deepseek-harness-sdk@0.1.0-alpha) when
you want it. The API may still change before 0.1.0.

Two prerequisites before the first run: a DSH runtime (see
Runtime acquisition) and model credentials
(DEEPSEEK_API_KEY in the environment, or Config::api_key /
Config::base_url).

Quickstart

use deepseek_harness_sdk::{Config, DeepSeekHarness, Input};
use std::time::Duration;

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    let mut harness = DeepSeekHarness::start(Config {
        dsh_bin: std::env::var("DSH_RUNTIME_BIN").ok(),
        request_timeout: Some(Duration::from_secs(120)),
        ..Config::default()
    })
    .await?;

    let session = harness.start_session(None);
    let result = session
        .run(Input::Text("Reply with exactly: ok".into()), None)
        .await?;

    println!("finish_reason: {:?}", result.finish_reason);
    println!("final_response: {}", result.final_response);

    harness.close().await?;
    Ok(())
}

DeepSeekHarness::start is eager: it resolves the runtime, resolves (and
creates) the harness home, spawns the subprocess, and completes the
initialize handshake before returning. The runtime inherits
DEEPSEEK_BASE_URL / DEEPSEEK_API_KEY from the environment unless the
crate injects overrides, so callers can use real model endpoints directly or
point those variables at a local proxy.

Runtime acquisition

The runtime is bring-your-own; the SDK only resolves and launches it. There
is no separate JSON-RPC agent program: the stdio JSON-RPC server the SDK
talks to is a plugin row inside the runtime's profile bundle. The runtime is
the ordinary dsh CLI from
deepseek-harness, booted
under the sdk profile (or any profile you name via Config::profile).

Three routes to a runtime:

Route A — the npm-published CLI (recommended)

npm install -g @deepseek-ai/dsh
export DSH_RUNTIME_BIN="$(command -v dsh)"

The dsh CLI is published on npm as @deepseek-ai/dsh. A bare
npm install -g @deepseek-ai/dsh installs the latest dist-tag, which can
lag behind upstream's newest release; npm install -g @deepseek-ai/dsh@alpha
tracks the newest. This crate's CI verifies the npm route with a keyless
handshake on every pull request, and the exact version under test lives in
.github/workflows/ci.yml.

The installed bin is a Node.js script, so Node.js must be on PATH for
the SDK to launch it. The crate spawns the resolved program directly without
a shell, and the npm bin is a Node.js script rather than a native executable
— on Windows, prefer the self-contained wheel route (Route B).

Route B — the platform wheel (self-contained, no Node.js)

python -m pip install deepseek-harness-runtime-bin
export DSH_RUNTIME_BIN="$(python -c 'import deepseek_harness_runtime as r; print(r.bundled_runtime_path())')"

The python -c invocation only locates the installed executable and prints
its path — no Python runs at SDK runtime. The SDK launches the executable
directly (always injecting the resolved DSH_HOME, so the home is explicit
even on first boot).

The wheel ships the runtime as a self-contained single-file executable — no
system Node.js needed at runtime (the plugin tree is embedded) — and installs
the normal dsh CLI as deepseek-harness-sdk-runtime-<platform>-<arch>.
Published targets are Linux x64, Linux arm64, macOS arm64, macOS x64, and
Windows x64
(Windows uses the .exe suffix); no Windows arm64 wheel is
published. macOS needs its sibling -spawn-helper beside the executable
(node-pty), and the Linux/macOS wheels carry a -rg ripgrep sidecar
(Windows -rg.exe) — copy any sidecar along when you relocate the executable.
Because the wheel needs no system Node.js at runtime, it stays the fallback
for Windows and for users who cannot install Node.js.

Route C — build from source

Build the runtime executable from source with the
build-exe-for-python-sdk script from the
official repository, then
point DSH_RUNTIME_BIN (or Config::dsh_bin) at the built executable.
Building from source is the only route that reproduces the upstream ref
the contract specs are frozen at (git checkout c389f96bf3 in the
official repository before building) — the specs' citation basis, distinct
from the runtime version under test, which CI owns via its own pin in
.github/workflows/ci.yml. Building from source is also the route for any
platform that has no published wheel.

How the SDK resolves the runtime

  1. Config::dsh_bin (non-empty);
  2. DSH_RUNTIME_BIN from the parent environment (non-empty);
  3. otherwise Error::RuntimeNotFound, whose message names the acquisition
    routes and cites the official repository.

An empty Config::dsh_bin and an empty DSH_RUNTIME_BIN both count as
absent, so resolution never produces an unlaunchable empty program.
DSH_RUNTIME_BIN is explicitly preserved after the runtime_bin →
dsh_bin rename: it is a supported product surface, not a compatibility
shim for a removed field.

The launch argv is exactly --profile <profile> (default "sdk") followed
by one --patch <absolute path> pair per configured Config::patches entry,
in caller order. Patch paths are resolved absolute before spawn. The crate
never passes application arguments and never emits the diagnostic
--dump-config / --dump-default-config subcommands. An empty profile
is rejected locally before spawn.

DSH_HOME resolution

The harness home resolves with the runtime's own precedence, highest
first:

  1. an explicit Config::dsh_home;
  2. a non-empty $DSH_HOME (first from Config::env, then inherited from
    the parent environment) — blank or whitespace-only counts as unset;
  3. ~/.dsh.

The resolved home is normalized to an absolute path with ~ expanded, is
created when absent so a fresh home boots, and is always injected into the
runtime child environment as DSH_HOME.

The selection is observable, never silent: call
Config::resolve_dsh_home(&parent_env) to read what a launch would pick, and
DeepSeekHarness::dsh_home() on a running instance to read the exact home it
was launched with.

Deliberate divergence from the Python SDK (documented; do not "fix"):
where Python raises ValueError rather than falling back, this crate
resolves ~/.dsh — ~/.dsh is the runtime's own documented default. The
crate follows the runtime's precedence (like the TypeScript SDK) instead of
Python's refusal. The real defect the Python refusal guards against is a
silent home selection; observability above addresses that instead.

API walkthrough

Layering

  • HarnessClient (low-level): spawns the runtime process, owns the stdio
    transport, speaks the JSON-RPC 2.0 wire protocol, and fans notifications
    out to subscriptions. Exposes LaunchSpec, ClientTimeouts, and
    NotificationSubscription.
  • DeepSeekHarness / Session (high-level): the Python-parity owned-run
    API on top of HarnessClient.
  • Input accepts either plain text (Input::Text) or raw content blocks
    (Input::Blocks), mirroring Python's normalize_input.

DeepSeekHarness::start

start is eager: it resolves the runtime, resolves and creates the
harness home (see DSH_HOME resolution), composes
the child environment, spawns the subprocess, and performs the initialize
handshake before returning. (This differs from the Python and TypeScript
SDKs, which start lazily on first use.) A failed handshake runs the close
ladder before the error propagates, so the spawned child is never leaked.

The initialize handshake is bounded by Config::initialize_timeout
(default 30 s, following Python; None means unbounded and is a
deliberate opt-out). The bound applies to the handshake only — never to the
activity interval or to session/prompt. On expiry start returns
Error::RequestTimeout { method: "initialize", .. }, whose message names
the selected profile (e.g. (selected dsh profile 'sdk')), and the child
is closed rather than left running.

Config::reasoning_effort (an Option<String>) is sent on initialize as
the wire key reasoningEffort only when set to a non-empty string: unset
or blank values are omitted entirely. The runtime rejects a non-string or
empty reasoningEffort, so blank is dropped rather than sent.

Config::cwd is resolved absolute and sent as initialize.cwd;
Config::runtime_cwd sets the subprocess cwd and defaults to cwd.
Config::request_timeout bounds every other request, including
session/prompt; None (the default) waits indefinitely.

Sessions created by start_session may run concurrently: the harness owns
the spawned child behind an async mutex, sessions interleave at the
session/prompt write, and each waits on its own subscription.

Session::run — one activity interval

run implements the Python Session.run algorithm:

  1. Subscribe to the session tree before writing the prompt, so no
    notification for this turn can be missed.
  2. Send session/prompt (bounded by Config::request_timeout).
  3. Wait for the durable agent/inbox/spliced receipt whose inserted[].id
    equals the returned message id (the field is id, not messageId);
    notifications before the receipt are dropped from both events and
    notifications.
  4. Collect — from the receipt inclusive — every tree notification until
    the root session reports session.status == "idle" (that idle
    notification is collected too; a non-root idle never terminates the run).

events holds root-session session.event payloads only; notifications
holds every tree notification (root + discovered descendants, incl.
session.status / subagent.started / subagent.finished) in transport
order.

run accepts an optional per-notification callback:

pub async fn run(
    &self,
    input: Input,
    on_notification: Option<&(dyn Fn(&Notification) + Send + Sync)>,
) -> Result<RunResult, Error>

The callback observes every notification delivered to the run's session-tree
subscription, in wire order
, and is invoked as each notification
arrives
, not deferred until the end. It is a pure observer: it cannot
change the returned RunResult (with and without a callback, events and
notifications hold the same sets in the same order), it is a layer over the
existing notification path rather than a second subscription, and it is
optional and additive (passing None behaves exactly like the plain run
path). A panic inside the callback is a caller bug and propagates; the crate
does not swallow it.

Both waits — the receipt wait and the idle wait — are unbounded (Python
parity); only the session/prompt request is bounded by
Config::request_timeout. Callers needing a bound wrap the call in
tokio::time::timeout — this bounds the local wait, not the runtime's turn.

Session format

The crate keeps session.event payloads untyped, so upstream
session-format advances (v2 … v4) pass through without touching the run
path. The v2 transition is kept below as a historical record of one such
advance:

  • assistant/message carries an embedded stream: AssistantStreamRecord[];
  • assistant/attempt was added;
  • assistant/chunk was removed — the crate does not claim assistant/chunk
    support and does not parse the embedded stream (non-goal).

Test-locking is scoped to exactly two v4 shapes: the flattened
tool/result event (a flat role: "tool" message) and the
developer/message passthrough — both locked as verbatim passthrough in
the crate's run-semantics suite.

Content block vocabulary

ContentBlock models the DSH ContentBlockMap with six typed variants
plus an Unknown fallback for unrecognized tags and malformed bodies:

Variant Shape
text {type:"text", text}
reasoning {type:"reasoning", text}
image {type:"image", attachment: ImageAttachmentRef}
file {type:"file", attachment: FileAttachmentRef}
tool-call {type:"tool-call", id, name, arguments} — arguments is a raw JSON string
tool-result {type:"tool-result", toolCallId, content: ContentBlock[], isError?} — content is recursive

FileAttachmentRef is {attachmentId, name, bytes}. ImageAttachmentRef is
{attachmentId, mediaType, bytes, width, height, name?, originalDimensions?};
originalDimensions is present only when normalization reduced the image, and
parses → serializes without loss. An unrecognized type (or a known tag with
a malformed body) falls through to ContentBlock::Unknown, preserving the raw
object verbatim.

RunResult

RunResult follows the Python SDK field set — exactly five fields, no
session_root (upstream removed it and asserts its absence). The TypeScript
SDK's RunResult lacks finish_reason; Rust follows Python:

Field Python TypeScript Rust (this crate)
session_id / sessionId yes yes session_id: String
final_response / finalResponse yes yes final_response: String
finish_reason yes no finish_reason: Option<String>
events yes (root session only) yes events: Vec<serde_json::Value>
notifications yes (root + descendants, transport order) yes notifications: Vec<Notification>
session_root no (removed) no does not exist

Both derived fields describe the owned activity interval rather than an
output causally assigned to the prompt: final_response is the last
committed root-session assistant text in the interval — steering, injected
context, and other queued work may contribute before idle — and
finish_reason is the kind of the last root-session turn/end in the
interval (such as completed, max-tokens, or error), None when no
turn ended. A turn/end without a string data.reason.kind violates the
runtime protocol and fails with Error::SdkProtocol.

Typed errors

All failure paths return Error variants instead of ad-hoc strings:

Variant Meaning
Error::RuntimeNotFound No runtime binary configured anywhere; message names the acquisition routes
Error::Config Invalid launch configuration (e.g. an empty profile), rejected locally before spawn
Error::TransportClosed Runtime process not running, or stdio closed unexpectedly; carries diagnostics (exit status and captured stderr tail)
Error::RequestTimeout A request got no response within the configured timeout; carries the method name and, for initialize, names the selected profile (e.g. (selected dsh profile 'sdk'))
Error::SdkProtocol A protocol-level violation (missing server identity, missing messageId, finish_reason extraction failure, malformed notifications, subscription lag); Error::is_protocol() detects it
Error::JsonRpc A JSON-RPC error response, preserving code (Option<i64>) and optional data
Error::Io / Error::Json I/O (spawn, stdio, transport) and JSON serialization/deserialization errors

Close ladder

DeepSeekHarness::close (and HarnessClient::close) runs the close ladder:
a cooperative shutdown request bounded by shutdown_timeout (default 1s,
diagnostic only on failure) → drop stdin (EOF) → wait eof_grace
(default 6s — the runtime gets time to flush durable state after stdin
closes) → SIGTERM → wait term_grace (default 3s) → SIGKILL → wait. The
ladder is idempotent, is unconditional teardown (failure at any tier still
reaps the child — the child is also killed on drop, so a ladder failure
cannot strand the process), and resolves all pending requests with
Error::TransportClosed.

Notifications

The wire has four server-to-client notifications: session.event,
session.status, subagent.started, and subagent.finished. Tree
notifications travel a broadcast channel capped at 4096 with drop-oldest
semantics. If a high-volume tree floods more notifications than fit between
the SDK's reads, the dropped set can include the inbox receipt or the
root-idle notification a run depends on — rather than hang forever or return
a silently truncated result, Session::run then fails fast with
Error::SdkProtocol. A caller expecting very large bursts can bypass the cap
only via the low-level HarnessClient::spawn_with_broadcast_capacity instead
of DeepSeekHarness::start. For arrival-order observation of every
notification, pass the per-notification callback to Session::run (above).

Environment variables

The parent environment is inherited wholesale; the SDK injects or overrides
only the keys below. Crate-injected values are applied first and the caller's
Config::env entries are applied after, so on collision the caller's value
wins (Python dict.update semantics) — except DSH_HOME, which is resolved,
never post-resolution overridden:

Variable Role Semantics
DSH_HOME Harness home (child env) Always written with the resolved absolute, ~-expanded home. The caller's value is an input to resolution (Config::dsh_home → non-empty DSH_HOME → ~/.dsh), not a post-resolution override; read the selection back via Config::resolve_dsh_home / DeepSeekHarness::dsh_home
DSH_RUNTIME_BIN Runtime binary resolution Consulted when Config::dsh_bin is not set; empty counts as absent. Explicitly preserved after the runtime_bin → dsh_bin rename (not a compatibility shim)
DEEPSEEK_BASE_URL / DEEPSEEK_API_KEY Model endpoint and credentials Inherited as-is; overridden when Config::base_url / Config::api_key is configured, and a caller-supplied Config::env entry for either key is applied after injection and wins on collision
DSH_CORDIS_CONFIG / DSH_SESSION_ROOT / DSH_CWD Removed — never written No reader upstream; see the removal table

Deliberate divergences

Each divergence below is deliberate and documented; a contributor must not
"fix" it back to reference behaviour without a superseding spec decision.

  1. DSH_HOME fallback — the crate resolves ~/.dsh where Python raises
    ValueError (DSH_HOME resolution).
  2. No client-directed request API — Python exposes next_request /
    respond / notify; the crate exposes none (the runtime emits no
    client-directed requests; they are auto-answered -32601). Non-goal, not
    a gap.
  3. Stricter malformed-notification policy — a session.event /
    session.status whose payload fails its shape check fails the run with
    Error::SdkProtocol. Python silently skips a malformed event/status and
    only raises on a malformed last turn/end; TypeScript raises for a
    malformed session.event but ignores a malformed session.status. This
    converts a silent hang into a typed failure and is not Python parity.
    Related local robustness choices: the embedded stderr tail is capped
    (8 KiB, newest lines first) and the broadcast buffer is bounded with a
    fail-fast on observed lag.
  4. Strict serverInfo.name equality — initialize requires the identity
    to be exactly deepseek-harness-sdk-runtime; Python treats the fields as
    optional and TypeScript checks presence only. An upstream rename fails
    loudly instead of being silently accepted.
  5. No run() convenience, no lazy start — the crate requires an explicit
    DeepSeekHarness::start; Python and TypeScript can start lazily on first
    use. Non-goal, not a gap.

Removed surface (v0.1 → current)

The following v0.1 identifiers are gone — no alias, no deprecated shim.
Each row names what it was and what replaces it:

Removed item What it was Replacement
Config::session_root claimed to control where sessions land Config::dsh_home — sessions live under $DSH_HOME/sessions
Config::cordis_config path to a cordis.yml config file the profile tree (Config::profile + Config::patches) — no config file is passed to the runtime
the DSH_CORDIS_CONFIG injection wrote DSH_CORDIS_CONFIG into the child env the profile tree — no reader upstream
the DSH_SESSION_ROOT injection wrote DSH_SESSION_ROOT into the child env Config::dsh_home — sessions live under $DSH_HOME/sessions
the DSH_CWD injection wrote DSH_CWD into the child env Config::cwd — sent as initialize.cwd
Config::launch_args_override replaced the whole argv with an opaque list Config::dsh_bin + Config::profile + Config::patches — the launch is composed from typed fields
Config::runtime_bin the runtime-path override field Config::dsh_bin (rename; DSH_RUNTIME_BIN env route preserved)
RunResult::session_root surfaced the session directory on every result dropped — no replacement (upstream removed it)
assets/cordis.yml bundled default config file the profile bundle — it mounted a package deleted upstream
DEFAULT_CORDIS_YML embedded the config file above the profile bundle
bundled_default_config_path the temp extraction path for the config the profile bundle — its whole purpose was the deleted injection channel
the assets/cordis.yml entry in Cargo.toml [package] include shipped the deleted file — (the file no longer exists)

Testing

  • cargo test — wire-protocol, lifecycle, and Session::run semantics
    suites against a scripted fake runtime (no real runtime needed).
  • tests/real_runtime.rs — a keyless handshake tier (start → initialize →
    close against a real dsh, no API key) that runs when a dsh binary is
    available, plus a live-turn tier gated on DEEPSEEK_API_KEY; otherwise it
    prints an explicit skip notice and passes, so cargo test is green with no
    runtime and no credentials present.

Platform support & MSRV

The SDK itself is pure Rust and platform-light; the consumed runtime decides
the platform matrix. Upstream publishes the runtime for 5 targets — the
list is in Runtime acquisition, Route B.

MSRV: current stable Rust (no minimum is pinned in Cargo.toml; the crate
tracks the stable toolchain).

Known limitations

  • Pre-release software — the crate ships pre-release versions while the
    runtime protocol settles; the API may change before 0.1.0. The
    real-runtime tests are environment-gated (see Testing); the
    fake-runtime suites carry protocol correctness.
  • No mid-turn cancel — there is no session-close / cancel RPC on the
    wire. Session::run waits until the root session reports idle; closing
    the harness mid-turn abandons the in-flight turn. A Config::request_timeout
    only abandons the local wait — the server-side work still runs until
    close.
  • No version negotiation — the runtime identifies as serverInfo 0.0.1
    pre-release, and initialize enforces a strict serverInfo.name check
    (deepseek-harness-sdk-runtime): the protocol declares the name
    wire-stable and has no negotiation, so an unexpected identity is a hard
    Error::SdkProtocol.
  • No runtime binary delivery / bundling / download — the runtime
    companion crate is a roadmap item, not part of this version. Acquire the
    runtime per Runtime acquisition.

License

Apache-2.0.

Install

🧩 Let your agent install it (recommended)

Install the catalog once, then DeepSeek Harness can find and install any plugin from this site automatically:

dsh plugin add dshbase-catalog

Then say "install dsh-rust-sdk for me" — your agent finds it in the directory and installs it. Docs: dshbase-catalog · verified packs.

This plugin is GitHub source (not published to npm) — install it straight from the repo:

Web profile:

dsh plugin --profile web add github:42ch-dev/dsh-rust-sdk

Headless (CLI) profile:

dsh plugin --profile headless add github:42ch-dev/dsh-rust-sdk

Test report

No short evidence report

There is no checkable per-level test record on this page. Stars, the catalog test field, a web-only flag, and whether the plugin needs an external account or service are not shown here as a pass or a failure. The catalog badge stays as published.

Security: not yet scanned — our daily static scan will cover it shortly.

Share this badge

More in Developer

Browse all 26834 plugins →