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}