# Ursly Go SDK

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

A typed, context-first v1 intent client for Go 1.23+ that uses only the standard library. Install the versioned SDK archive from the website. Module proxy or registry publication is separate, and the source repository is private, so the module path is not fetched over the network.

```sh
# Download ursly-sdk-go-<version>.zip from the website's downloads, then
# compare this digest with the SHA-256 published for that release.
sha256sum ursly-sdk-go-<version>.zip
mkdir -p third_party/ursly-go
unzip ursly-sdk-go-<version>.zip -d third_party/ursly-go  # go.mod at third_party/ursly-go/go.mod
go mod edit -require=ursly.io/sdk/go@v<version> \
  -replace=ursly.io/sdk/go=./third_party/ursly-go
```

`<version>` is `release.version` in `/downloads/manifest.json`; the extracted `version.go` holds it as `ursly.Version`. A directory replacement needs no `go.sum` entry and no module proxy; builds work with `GOPROXY=off`. Run `go mod tidy` only after your code imports the package: before that, tidy drops the requirement and later restores it as a `v0.0.0-…` pseudo-version. The public API host is `https://ursly.io`; pass it explicitly to `ursly.New`, because the client has no default destination.

```go
import ursly "ursly.io/sdk/go"

client, err := ursly.New("https://YOUR-API-HOST", ursly.WithTimeout(5*time.Second))
if err != nil {
	return err
}
client.RegisterHandler(ursly.ActionNavigationOpen,
	func(ctx context.Context, action ursly.Action, intent ursly.Intent) (any, error) {
		// Your router decides what navigation means. No built-in navigation occurs.
		return nil, router.Open(ctx, action.Target)
	})
req := ursly.ResolveRequest{
	Input: ursly.Input{Sense: ursly.SenseVoice, Transcript: "open home"},
	Rules: []ursly.Rule{{ID: "home", Match: "open home",
		Action: ursly.Action{Type: ursly.ActionNavigationOpen, Target: "/"}}},
}
resolution, err := client.Resolve(ctx, req)         // Returns intents and performs no action.
executed, err := client.ResolveAndDispatch(ctx, req) // Explicit application handlers.
```

Methods are `Capabilities(ctx)`, `Actions(ctx)`, `Templates(ctx)`, `Validate(ctx, req)` and `Resolve(ctx, req)`. `Templates` supplies editable requests. `RegisterHandler(type, fn)` configures application-owned action behavior and returns an unregister function that removes only that registration, never a later replacement. `Dispatch(ctx, resolution)` is explicit. Handlers receive the context, the action and its intent. Missing handlers are checked before any handler runs; a handler error stops later handlers without retry or rollback of completed effects. The client and its handler registry are safe for concurrent use. A runnable example is in [example_test.go](example_test.go).

## Transport, cancellation and timeouts

The base URL is required; an HTTP(S) proxy path prefix is supported and trailing slashes are trimmed. Embedded credentials, queries and fragments are rejected with `*ConfigurationError`. Default timeout is 10 seconds and covers reading the response body. Requests send `User-Agent: ursly-sdk-go/<version>`, where `<version>` is `ursly.Version`. `WithTransport` accepts any `http.RoundTripper`, for example to inject responses in tests; the client always wraps it in an `http.Client` that does not follow redirects.

Every method takes a `context.Context`. The client checks it before sending, while waiting and before each handler. Caller cancellation, including the caller's own deadline, returns `*CancelledError` (code `cancelled`); the client timeout returns `*TimeoutError` (code `timeout`). The deadline holds even when a transport or response body ignores the context: client waiting stops, and the abandoned exchange may finish in a background goroutine. A request that already reached the server cannot be unsent, and no handler executes from an abandoned response. No automatic retry is performed, and a redirect response is returned as `*APIError` with its 3xx status.

Request bodies are encoded without HTML escaping and limited to 32768 bytes; larger requests fail with code `payload_too_large` before sending, and values JSON cannot encode fail with `invalid_request`. At most 1 MiB of a response body is read; larger bodies are `*InvalidResponseError`.

Typed errors distinguish API rejection (`*APIError` with `Status` and the server `Code`, or `http_error`), timeout, cancellation, `*TransportError`, `*InvalidResponseError`, `*MissingHandlerError` and `*HandlerError`. All embed `SDKError` with a stable `Code`; use `errors.As`, or `ursly.ErrorCode(err)`. `*HandlerError` never includes your handler's message but unwraps to it. The client does not log request bodies, secrets or URLs, and `*TransportError` hides the underlying network error.

## 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` applies the same checks to resolutions you construct and passes handlers a validated copy. Optional `Confidence` and `MinConfidence` are `*float64`: JSON omission and `null` both decode to `nil`, and `nil` is encoded by omission, which the API treats the same way. `Enabled` is `*bool`; `nil` means the API default, enabled. Applications still own authorization and the safety of handler side effects.

## Declarative actions

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 and adapter-supplied `Confidence` on inputs; `ursly.Float64` and `ursly.Bool` build the pointers. Replace voice input with `ursly.Input{Sense: ursly.SenseMotion, Gesture: "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 Go SDK is new since 0.1.0, the last published developer release, so there is no earlier Go 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 `invalid_response`, like the other clients.

## Development

```sh
cd sdk/go
gofmt -l .
go vet ./...
go test ./...
go test -race ./...
URSLY_TEST_URL=http://127.0.0.1:PORT go test -run Real -v ./...
```

The module is `ursly.io/sdk/go`, versioned by `ursly.Version` in `version.go`; package `ursly` has no dependencies outside the standard library. Default tests use injected transports and real local HTTP servers. `URSLY_TEST_URL` adds contract and validation-parity tests against an isolated real Rust API, which generates the authoritative OpenAPI and catalogs. [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.
