Skip to content

Bring the Rust SDK to documentation parity with Python and TypeScript - #142

Open
ron-42 wants to merge 5 commits into
inkbox-ai:mainfrom
ron-42:feature/rust-sdk-parity
Open

Bring the Rust SDK to documentation parity with Python and TypeScript#142
ron-42 wants to merge 5 commits into
inkbox-ai:mainfrom
ron-42:feature/rust-sdk-parity

Conversation

@ron-42

@ron-42 ron-42 commented Aug 8, 2026

Copy link
Copy Markdown

Fixes #141

The gap

sdk/rust is feature-complete and published on crates.io as inkbox v0.5.14, but it is invisible from everywhere except its own directory:

  • README.md — the install table lists Python, TypeScript, and the CLI. No Rust row, no crates.io badge, and sdk/rust/ missing from "What's in this repo" — even though RELEASING.md already documents publishing to crates.io.
  • skills/ — ships inkbox-python and inkbox-ts. A coding agent asked to add Inkbox to a Rust project has no skill to load, so it guesses at the API or falls back to a Python/TS idiom that does not compile.
  • examples/ — every other SDK has runnable examples. sdk/rust/examples/ holds two low-level smoke checks, not agent examples. use-inkbox-signup/ and use-inkbox-vault/ are polyglot (.py + .ts) and Rust is conspicuously absent from both.

Net effect: a Rust user landing on the repo README has no signal the SDK exists.

Full evidence — the per-SDK example coverage table and the seven ways the Rust API actually diverges from Python/TS — is in #141.

What this PR adds

Docs, skills, and examples only. No change to sdk/rust's implementation.

1. README.md

  • Rust row in the install table — cargo add inkbox, linking ./sdk/rust/ and crates.io.
  • crates.io version badge alongside the existing PyPI and npm badges.
  • sdk/rust/, skills/inkbox-rust/, and examples/use-inkbox-rust/ rows in "What's in this repo", so the two new directories are reachable.

2. skills/inkbox-rust/

A new SKILL.md mirroring skills/inkbox-ts/SKILL.md section for section — same headings, same order, same tone and capability coverage (signup, identities, mail + imports + storage caps, mail clients, phone, text/SMS, iMessage, SMS opt-ins, A2A, vault, TOTP, admin resources, contact rules, contacts, notes, whoami, tunnels, webhooks, error handling, key conventions).

Adapted to Rust idioms rather than transliterated:

  • Arc<Inkbox>, inkbox::Result<T> / InkboxError, ? propagation.
  • Positional arguments instead of kwargs, with the Unset<T> and Option<Option<T>> tri-state sentinels called out explicitly — the single biggest porting trap.
  • Enum variants (MailRuleAction::Allow, CallOrigin::DedicatedNumber, SecretPayload::Login(..)) rather than string literals.
  • match on typed InkboxError variants instead of exception subclasses.
  • The tunnels-runtime cargo feature gate.

It also documents where Rust genuinely differs from the other SDKs, so the skill does not promise things the crate cannot do:

  • iter_emails drains every page eagerly into a Vec<Message> — not a lazy generator.
  • A2A receiver configuration and the standard outbound protocol client are Python/TS only; Rust exposes history, directories, and invitations.
  • Tunnels forward to a URL only — no in-process Fetch/ASGI handler equivalent.
  • The API is blocking; call it from spawn_blocking in an async context.
  • UnlockedVault mutators take &mut self.

skills/README.md gains the inkbox-rust row, a cargo add inkbox prerequisite, the manual-install cp line, and a "six skills" → "seven skills" correction.

skills/inkbox-all/SKILL.md — the index-of-skills skill — gains inkbox-rust under Core Skills, use-inkbox-rust under Related Examples, and a "How To Choose" bullet. Its Rust entry names the deltas above so an agent picking a skill knows what Rust does not do before it commits.

3. examples/use-inkbox-rust/

Four runnable examples as numbered binaries in one crate, deliberately mirroring the numbered scripts in use-inkbox-cli so the two sets line up:

Binary Mirrors What it does
01-identity-and-email 01-identity-and-email.sh + 03-phone-call.sh Create an identity (mailbox + tunnel atomic), send and read an email, place a call, clean up
02-vault-totp 02-vault-totp.sh Login credential with TOTP, generate codes, remove TOTP, clean up
04-inbox-monitor 04-inbox-monitor.sh Poll unread in a loop, read bodies, batch mark-read
07-signup use-inkbox-signup/agent_signup.py Self-register with no API key, verify, status, welcome email, cleanup

