clawdforge/clients/rust
Kayos 062d405a9e clients/rust: initial Rust SDK for clawdforge
Async client over reqwest+tokio with builder-pattern Client, serde
RunRequest/RunResult/FileToken/AppToken types, thiserror Error enum,
streaming multipart upload via tokio::fs::File, and 14 wiremock-backed
integration tests covering healthz, run-success-json, run-success-text,
run-502, run-with-files, file-upload, token mint/list/revoke, auth
failure, missing-token short-circuit, transport timeout, and builder
validation. Doc-tested. cargo test, cargo clippy --all-targets -D
warnings, and cargo build --examples all clean.
2026-04-28 22:35:16 -07:00
..
examples clients/rust: initial Rust SDK for clawdforge 2026-04-28 22:35:16 -07:00
src clients/rust: initial Rust SDK for clawdforge 2026-04-28 22:35:16 -07:00
tests clients/rust: initial Rust SDK for clawdforge 2026-04-28 22:35:16 -07:00
Cargo.toml clients/rust: initial Rust SDK for clawdforge 2026-04-28 22:35:16 -07:00
README.md clients/rust: initial Rust SDK for clawdforge 2026-04-28 22:35:16 -07:00

clawdforge — Rust client

Async Rust SDK for clawdforge, a small LAN-only HTTP service that wraps claude -p subprocess calls behind a bearer-token-gated REST API.

  • Tokio + reqwest under the hood
  • serde + serde_json types
  • Streaming multipart upload (tokio::fs::File, no full-file buffer)
  • Builder pattern for configuration
  • Typed RunResult::as_json::<T>() and as_text() helpers over a serde_json::Value payload

Install

This crate is not on crates.io. Pull it directly from the upstream git host:

cargo add clawdforge --git https://gitea.sulkta.com/Sulkta-Coop/clawdforge --rev <pin>

Or pin manually in Cargo.toml:

[dependencies]
clawdforge = { git = "https://gitea.sulkta.com/Sulkta-Coop/clawdforge", rev = "<pin>" }
tokio = { version = "1", features = ["macros", "rt-multi-thread"] }

For an in-repo workspace consumer, point at the clients/rust/ path:

clawdforge = { path = "../clawdforge/clients/rust" }

Quickstart

use clawdforge::{Client, RunRequest};

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    let client = Client::builder()
        .base_url("http://localhost:8800")
        .token("cf_xxxxxxxxxxxxxxxx")
        .build()?;

    // Liveness — does not require a token, but sends one if configured.
    let h = client.healthz().await?;
    println!("claude present: {} version: {:?}", h.claude_present, h.claude_version);

    // Run a prompt. `result` is a serde_json::Value — narrow via .as_json::<T>().
    let r = client.run(RunRequest {
        prompt: "Reply with JSON: {\"hello\":\"world\"}".into(),
        model: Some("sonnet".into()),
        timeout_secs: Some(30),
        ..Default::default()
    }).await?;

    #[derive(serde::Deserialize)]
    struct Hello { hello: String }
    let typed: Hello = r.as_json()?;
    println!("{}", typed.hello);

    // Upload a file, then attach it to a follow-up run.
    let ft = client.upload_file("./recipe.png", Some(3600)).await?;
    let r2 = client.run(RunRequest {
        prompt: "extract recipe data".into(),
        files: Some(vec![ft.file_token]),
        ..Default::default()
    }).await?;
    println!("{:?}", r2.result);

    Ok(())
}

Public API

Client::builder()

Builder for the HTTP client.

Method Purpose
.base_url(url) Required. e.g. "http://localhost:8800".
.token(t) App bearer for /run, /files.
.admin_token(t) Admin bearer for /admin/*.
.timeout(Duration) Per-request timeout (default 120 s).
.user_agent(s) Override User-Agent header.
.danger_accept_invalid_certs(bool) Skip TLS verify (off by default).
.build() Returns Result<Client, Error>.

Client async methods

Method Endpoint Notes
healthz() GET /healthz Returns Healthz.
run(RunRequest) POST /run Returns RunResult. 502 surfaces as Error::Api.
upload_file(path, ttl_secs) POST /files Streams from disk; returns FileToken.
create_token(TokenCreateRequest) POST /admin/tokens Admin only. Returns AppToken.
list_tokens() GET /admin/tokens Admin only. Returns TokenList.
revoke_token(name) DELETE /admin/tokens/{name} Admin only.

RunResult helpers

let r = client.run(req).await?;

// Try a typed shape.
#[derive(serde::Deserialize)]
struct Recipe { name: String, qty: u32 }
let recipe: Recipe = r.as_json()?;

// Or fall back to a string when the model declined to emit JSON.
if let Some(text) = r.as_text() {
    println!("{text}");
}

r.result itself is serde_json::Value if you need to branch on shape.

Error model

pub enum Error {
    Auth(String),                // missing/invalid bearer, 401, 403
    Api { status: u16, body: String },  // any other non-2xx
    Transport(reqwest::Error),   // connect, TLS, read, request timeout
    Json(serde_json::Error),     // decode failure on a 2xx body
    Io(std::io::Error),          // local file open in upload_file
    Timeout(String),             // explicit deadline (reserved)
    Config(String),              // builder misconfiguration
}

A 502 from /run lands in Error::Api { status: 502, body } — the body is the JSON failure envelope. Recover the structured form with:

let parsed: clawdforge::RunFailure = serde_json::from_str(&body)?;

Wire format

clawdforge speaks snake_case JSON end-to-end. The structs in this crate match that without #[serde(rename_all = "camelCase")]. If a future endpoint exposes camelCase, prefer per-field #[serde(rename = "...")] over a blanket container attribute so both styles can coexist.

Examples

CLAWDFORGE_URL=http://localhost:8800 \
CLAWDFORGE_TOKEN=cf_xxxx \
cargo run --example basic

Optional file demo:

CLAWDFORGE_DEMO_FILE=./some.png cargo run --example basic

Development

cargo build --release
cargo test --all
cargo clippy --all-targets -- -D warnings
cargo build --examples

Tests use wiremock — no live clawdforge needed.

License

MIT.