CLI app for developers prototyping atproto functionality
1
fork

Configure Feed

Select the types of activity you want to include in your feed.

atproto-devtool#

Last verified: 2026-04-21

Single-crate Rust 2024 binary providing developer tooling for the atproto ecosystem. Shipped subcommands: test labeler (conformance suite against an atproto labeler, including an optional report-submission stage) and test oauth client (conformance suite for an atproto OAuth client — static checks plus an optional interactive mode that runs an in-process fake authorization server).

Tech stack#

  • Rust 2024, single crate (binary + library). Library target exists so integration tests under tests/*.rs can reach pipeline internals via atproto_devtool::commands::test::labeler::... and atproto_devtool::commands::test::oauth::client::....
  • Async runtime: tokio (rt + macros + time + net + signal — not full).
  • HTTP: reqwest with rustls. WebSockets: tokio-tungstenite with rustls-tls-native-roots. In-process HTTP server (fake AS for the oauth client interactive mode, plus the labeler report stage's self-mint DID doc server): axum 0.8 (default features off; http1 + json + tokio only).
  • CLI: clap derive. Diagnostics: miette (fancy) + thiserror.
  • Crypto: k256 + p256 (ECDSA), sha2, multibase. getrandom is a direct dep so we can reach OsRng for sentinel run-ids and JWT jti nonces (the transitive rand_core inside elliptic-curve is built without the getrandom feature).
  • JWTs: two separate surfaces live side-by-side and do not share code.
    • src/common/jwt.rs — hand-rolled compact JWS encoder/decoder (ES256 / ES256K only), used by the labeler report stage to mint self-mint service-auth tokens. No JWT library.
    • src/common/oauth/jws.rs — typed wrappers around jsonwebtoken 10 (rust_crypto backend, no ring) for the OAuth client's RP. PKCS8 export on p256 is required because jsonwebtoken's rust_crypto backend loads keys through from_pkcs8_der.
  • Deterministic RNG for OAuth test flows: rand_chacha + rand_core (seeded ChaCha20Rng). Form encoding for OAuth request bodies: serde_urlencoded.
  • atproto types: atrium-api (not atrium-xrpc-client — we use reqwest directly through our own seams so every network hop is mockable).
  • Testing: insta snapshots, assert_cmd for CLI end-to-end.

Commands#

  • cargo build / cargo run -- test labeler <target> / cargo run -- test oauth client <target> (static checks) / cargo run -- test oauth client <target> interactive [--port N] [--public-base-url URL] (static checks plus in-process fake AS driving a full OAuth flow).
  • cargo test — unit + integration + snapshot tests.
  • cargo insta review — review snapshot diffs after test runs.
  • cargo fmt / cargo clippy -- -D warnings — both must be clean.
  • cargo read <crate> / cargo read --api <crate> / cargo read --docs <crate> — preferred over browsing docs.rs.

Project structure#

  • src/main.rs — thin tokio bootstrap over cli::run.
  • src/lib.rs — re-exports everything under the same tree so integration tests in tests/ can reach pipeline internals.
  • src/cli.rs — clap root, tracing subscriber wiring (--verbose toggles DEBUG), NO_COLOR/--no-color handling.
  • src/commands/test/labeler/ — the test labeler subcommand. See src/commands/test/labeler/CLAUDE.md for pipeline architecture. The report stage lives under src/commands/test/labeler/create_report/ (self-mint DID doc server, sentinel run-id, pollution checks).
  • src/commands/test/oauth/client/ — the test oauth client subcommand (discovery / metadata / JWKS static stages plus an optional interactive stage with scope_variations and dpop_edges sub-stages). Contains an in-process fake_as axum server module used in interactive mode. See src/commands/test/oauth/client/CLAUDE.md.
  • src/common/identity.rs — DID/handle/multikey/PLC primitives plus signing-key types (AnySigningKey, AnyVerifyingKey, AnySignature) shared across stages. See src/common/CLAUDE.md.
  • src/common/jwt.rs — compact JWS encoder/decoder (ES256 / ES256K) used by the labeler report stage to mint self-mint service-auth tokens. See src/common/CLAUDE.md.
  • src/common/oauth/ — OAuth 2.0 / JOSE primitives shared by the test oauth * family: clock (testable time source), jws (JWK parsing + ES256 signing via jsonwebtoken), relying_party (atproto-spec RP that drives PAR, PKCE S256, DPoP, private_key_jwt). See src/common/CLAUDE.md.
  • src/common/report.rs — shared report contract (CheckStatus, CheckResult, Stage, LabelerReport, RenderConfig, plus the blocked_by / skipped_with_reason helpers). Promoted out of the labeler tree in Phase 1 of the oauth client work; used by both labeler and oauth_client commands. Stage is a &'static str newtype so commands can extend the stage space without a shared enum.
  • src/common/diagnostics.rs — small helpers for building NamedSource and spans against JSON payloads.
  • tests/ — one integration binary per stage. Labeler: labeler_identity.rs, labeler_http.rs, labeler_subscription.rs, labeler_report.rs, labeler_endtoend.rs, labeler_cli.rs. OAuth client: oauth_client_discovery.rs, oauth_client_metadata.rs, oauth_client_jwks.rs, oauth_client_interactive.rs, oauth_client_endtoend.rs, oauth_client_cli.rs, oauth_client_broken_rp.rs (interactive-mode failure ACs driven by a deliberately broken RP), oauth_client_check_id_coverage.rs (guards that every check ID and diagnostic code is pinned in a snapshot). Shared fakes (FakeHttpClient, FakeDnsResolver, FakeRawHttpTee, FakeWebSocketClient, FakeJwksFetcher, FakeClock, plus the report stage's FakeCreateReportTee / FakePdsXrpcClient) live in tests/common/mod.rs, with fake smoke tests in tests/common_fakes.rs.
  • tests/fixtures/ — JSON + binary CBOR fixtures per stage. The tests/snapshots/ tree holds insta snapshots.
  • docs/implementation-plans/2026-04-13-test-labeler/ — the initial phased implementation plan for the four-stage labeler pipeline.
  • docs/implementation-plans/2026-04-17-labeler-report-stage/ — the phased implementation plan that added the labeler report stage.
  • docs/implementation-plans/2026-04-16-test-oauth-client/ — the phased implementation plan (Phases 1-8) that produced the oauth client subcommand.
  • docs/test-oauth-client-reachability.md — operator-facing guide for exposing the fake AS over HTTPS (tunnels / --public-base-url).

Conventions#

  • No mod.rs. Use the foo.rs + foo/ sibling-file layout.
  • #[expect(...)] over #[allow(...)] for every lint suppression.
  • Imports at module top only (the one exception is cfg()-gated items).
  • uninlined_format_args = "deny" is on in Cargo.toml. Always use format!("{x}"), never format!("{}", x).
  • All code comments end with a period. Sentence case for headings.
  • Every thiserror::Error that is rendered to users must also derive miette::Diagnostic with a stable code = "..." string. Diagnostic codes appear in snapshot tests — changing one is a breaking change.
  • Never use #[serde(flatten)] or #[serde(untagged)]; write explicit structs and/or custom visitors.
  • Newtypes everywhere: Did, Curve, AnyVerifyingKey, etc. Don't let raw strings leak across stage boundaries.

Testing pattern#

Every network-touching stage goes through an injected trait object (HttpClient, DnsResolver, RawHttpTee, WebSocketClient, CreateReportTee, PdsXrpcClient). Production wires in Real* implementations; tests wire in fakes from tests/common/mod.rs. Do not add network calls that bypass these seams — they make stages untestable and break snapshot determinism.

Integration tests pin per-check output via insta snapshots. When a check's rendered text, diagnostic code, or order changes, cargo insta review is the expected workflow. Treat every .snap file under tests/snapshots/ as part of the public contract for the CLI.

Boundaries#

  • Safe to edit: src/, tests/, docs/.
  • The docs/implementation-plans/ tree is historical — update it only when the plan genuinely needs revising, not as part of regular code changes.
  • Cargo.lock is committed; update it through cargo only.