use-inkbox-signup and use-inkbox-vault are polyglot directories that had no Rust counterpart; those flows are covered here rather than by adding a Cargo.toml to each. Numbering leaves gaps where no Rust counterpart exists yet.

  • One dependency: inkbox. No clap, no uuid, no async runtime.
  • default-run keeps a bare cargo run working for 01; the rest use cargo run --bin <name>.
  • Teardown runs even when a step fails, so a bad run never leaves a live identity — or a billed phone number — behind.
  • Billable and outward-facing steps are opt-in: the call needs INKBOX_DEMO_PHONE, signup registration is its own subcommand.
  • 02-vault-totp deliberately demonstrates the snapshot semantics of credentials() — it prints the login count before and after re-unlocking, because a freshly created secret is not in the unlock snapshot.

Verification

Every Rust snippet is grounded in sdk/rust/src — no invented methods.

Compile. cargo build, cargo fmt --check, and cargo clippy --all-targets -- -D warnings all pass in examples/use-inkbox-rust against inkbox v0.5.14 from crates.io.

Every code sample in the skill type-checks. All snippets in skills/inkbox-rust/SKILL.md were extracted into a scratch crate and compiled against the published crate — zero errors. The only warnings are the intentional uses of mail_contact_rules(), phone_contact_rules(), and the org-level create_signing_key(), all three of which the skill labels DEPRECATED. This caught four real defects before they shipped:

  • ContactImportResult::created_ids is a method, not a field (and the per-card list is results, not errors)
  • ContactFactCitation::source_url is Option<String>
  • TextWebhookPayload::event_type is a typed enum, not a string
  • WebhookSubscriptionCreateResponse serde-flattens its base under .subscription

End-to-end against the live API. Examples 01, 02, and 04 all ran clean against a real organization with an admin-scoped key, plus 07's cleanup and argument-handling paths. A follow-up list confirmed no residue. (07 register was not run — it provisions a new organization and emails a real human. The phone leg of 01 was likewise left off, since it provisions a billable number and dials a real destination.)

=> Creating identity: rust-demo (mailbox + tunnel provisioned atomically)
   rust-demo@inkboxmail.com
   https://rust-demo.inkboxwire.com
=> Sending a test email to rust-demo@inkboxmail.com
   sent 5aa255d4-4ac5-4286-9c5c-7e61e2dab4ef (sent)
=> Polling the inbox for delivery
   [rust-demo@inkboxmail.com] Rust SDK demo
   Hello from the Inkbox Rust SDK!
=> Cleaning up...
   Done.

02-vault-totp live-confirmed the snapshot semantics the skill documents (0 login(s) before re-unlock, 1 after) and produced correctly rotating codes — the same value twice inside one 30-second window, then a new one. 04-inbox-monitor found the unread message, printed the body, marked it read, and came back clean on the next check; its output also incidentally confirmed the Free-plan stored-body footer the skill warns about.

A separate read-only pass exercised the rest of the documented surface live — whoami discriminant, identity facade accessors, mailbox storage fields (storage_limit_bytes came back 1073741824, exactly 1 GiB, confirming the binary-units guidance), iter_emails, the serde-flattened ThreadDetail.thread.folder, list_folders, identity-keyed contact rules, the iMessage triage number, vault info/secrets/credentials, contacts, notes, both A2A directories plus tasks/contexts with next_cursor, signing-key status, webhook subscriptions, phone numbers, tunnels, SMS opt-ins, and domains. Every field name and shape matched what the skill documents.

One finding worth surfacing. Re-running 01 fails with 409 agent_handle_unavailable: handles live in a namespace shared with tunnels and mail and stay reserved after deletion. The SDK ships a typed view for exactly this — HandleUnavailableError::from_error(&e)blocking_namespace — which the first draft of the skill did not mention, despite this being the most common create_identity failure. Now documented as a "Handle collisions (409)" subsection under Error Handling, with a note in the example README. Type-checking alone would never have caught it.

CI is unaffected: .github/workflows/tests.yml only builds sdk/rust, and the example is a standalone crate outside any workspace.

