jev-sdk 0.1.0

Unofficial Rust client for the TypeSafe AI System One API (Jev): typed decisions with calibrated probabilities
Documentation
# AGENTS.md

Guidance for coding agents working in this repository.

## What this is

`jev-sdk` — an unofficial Rust client library for the
[TypeSafe AI](https://typesafe.ai) System One API
([Jev](https://typesafe.ai/blog/introducing-system-one-models-and-jev)).
Jev evaluates typed *questions* (Noul/Choice/Score) against a *state* and
returns structured decisions with calibrated probabilities and confidence.
(Personal, unaffiliated crate — keep "unofficial" in user-facing
descriptions.)

Authoritative API contract: <https://docs.typesafe.ai/api> (endpoint
`POST https://api.typesafe.ai/v1/systemone`, Bearer auth; models at
`GET /v1/models`). The official Python SDK behavior is documented at
<https://docs.typesafe.ai/sdk/python/api> — mirror its semantics where the
HTTP API is silent.

Related tool built on this crate:
[jev-cli](https://github.com/portlandhodl/jev-cli) (agent-facing CLI).

## Layout

```
Cargo.toml
src/
├── lib.rs        # crate docs + public re-exports
├── client.rs     # TypeSafeClient, ClientBuilder, retry loop, base-URL validation
├── question.rs   # Noul / Choice / Score question types
├── response.rs   # SystemOneResponse / Answer / typed accessors
├── models.rs     # Models API types
├── error.rs      # Error / ApiError (parses {"detail": ...} shapes)
└── retry.rs      # RetryPolicy: saturating backoff, Retry-After cap
examples/triage.rs
tests/
├── live.rs       # real-API tests, skip silently without TYPESAFE_API_KEY
└── hardening.rs  # security regression tests w/ in-process TCP server
```

## Commands

```sh
cargo build
cargo test                          # unit + hardening (live tests skip w/o key)
TYPESAFE_API_KEY=... cargo test     # includes live API tests
TYPESAFE_API_KEY=... cargo run --example triage
cargo clippy --all-targets          # must be warning-free
cargo fmt -- --check                # must be clean
cargo audit                         # dependency vulnerability scan; must be clean
cargo publish --dry-run             # packaging check
```

Run all of the above before considering a change done.

## Conventions

- Async-only (tokio + reqwest/rustls). No blocking façade exists; discuss
  before adding one.
- No `unwrap`/`expect`/panics in library code paths that user or network
  input can reach. All fallible operations return `Error`. (Tests are
  exempt.)
- Keep the public API surface minimal and consistent with the official
  Python/JS SDKs' naming (`system_one`, `list_models`, `Noul`, `Choice`,
  `Score`, `RetryPolicy`, env vars `TYPESAFE_API_KEY`,
  `TYPESAFE_DEFAULT_MODEL`, `TYPESAFE_BASE_URL`).
- Questions/answers serialize with `#[serde(tag = "type", rename_all =
  "lowercase")]`; insertion order of question/option maps is semantically
  meaningful (preserve_order + indexmap) — do not switch to `HashMap`. All
  wire types (questions, answers, models) derive both `Serialize` and
  `Deserialize` so tools can round-trip them.
- Add tests for every behavior change; hardening behaviors get regression
  tests in `tests/hardening.rs`.
- Update `README.md` and this file when behavior, layout, or commands
  change.

## Security invariants — do not regress

These exist deliberately; each has a test. Changing one requires an explicit
reason in the commit/PR description.

1. **Never commit or print credentials.** API keys come from
   `TYPESAFE_API_KEY` or `ClientBuilder::api_key` only. This crate
   deliberately does **not** load `.env` or any file: libraries must not
   mutate process env as a side effect. `Debug` impls for
   `TypeSafeClient`/`ClientBuilder` are hand-written to redact the key —
   never re-derive `Debug` on them. The auth `HeaderValue` is marked
   sensitive. Tests verify redaction.
2. **Header-injection guard**: keys/tokens containing header control
   characters are rejected at build time (`HeaderValue::from_str`
   validation).
3. **HTTPS-only base URLs** (`validate_base_url` in client.rs); plain HTTP is
   allowed exclusively for loopback hosts (`localhost`, `*.localhost`,
   `127.0.0.0/8`, `::1`) to support local test servers. Error messages must
   not quote the key.
4. **Redirects disabled** on the reqwest client so `Authorization` headers
   can never be forwarded cross-origin.
5. **Bounded response bodies**: success bodies capped at 16 MiB
   (`MAX_SUCCESS_BODY`), error bodies truncated to a 64 KiB prefix
   (`MAX_ERROR_BODY`), read incrementally via `chunk()`; never
   `resp.text()`/`resp.json()` unbounded.
6. **Bounded waits**: `RetryPolicy::backoff_for` is total (saturates on
   overflow, sanitizes non-finite `multiplier`); `Retry-After` hints are
   capped at `MAX_RETRY_AFTER` (60s). Never let server input cause unbounded
   sleep or panic.
7. **TLS**: rustls with certificate verification; no feature or option may
   disable verification. Do not enable reqwest's gzip/brotli features without
   reviewing decompression-bomb exposure.
8. **Retries** only on transient statuses (default: 408/409/425/429/5xx/529)
   and connect/timeout errors. The evaluation API is idempotent; do not add
   retries for 4xx validation/auth failures.
9. When a user supplies their own `reqwest::Client` (`http_client`), the
   SDK's transport hardening no longer applies — the docs must say so.
10. Run `cargo audit` after any dependency change; keep dependencies minimal
    and mainstream.

## Gotchas

- `tests/live.rs` **skips silently** when `TYPESAFE_API_KEY` is unset — a
  green run without the key does not prove live behavior.
- The API echoes the resolved model version (e.g. `jev-1.13.0`) in responses,
  not the requested alias — don't assert equality with `jev-latest`.
- Score answers may omit `probabilities`; the type uses `#[serde(default)]`
  on purpose.