Skip to main content

deepseek_harness_sdk_rs/
lib.rs

1//! # deepseek-harness-sdk-rs
2//!
3//! Drive a DeepSeek Harness runtime from Rust. The SDK spawns the packaged
4//! `dsh` CLI (`--profile sdk`) as a subprocess and speaks the line-delimited
5//! JSON-RPC 2.0 protocol over its stdio
6//! ([protocol reference](https://github.com/deepseek-ai/deepseek-harness/blob/master/packages/sdk/protocol/README.md));
7//! the CLI is distributed in the `deepseek-harness-runtime-bin` wheel also
8//! used by the [Python SDK](../../../python/README.md).
9//!
10//! ```no_run
11//! use std::path::PathBuf;
12//! use deepseek_harness_sdk_rs::{DeepSeekHarness, DeepSeekHarnessConfig, RunOptions};
13//!
14//! # async fn example() -> Result<(), deepseek_harness_sdk_rs::SdkError> {
15//! let harness = DeepSeekHarness::new(DeepSeekHarnessConfig {
16//!     dsh_home: Some(PathBuf::from("/absolute/path/to/isolated-dsh-home")),
17//!     ..Default::default()
18//! });
19//! let result = harness.run("say hi", &RunOptions::default()).await?;
20//! println!("{}", result.final_response);
21//! harness.close().await;
22//! # Ok(())
23//! # }
24//! ```
25//!
26//! The runtime launches as the packaged `dsh` CLI with `--profile sdk`.
27//! Managed launches require an explicit Harness home (`dsh_home` or a
28//! non-empty `DSH_HOME`); the SDK never discovers `~/.dsh`. Without any launch configuration the
29//! `runtime-download` feature (on by default) fetches the platform runtime
30//! wheel from PyPI, verifies its SHA-256 digest, and caches the executable
31//! and its sidecars. Callers who spawn their own runtime pass `runtime_bin`
32//! or `launch_args_override` and can disable the feature. A blocking facade
33//! ([`DeepSeekHarnessSync`]) covers synchronous callers.
34
35pub mod api;
36pub mod client;
37pub mod error;
38pub mod events;
39#[cfg(feature = "runtime-download")]
40pub mod runtime;
41mod transport;
42
43use std::path::{Component, Path, PathBuf};
44
45/// Resolve a path the way Python's `Path.resolve(strict=False)` does: make it
46/// absolute, collapse `.` and `..` lexically, resolve symlinks along the
47/// existing prefix, and keep any not-yet-existing tail verbatim. The SDK
48/// applies this to `cwd`/`runtime_cwd`, managed `dsh_home` and patch paths,
49/// explicit runtime binaries, and the wire handshake.
50pub(crate) fn resolve_path(path: &Path) -> PathBuf {
51    if path.as_os_str().is_empty() {
52        return std::env::current_dir().unwrap_or_else(|_| PathBuf::from("."));
53    }
54    let absolute = std::path::absolute(path).unwrap_or_else(|_| path.to_path_buf());
55    let normalized = collapse_dot_components(absolute);
56    if normalized.exists() {
57        return normalized.canonicalize().unwrap_or(normalized);
58    }
59    let mut missing = Vec::new();
60    let mut prefix = normalized.as_path();
61    while !prefix.exists() {
62        let Some(parent) = prefix.parent() else {
63            break;
64        };
65        let Some(name) = prefix.file_name() else {
66            break;
67        };
68        missing.push(name.to_os_string());
69        prefix = parent;
70    }
71    let mut resolved = prefix
72        .canonicalize()
73        .unwrap_or_else(|_| prefix.to_path_buf());
74    for name in missing.iter().rev() {
75        resolved.push(name);
76    }
77    resolved
78}
79
80/// Lexically collapse `.` and `..` components, clamping `..` at the root the
81/// way `Path.resolve()` does. No filesystem access.
82fn collapse_dot_components(path: PathBuf) -> PathBuf {
83    let mut collapsed = PathBuf::new();
84    for component in path.components() {
85        match component {
86            Component::CurDir => {}
87            Component::ParentDir => {
88                if matches!(
89                    collapsed.components().next_back(),
90                    Some(Component::Normal(_))
91                ) {
92                    collapsed.pop();
93                }
94                // At the root, a parent component stays at the root.
95            }
96            other => collapsed.push(other.as_os_str()),
97        }
98    }
99    collapsed
100}
101
102pub use api::{
103    DeepSeekHarness, DeepSeekHarnessConfig, DeepSeekHarnessSync, Input, RunOptions, RunResult,
104    Session,
105};
106pub use client::{
107    ApprovalOutcome, ApprovalRequestParams, ApprovalRequestResult, HarnessClient,
108    HarnessClientOptions, IncomingRequest, InitializeParams, InitializeResult, Notification,
109    ServerInfo, UserQuestionAnswer, UserQuestionAnswerItem, UserQuestionCancellation,
110    UserQuestionHandler, UserQuestionRequest,
111};
112pub use error::SdkError;
113pub use transport::{NotificationFilter, NotificationSubscription};
114
115#[cfg(test)]
116mod tests {
117    use super::*;
118
119    #[test]
120    fn resolve_path_collapses_parent_components_like_python() {
121        let temp = tempfile::TempDir::new().expect("tempdir");
122        let expected = temp
123            .path()
124            .canonicalize()
125            .expect("canonical tempdir")
126            .join("bar");
127        assert_eq!(
128            resolve_path(&temp.path().join("missing").join("..").join("bar")),
129            expected
130        );
131    }
132
133    #[cfg(unix)]
134    #[test]
135    fn resolve_path_clamps_parent_components_at_the_root() {
136        assert_eq!(resolve_path(Path::new("/")), PathBuf::from("/"));
137        assert_eq!(
138            resolve_path(&Path::new("/..").join("..").join("etc")),
139            Path::new("/etc").canonicalize().expect("canonical /etc")
140        );
141    }
142}