# 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.