Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 19 additions & 4 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,13 @@ env:
CARGO_TERM_COLOR: always
RUST_BACKTRACE: 1

permissions:
contents: read

concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true

jobs:
fmt:
name: Format
Expand Down Expand Up @@ -39,10 +46,18 @@ jobs:
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
- uses: dtolnay/rust-toolchain@29eef336d9b2848a0b548edc03f92a220660cdb8 # stable
- uses: Swatinem/rust-cache@42dc69e1aa15d09112580998cf2ef0119e2e91ae # v2
- run: cargo test --lib --all-features
- run: cargo test --test '*_types'
- run: cargo test --test wire_format_peers
- run: cargo test --test compile_assertions
- run: cargo test --all-features

msrv:
name: MSRV
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
- uses: dtolnay/rust-toolchain@29eef336d9b2848a0b548edc03f92a220660cdb8 # stable
with:
toolchain: 1.88.0
- uses: Swatinem/rust-cache@42dc69e1aa15d09112580998cf2ef0119e2e91ae # v2
- run: cargo check --all-targets --all-features

doc:
name: Docs
Expand Down
2 changes: 1 addition & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
<!-- gitnexus:start -->
# GitNexus — Code Intelligence

This project is indexed by GitNexus as **honcho-rust-sdk** (2147 symbols, 4981 relationships, 186 execution flows). Use the GitNexus MCP tools to understand code, assess impact, and navigate safely.
This project is indexed by GitNexus as **honcho-rust-sdk** (2184 symbols, 5073 relationships, 190 execution flows). Use the GitNexus MCP tools to understand code, assess impact, and navigate safely.

> If any GitNexus tool warns the index is stale, run `npx gitnexus analyze` in terminal first.

Expand Down
26 changes: 26 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,32 @@ All notable changes to this project will be documented in this file.

The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).

## [0.1.2] - 2026-05-25

### Added

- `HonchoError::is_retryable()` to expose the SDK retry policy for callers.
- Client-side validation for dialectic queries, including the 10,000 character maximum.
- Client-side validation for pagination parameters (`page >= 1`, `size` between 1 and 100).
- Client-side validation for workspace IDs (`1..=512`, ASCII alphanumeric, `_`, or `-`).
- CI MSRV check for Rust 1.88.0.

### Changed

- Workspace metadata and configuration reads now use the OpenAPI `POST /v3/workspaces` get-or-create endpoint.
- `Honcho::base_url()` now reports the same normalized base URL used by the HTTP client.
- CI now runs the full all-features test suite and avoids duplicate doctest execution.
- Upload and peer-configuration docs now describe supported MIME types and peer existence requirements.

### Fixed

- Invalid base URLs such as `localhost:8000`, unsupported schemes, or URLs without hosts are rejected during client construction.
- Base URLs with non-root trailing slashes are normalized consistently between `Honcho` and `HttpClient`.
- `Honcho::schedule_dream` rejects an empty `observer` before making network requests.
- Pagination rejects invalid `page`/`size` values before making network requests.
- Route and docs tests cover new validation boundaries and workspace get-or-create behavior.
- README upload and peer-management examples corrected.

## [0.1.1] - 2025-05-13

### Breaking Changes
Expand Down
2 changes: 1 addition & 1 deletion CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
<!-- gitnexus:start -->
# GitNexus — Code Intelligence

This project is indexed by GitNexus as **honcho-rust-sdk** (2147 symbols, 4981 relationships, 186 execution flows). Use the GitNexus MCP tools to understand code, assess impact, and navigate safely.
This project is indexed by GitNexus as **honcho-rust-sdk** (2184 symbols, 5073 relationships, 190 execution flows). Use the GitNexus MCP tools to understand code, assess impact, and navigate safely.

> If any GitNexus tool warns the index is stale, run `npx gitnexus analyze` in terminal first.