sdk/rust is feature-complete and published on crates.io, but was not
surfaced anywhere else in the repo: the README install table listed only
Python, TypeScript, and the CLI; skills/ shipped inkbox-python and
inkbox-ts with no Rust equivalent; and examples/ had no Rust agent (the
two files under sdk/rust/examples/ are low-level smoke checks). A Rust
user landing on the repo README had no signal the SDK existed.

Docs, skills, and examples only — no change to sdk/rust's implementation.

README.md
  Rust row in the install table (cargo add inkbox, linking ./sdk/rust/
  and crates.io), a crates.io badge, and sdk/rust/ plus the two new
  directories in the "What's in this repo" table.

skills/inkbox-rust/
  New SKILL.md mirroring skills/inkbox-ts/SKILL.md section for section,
  adapted to Rust idioms: Arc<Inkbox>, inkbox::Result<T> / InkboxError,
  positional arguments, the Unset<T> and Option<Option<T>> tri-state
  sentinels, typed enum variants, and the tunnels-runtime feature gate.
  It also names where Rust genuinely differs — iter_emails drains every
  page eagerly into a Vec, A2A receiver config and the outbound protocol
  client are Python/TS only, tunnels forward to a URL rather than an
  in-process handler, and UnlockedVault mutators take &mut self — so the
  skill does not promise what the crate cannot do.

skills/README.md, skills/inkbox-all/SKILL.md
  Register the new skill in the skills table and the skills index, with
  a cargo add prerequisite and a How To Choose entry.

examples/use-inkbox-rust/
  Four runnable binaries in one crate, numbered to line up with the
  scripts in use-inkbox-cli: 01-identity-and-email, 02-vault-totp,
  04-inbox-monitor, and 07-signup. use-inkbox-signup and
  use-inkbox-vault are polyglot directories that had no Rust
  counterpart; those flows are covered here. One dependency, no async
  runtime. Teardown runs even when a step fails, and billable or
  outward-facing steps are opt-in.

Verified: cargo build, cargo fmt --check, and cargo clippy --all-targets
-D warnings all pass against inkbox v0.5.14 from crates.io. Every code
sample in the skill was extracted into a scratch crate and type-checked
against the published crate, which caught four defects before they
shipped (created_ids is a method not a field, source_url is an Option,
event_type is a typed enum, and the create response flattens its base
under .subscription). Examples 01, 02, and 04 were run end to end
against a live organization, plus 07's cleanup path; a follow-up list
confirmed no residue. That live run also surfaced the 409
agent_handle_unavailable case and its HandleUnavailableError view, now
documented under Error Handling.

CI is unaffected: tests.yml only builds sdk/rust, and the example is a
standalone crate outside any workspace.

@dimavrem22 dimavrem22 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Two blocking issues:

  1. The newly advertised Rust 1.74 minimum does not work with a fresh dependency resolution. cargo +1.74.1 build in examples/use-inkbox-rust fails before compilation because the current transitive graph includes a Rust-2024 manifest and dependencies requiring newer compilers. The successful build on current stable does not verify the documented MSRV. Please either raise the documented minimum consistently or constrain the dependency graph to 1.74-compatible releases, and add a minimum-toolchain CI check.

  2. 02-vault-totp does not fulfill the promise that teardown runs when a step fails. Once create_secret succeeds, any later error returns through run; cleanup deletes only the identity, while the vault secret remains unless execution reaches the success-path delete_secret. Please retain the secret ID outside the fallible workflow and best-effort delete it on every exit before deleting the identity.

I verified the current-toolchain build, formatting, and clippy checks pass; these failures are in the advertised compatibility and error-cleanup paths.

Two review findings, both real.

1. The advertised Rust 1.74 minimum was never achievable.

   rust-version was copied from sdk/rust/Cargo.toml and only ever verified
   on current stable, which proves nothing about the floor. With a fresh
   resolution, cargo 1.74 cannot even parse the manifest of rand_pcg
   0.10.2 (edition 2024), and 1.85 is rejected by icu_* 2.2 and
   idna_adapter 1.2, which require 1.86.

   Measured the real floor rather than guessing: 1.74.1 and 1.85.0 both
   fail, 1.86.0 resolves and builds. The example now declares
   rust-version = "1.86", and Cargo.lock is committed so that number is
   reproducible instead of drifting with upstream releases — .gitignore
   updated accordingly, with the reasoning recorded in both files.

   Documented minimums are now consistent across the root README install
   table, skills/README.md, the skill, and the example README. The skill
   and example README also explain why the crate's own declared 1.74 is
   not attainable, so the next reader does not "fix" it back.

   Added CI: rust-example builds, clippies, and format-checks with
   --locked on both 1.86.0 and stable. A second advisory job,
   rust-example-fresh-resolution, deletes the lockfile and rebuilds on
   1.86.0, so dependency drift past the documented minimum surfaces as a
   warning rather than a surprise for the next contributor. This is the
   one place this branch touches something outside docs, skills, and
   examples.

