# Ursly Rust SDK

Public documentation: [developer.ursly.io](https://developer.ursly.io/).

A typed, synchronous v1 intent client for Rust 1.85+ (edition 2024). The default `http` feature adds a blocking Rustls HTTP adapter built on reqwest; with `default-features = false` the crate depends only on serde, serde_json and url, and you inject a transport. Install the versioned SDK archive from the website. The archive is the `.crate` file produced by `cargo package`; crates.io publication is separate and the source repository is private.

```sh
# Download ursly-sdk-<version>.crate from the website and compare its SHA-256 with the
# value listed next to the download. Delete the file and stop if they differ.
sha256sum ursly-sdk-<version>.crate
mkdir -p vendor
tar -xzf ursly-sdk-<version>.crate -C vendor  # creates vendor/ursly-sdk-<version>
```

```toml
[dependencies]
ursly-sdk = { path = "vendor/ursly-sdk-<version>" }
```

`<version>` is `release.version` in `/downloads/manifest.json`. The public API host is `https://ursly.io`; pass it explicitly to `Client::new` or `Client::builder`, because the client has no default destination.

```rust
use ursly::{Action, ActionRule, ActionType, Client, ResolveRequest, SenseInput, Value};
use std::time::Duration;

let client = Client::builder("https://YOUR-API-HOST")
    .timeout(Duration::from_secs(5))
    .build()?;
client.register_handler(ActionType::NavigationOpen, |action, _intent| {
    // Your router decides what navigation means. No built-in navigation occurs.
    if let Action::NavigationOpen { target } = action {
        println!("Requested route: {target}");
    }
    Ok(Value::Null)
});
let request = ResolveRequest::new(
    SenseInput::voice("open home"),
    vec![ActionRule::new("home", "open home", Action::NavigationOpen { target: "/".into() })],
);
let resolution = client.resolve(&request)?; // Returns intents and performs no action.
let executed = client.resolve_and_dispatch(&request)?; // Explicit application handlers.
```

The package is `ursly-sdk`; the library is imported as `ursly`. Methods are `capabilities()`, `actions()`, `templates()`, `validate(&request)` and `resolve(&request)`. `templates()` supplies editable requests. `register_handler(type, fn)` returns a `HandlerId`; `unregister_handler(id)` removes only that registration, so a stale id never removes a replacement. `dispatch(&resolution)` is explicit. Handlers are synchronous closures receiving the action and intent and returning `Result<Value, Box<dyn Error + Send + Sync>>`. Missing handlers are checked before any dispatch; handler failure stops later handlers without retry or rollback of completed effects. A panicking handler unwinds through `dispatch`; return `Err` for expected failures.

## Transport, timeouts and cancellation

The base URL is required; an HTTP(S) proxy prefix is supported. Embedded credentials, queries and fragments are rejected. Default timeout is 10 seconds; it must be non-zero and at most 2^31 - 1 ms. Each request carries a total deadline from connecting until the response body is read. Request bodies over 32768 bytes are rejected before anything is sent (`payload_too_large`), as are non-finite confidence values (`invalid_request`), which JSON cannot represent. Responses over 1 MiB are rejected. No automatic retry or redirect is performed; a redirect is reported as an API error carrying its 3xx status. Requests identify the SDK as `ursly-sdk-rust/<version>`.

`Client::with_cancel(&AtomicBool)` returns a view with the same methods that checks the flag before sending, after the transport returns and before each handler. A blocking HTTP request already in flight cannot be unsent; its outcome is discarded and no handler executes from it.

A transport can be injected with `ClientBuilder::transport`: implement `Transport`, or pass a closure `Fn(&TransportRequest) -> Result<TransportResponse, Error>`. The request carries the method, the full `/api/v1/` URL, the encoded JSON body and the timeout. Injected transports must bound their own I/O and must not retry or follow redirects.

Typed `Error` variants distinguish API rejection (`status`, server `code`, `http_error` fallback), configuration, timeout, cancellation, transport/invalid response, invalid or oversized request, missing handler and handler failure; `Error::code()` matches the other Ursly SDKs. The client does not log request bodies, secrets or credential-bearing URLs, and handler error contents are never included in SDK errors.

## Async applications

The client is blocking. Like every `reqwest::blocking` client, it must not be built or called directly on an async runtime thread. Share it with `Arc` and move calls onto a blocking thread:

```rust
use std::sync::Arc;

let client = Arc::new(
    tokio::task::spawn_blocking(|| Client::new("https://YOUR-API-HOST")).await??,
);
let worker = Arc::clone(&client);
let resolution = tokio::task::spawn_blocking(move || worker.resolve(&request)).await??;
```

Dropping the future returned by `spawn_blocking` does not stop the request; use `with_cancel` to prevent sending and dispatch.

## Response validation

Catalog entries and template requests are validated before being returned. Invalid action payloads are rejected before registered handlers execute, including external navigation (a path whose `//`, `.` or `..` segments, `%2e` included, could resolve to another host), invalid identifiers, unknown action fields and oversized UTF-8 payloads. `dispatch` validates resolutions you construct yourself in the same way, and a matched resolution must contain intents. Optional `confidence` and `minConfidence` accept both omission (`None`) and JSON `null` (`Some(None)`), matching the generated API and templates; `enabled` and workflow `parameters` may be omitted but not null. Applications still own authorization and the safety of handler side effects.

## Actions and product boundaries

Supported types are `navigation.open`, `text.insert`, `media.play`, `media.pause`, `scene.focus` and `workflow.trigger`. Workflow parameters are bounded string maps. Configure optional `enabled`/`minConfidence` on rules with `ActionRule::with_enabled`/`with_min_confidence`, and adapter-supplied confidence on inputs with `SenseInput::with_confidence`. Use `SenseInput::motion("swipe_left")` for normalized motion rules. No microphone/camera acquisition, recognition, shell execution or server HTTP relay is built in.

Voice production and motion in development describe founder-reported product stages, not certified hardware or recognition performance. Remaining senses explicitly return HTTP 422 `sense_unavailable`. Tests of this client do not validate a physical sensor or commercial integration.

## Changes since 0.1.0

The Rust SDK is new since 0.1.0, the last published developer release, so there is no earlier Rust release to upgrade from. Before that first release, its source on `main` accepted `navigation.open` targets with a `.` or `..` segment (also spelled `%2e`) or `//` inside the path; the released SDK refuses them as `Error::InvalidResponse` (code `invalid_response`), like the other clients.

## Development

```sh
cargo fmt --manifest-path sdk/rust/Cargo.toml --all --check
cargo clippy --manifest-path sdk/rust/Cargo.toml --locked --all-targets --all-features -- -D warnings
cargo clippy --manifest-path sdk/rust/Cargo.toml --locked --no-default-features --lib -- -D warnings
cargo test --manifest-path sdk/rust/Cargo.toml --locked
URSLY_TEST_URL=http://127.0.0.1:4198 cargo test --manifest-path sdk/rust/Cargo.toml --locked --test real_api
cargo package --manifest-path sdk/rust/Cargo.toml --locked
```

`cargo package` writes `sdk/rust/target/package/ursly-sdk-<version>.crate`. This is an independent Cargo workspace with its own lockfile; it shares no code with the Ursly CLI or server crates. Typed contracts, response validation, explicit handler dispatch and the `Transport` port are framework-independent; reqwest is only the default adapter. The shared Rust API generates authoritative OpenAPI and catalogs. Offline injected-transport tests, local HTTP fixtures and optional real-API tests establish different evidence. [SOURCES.md](SOURCES.md) records versions, official references and evidence boundaries.

## License

License terms have not yet been published. Public downloads do not grant an open-source license; the source repository remains private.