Expand Down
2 changes: 1 addition & 1 deletion Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "honcho-ai"
version = "0.1.1"
version = "0.1.2"
edition = "2024"
rust-version = "1.88"
license = "MIT"
Expand Down
7 changes: 5 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -130,7 +130,7 @@ let msgs = session.upload_file(FileSource::bytes("doc.pdf", data, "application/p
.send().await?;

// Streaming upload
let msgs = session.upload_file_streamed("large.bin", reader, "application/octet-stream")
let msgs = session.upload_file_streamed("large.txt", reader, "text/plain")
.peer("alice")
.send().await?;

Expand All @@ -141,6 +141,7 @@ session.set_peers(["alice", "bob"]).await?;
session.remove_peers(["bob"]).await?;
let peers = session.peers().await?;
let cfg = session.get_peer_configuration("alice").await?;
// alice must already be present; add_peer/add_peers/set_peers above satisfy this.
session.set_peer_configuration("alice", &cfg).await?;

// Context & Search
Expand All @@ -165,6 +166,8 @@ session.set_configuration(config).await?;

### Pagination

Page numbers are 1-based. Page size must be between 1 and 100 inclusive.

```rust
let page = client.peers().await?;
for peer in page.items() {
Expand Down Expand Up @@ -239,7 +242,7 @@ These APIs have no equivalent in the Python/TypeScript SDKs:
| Context (OpenAI/Anthropic) | ✓ | ✓ |
| Blocking API | ✗ | ✓ |
| Webhooks | ✓ | ✗ |
| API keys | ✓ | |
| API keys | ✓ | |


## Links
Expand Down
4 changes: 4 additions & 0 deletions src/blocking/client.rs
Original file line number Diff line number Diff line change
Expand Up @@ -169,6 +169,8 @@ impl Honcho {
}

/// List peers with filters, collecting across pages.
///
/// `page` is 1-based. `size` must be in `1..=100`.
pub fn peers_with_filters(
&self,
filters: HashMap<String, Value>,
Expand All @@ -194,6 +196,8 @@ impl Honcho {
}

/// List sessions with filters, collecting across pages.
///
/// `page` is 1-based. `size` must be in `1..=100`.
pub fn sessions_with_filters(
&self,
filters: HashMap<String, Value>,
Expand Down
2 changes: 1 addition & 1 deletion src/blocking/conclusion.rs
Original file line number Diff line number Diff line change
Expand Up @@ -214,7 +214,7 @@ impl BlockingListConclusionsBuilder {
}
}

/// Page size.
/// Page size. Must be in `1..=100`.
#[must_use]
pub fn size(self, size: u32) -> Self {
Self {
Expand Down
10 changes: 10 additions & 0 deletions src/blocking/session.rs
Original file line number Diff line number Diff line change
Expand Up @@ -152,6 +152,10 @@ impl Session {
}

/// Set per-peer configuration.
///
/// The peer must already be present in the session. This method does not
/// create or add peers; use [`Session::add_peer`] or [`Session::add_peers`]
/// first. If the peer is absent, the server may return 404/`NotFound`.
pub fn set_peer_configuration(&self, peer_id: &str, config: &SessionPeerConfig) -> Result<()> {
block_on(self.inner.set_peer_configuration(peer_id, config))
}
Expand Down Expand Up @@ -248,6 +252,9 @@ impl Session {

/// Begin a file upload to this session.
///
/// The API currently accepts `text/plain`, `application/pdf`, and
/// `application/json`; other MIME types may be rejected by the server.
///
/// Returns a [`BlockingUploadFileBuilder`]. You **must** call `.peer(id)`
/// and then `.send()` to complete the upload.
///
Expand All @@ -272,6 +279,9 @@ impl Session {

/// Begin a file upload from a synchronous reader.
///
/// The API currently accepts `text/plain`, `application/pdf`, and
/// `application/json`; other MIME types may be rejected by the server.
///
/// The reader is consumed in a background thread and piped to the async
/// multipart stream **without buffering the entire file in memory**.
///
Expand Down
85 changes: 63 additions & 22 deletions src/client.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ use tokio::sync::OnceCell;
use url::Url;

use crate::error::{HonchoError, Result};
use crate::http::client::HttpClient;
use crate::http::client::{HttpClient, normalize_base_url};
use crate::http::routes;
use crate::peer::Peer;
use crate::session::{PeerSpec, Session};
Expand Down Expand Up @@ -92,15 +92,12 @@ impl Honcho {
/// # Ok::<(), honcho_ai::error::HonchoError>(())
/// ```
pub fn new(base_url: &str, workspace_id: &str) -> Result<Self> {
if workspace_id.is_empty() {
return Err(HonchoError::Configuration(
"workspace_id must not be empty".into(),
));
}
let http =
HttpClient::from_params(HttpClient::builder().base_url(base_url.to_string()).build())?;
let url = Url::parse(base_url)
.map_err(|e| HonchoError::Configuration(format!("invalid base_url: {e}")))?;
validate_workspace_id(workspace_id)?;
let url = normalize_base_url(base_url)?;
Comment thread
sourcery-ai[bot] marked this conversation as resolved.
let http = HttpClient::from_params_with_base_url(
HttpClient::builder().base_url(base_url.to_owned()).build(),
url.clone(),
)?;
Ok(Self {
inner: Arc::new(Inner {
http,
Expand Down Expand Up @@ -154,16 +151,10 @@ impl Honcho {
.or_else(|| std::env::var("HONCHO_WORKSPACE_ID").ok())
.unwrap_or_else(|| "default".to_owned());

if resolved_workspace_id.is_empty() {
return Err(HonchoError::Configuration(
"workspace_id must not be empty".into(),
));
}

let base_url = Url::parse(&resolved_base_url)
.map_err(|e| HonchoError::Configuration(format!("invalid base_url: {e}")))?;
validate_workspace_id(&resolved_workspace_id)?;
let base_url = normalize_base_url(&resolved_base_url)?;

let http = HttpClient::from_params(
let http = HttpClient::from_params_with_base_url(
HttpClient::builder()
.base_url(resolved_base_url)
.maybe_api_key(resolved_api_key)
Expand All @@ -173,6 +164,7 @@ impl Honcho {
.default_headers(params.default_headers.unwrap_or_default())
.default_query(params.default_query.unwrap_or_default())
.build(),
base_url.clone(),
)?;

Ok(Self {
Expand Down Expand Up @@ -252,10 +244,15 @@ impl Honcho {

/// Fetch workspace metadata from the server.
pub async fn get_metadata(&self) -> Result<HashMap<String, Value>> {
let body = crate::types::workspace::WorkspaceCreate {
id: self.inner.workspace_id.clone(),
metadata: None,
configuration: None,
};
let ws: Workspace = self
.inner
.http
.get(&routes::workspace(self.workspace_id())?, &[])
.post(&routes::workspaces(), Some(&body), &[])
.await?;
Comment on lines 246 to 256
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

The change from GET to POST for fetching workspace metadata (and configuration in subsequent methods) introduces a potential regression. While ensure_workspace (line 194) explicitly handles 409 Conflict as a success case, these methods do not. If the workspace already exists and the server returns 409 on a POST request, these methods will fail with a Conflict error instead of returning the data.

If the server is guaranteed to return 200 OK with the workspace object on POST even if it exists, then the 409 handling in ensure_workspace is redundant. Otherwise, these methods should handle the 409 case (e.g., by falling back to a GET request) to ensure they work correctly for existing workspaces.

Ok(ws.metadata)
}
Expand All @@ -282,10 +279,15 @@ impl Honcho {
/// }
/// ```
pub async fn get_configuration(&self) -> Result<WorkspaceConfiguration> {
let body = crate::types::workspace::WorkspaceCreate {
id: self.inner.workspace_id.clone(),
metadata: None,
configuration: None,
};
let ws: Workspace = self
.inner
.http
.get(&routes::workspace(self.workspace_id())?, &[])
.post(&routes::workspaces(), Some(&body), &[])
.await?;
Ok(ws.configuration)
}
Expand Down Expand Up @@ -320,10 +322,15 @@ impl Honcho {
/// Use this when the server returns fields not yet represented in
/// [`WorkspaceConfiguration`].
pub async fn get_configuration_raw(&self) -> Result<HashMap<String, Value>> {
let body = crate::types::workspace::WorkspaceCreate {
id: self.inner.workspace_id.clone(),
metadata: None,
configuration: None,
};
let raw: serde_json::Value = self
.inner
.http
.get(&routes::workspace(self.workspace_id())?, &[])
.post(&routes::workspaces(), Some(&body), &[])
.await?;
match raw.get("configuration") {
Some(serde_json::Value::Object(map)) => {
Expand Down Expand Up @@ -557,6 +564,11 @@ impl Honcho {
session_id: Option<&str>,
observed_peer: Option<&str>,
) -> Result<()> {
if observer.is_empty() {
return Err(HonchoError::Validation(
"observer must not be empty".to_string(),
));
}
self.ensure_workspace().await?;
let observed_peer = observed_peer.unwrap_or(observer);
let body = crate::types::dream::ScheduleDreamRequest {
Expand Down Expand Up @@ -623,6 +635,8 @@ impl Honcho {

/// List peers with filters.
///
/// `page` is 1-based. `size` must be in `1..=100`.
///
/// # Examples
///
/// ```no_run
Expand Down Expand Up @@ -691,6 +705,8 @@ impl Honcho {

/// List sessions with filters.
///
/// `page` is 1-based. `size` must be in `1..=100`.
///
/// # Examples
///
/// ```no_run
Expand Down Expand Up @@ -756,3 +772,28 @@ impl Honcho {
Ok(page.map(|ws| ws.id))
}
}

fn validate_workspace_id(workspace_id: &str) -> Result<()> {
if workspace_id.is_empty() {
return Err(HonchoError::Configuration(
"workspace_id must not be empty".into(),
));
}

if workspace_id.len() > 512 {
return Err(HonchoError::Configuration(
"workspace_id must be at most 512 characters".into(),
));
}

if !workspace_id
.bytes()
.all(|b| b.is_ascii_alphanumeric() || matches!(b, b'_' | b'-'))
{
return Err(HonchoError::Configuration(
"workspace_id must match [a-zA-Z0-9_-]+".into(),
));
}

Ok(())
}
7 changes: 1 addition & 6 deletions src/conclusion.rs
Original file line number Diff line number Diff line change
Expand Up @@ -636,7 +636,7 @@ impl ListConclusionsBuilder {
self
}

/// Set the page size (default 50).
/// Set the page size (default 50, must be in `1..=100`).
///
/// # Examples
///
Expand Down Expand Up @@ -690,11 +690,6 @@ impl ListConclusionsBuilder {
/// # }
/// ```
pub async fn send(self) -> Result<ConclusionPage> {
if self.size == 0 {
return Err(HonchoError::Validation(
"page size must be greater than 0".to_string(),
));
}
let mut filters = serde_json::json!({
"observer_id": self.scope.inner.observer,
"observed_id": self.scope.inner.observed,
Expand Down
Loading
Loading