2. 02-vault-totp orphaned its vault secret on any mid-workflow failure.

   The secret id lived inside run(), and delete_secret sat on the success
   path, so any error after create_secret returned straight past it;
   cleanup then deleted only the identity.

   Confirmed the leak is real rather than assumed — a vault secret is an
   organization-level row that does not cascade: creating one, deleting
   its identity, and listing secrets leaves the row behind.

   The id is now recorded in an out-parameter the moment the secret
   exists, and cleanup best-effort deletes it on every exit, before the
   identity. The success-path delete is gone, so there is exactly one
   teardown path. Verified by injecting a failure immediately after
   creation: the secret is deleted, the identity is deleted, and the
   original error still propagates. The happy path is unchanged.

   02 also now honours INKBOX_AGENT_HANDLE like the other three
   binaries, since deleted handles stay reserved and a fixed constant
   made re-runs fail with a 409.

Verified: build, clippy -D warnings, and fmt --check pass with --locked
on 1.86.0 and stable; 1.85.0 and 1.74.1 still fail, so the floor is
meaningful. Skill code samples still type-check with zero errors.

@dimavrem22 dimavrem22 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

One remaining documentation fix is needed.

Comment thread skills/inkbox-rust/SKILL.md Outdated
Admin-only free-form notes with per-identity access grants. There is no wildcard for notes — grant identities explicitly. Note and identity ids are `Uuid`, not strings.

```rust
use uuid::Uuid;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please remove this unused import. The skill tells users to depend only on inkbox; uuid is only transitive and is not available to consumer code, so this snippet fails with E0432 unless users separately add uuid.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch, removed in 82b2ffe.

My snippet check had uuid as its own dependency, so it was not building the way a reader would. Rebuilding it with inkbox alone also caught two spots that genuinely need serde_json (webhook payload, tunnel metadata), now flagged at the call site and in Install.

The skill compiles against inkbox plus serde_json and nothing else.

ron-42 added 2 commits August 9, 2026 10:56
Follow-up to bd28d72, which corrected the example but left the published
crate declaring a minimum it cannot meet.

sdk/rust/Cargo.toml declared rust-version = "1.74". That is not
achievable, and — unlike the example — a committed Cargo.lock does not
rescue it, because a library's lockfile does not apply to its consumers:
`cargo add inkbox` always resolves fresh. Measured rather than assumed:

  1.74.1  fails even with --locked; base64ct 1.8.3 in the committed
          lockfile is an edition-2024 manifest that cargo cannot parse
  1.82.0  same, fails on base64ct 1.8.3
  1.85.0  rejected by icu_* 2.2
  1.86.0  builds and passes the full suite (231 and 293 tests across
          default and tunnels-runtime)

So the floor is 1.86 for consumers and for developing the crate alike.
Bumped rust-version to 1.86 and documented the reason in the manifest and
sdk/rust/README.md — the constraint comes entirely from the transitive
graph, not from anything in this crate's source, which is why it drifted
unnoticed.

This is a metadata and documentation change: no source and no lockfile in
sdk/rust is modified.

Added a rust-msrv CI job building and testing both feature sets on the
declared minimum, so the floor cannot regress silently the next time a
dependency raises its own. rust-tests continues to cover stable.

Also narrowed the rust-example job: clippy and rustfmt now run on stable
only. Lint sets drift between compiler releases, so pinning them to the
MSRV toolchain would fail on lints the minimum-supported build has no say
over. The MSRV entry still builds.

Every Rust MSRV claim in the repo now reads 1.86 and agrees with the
manifests: the root README install table, skills/README.md, the skill,
sdk/rust/README.md, both Cargo.tomls, and the CI matrices. The passages
that previously explained "the crate declares 1.74" were updated rather
than left to contradict it.

Verified: rust-msrv, rust-tests, and the rust-example matrix all
reproduced locally and pass.
07-signup had a fail_str wrapper that only forwarded to fail, left over
from an earlier iteration, and unique_suffix summed subsec nanos with
whole seconds. That sum is meaningless and its {:08x} format implied a
width the value did not have. Use the low 32 bits of the nanosecond
timestamp so the suffix is exactly 8 hex characters.

No behaviour change beyond the suffix format.
@ron-42

ron-42 commented Aug 9, 2026

Copy link
Copy Markdown
Author

Two blocking issues:

1. The newly advertised Rust 1.74 minimum does not work with a fresh dependency resolution. `cargo +1.74.1 build` in `examples/use-inkbox-rust` fails before compilation because the current transitive graph includes a Rust-2024 manifest and dependencies requiring newer compilers. The successful build on current stable does not verify the documented MSRV. Please either raise the documented minimum consistently or constrain the dependency graph to 1.74-compatible releases, and add a minimum-toolchain CI check.

2. `02-vault-totp` does not fulfill the promise that teardown runs when a step fails. Once `create_secret` succeeds, any later error returns through `run`; `cleanup` deletes only the identity, while the vault secret remains unless execution reaches the success-path `delete_secret`. Please retain the secret ID outside the fallible workflow and best-effort delete it on every exit before deleting the identity.

I verified the current-toolchain build, formatting, and clippy checks pass; these failures are in the advertised compatibility and error-cleanup paths.

Both fixed, thanks.

1. MSRV

Raised the minimum instead of pinning deps. Tested it this time:

Toolchain Result
1.74.1 fails, rand_pcg 0.10.2 is edition 2024
1.85.0 fails, icu_* 2.2 needs 1.86
1.86.0 works

rust-version is now 1.86, and Cargo.lock is committed so it stays reproducible. Every doc says 1.86.

Note that cargo +1.74.1 build still fails. 1.74 is not reachable with this graph, so the fix was to stop claiming it.

sdk/rust/Cargo.toml had the same bug. It failed on 1.74 even with its own lockfile, because base64ct 1.8.3 is edition 2024. A library lockfile does not apply to consumers, so cargo add inkbox always resolves fresh. Bumped it too. Metadata and README only, no source or lockfile. That is published crate metadata though, so it should go out with a release rather than quietly.

New CI jobs:

  • rust-msrv: SDK, both feature sets, at the declared minimum
  • rust-example: 1.86 and stable, with --locked. Clippy and fmt run on stable only, since lint sets change between releases and the MSRV build has no say over them
  • rust-example-fresh-resolution: drops the lockfile to catch future drift

2. Teardown

The secret id now lives outside run, and cleanup deletes it best effort on every exit, before the identity. delete_secret appears once now.

Forced an error right after creation to check:

   Created secret: 23fae02f-...
=> Cleaning up...
   Deleted secret 23fae02f-...
Error: InvalidArgument("INJECTED FAILURE")

I also confirmed vault secrets do survive identity deletion, so this was a real leak.

Also 02 now reads INKBOX_AGENT_HANDLE, since deleted handles stay reserved and re-runs hit a 409.

Commits: bd28d72, e7878ec, ce23ee5. The CI jobs pass locally but have not run on GitHub yet.

The Notes snippet opened with `use uuid::Uuid;`. The import was unused,
and worse, unusable: the skill tells readers to run `cargo add inkbox`,
and inkbox does not re-export uuid, so the snippet failed with E0432
unless the reader separately added the crate.

My verification harness never caught this because it listed uuid and
serde_json as its own dependencies, so it was not building the way a
reader following the skill would. Rebuilt it with inkbox alone, which
surfaced every snippet quietly relying on a crate the skill never
mentions. Two are real and legitimate: deserializing a webhook payload
and building tunnel metadata both need serde_json. Both are now called
out at the call site, and Install & Init states up front which extra
crates are needed and why inkbox does not provide them.

The harness now depends on exactly what the skill tells a reader to add,
with uuid deliberately absent so a snippet needing it fails the check
rather than passing silently.

Verified: the full skill compiles against inkbox + serde_json only.
@ron-42
ron-42 requested a review from dimavrem22 August 9, 2026 06:19
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

The Rust SDK is invisible outside sdk/rust/

2 participants