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/*.rscan reach pipeline internals viaatproto_devtool::commands::test::labeler::...andatproto_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+tokioonly). - CLI: clap derive. Diagnostics: miette (fancy) + thiserror.
- Crypto: k256 + p256 (ECDSA), sha2, multibase.
getrandomis a direct dep so we can reachOsRngfor sentinel run-ids and JWTjtinonces (the transitiverand_coreinsideelliptic-curveis built without thegetrandomfeature). - 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 aroundjsonwebtoken10 (rust_cryptobackend, no ring) for the OAuth client's RP. PKCS8 export on p256 is required becausejsonwebtoken's rust_crypto backend loads keys throughfrom_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 overcli::run.src/lib.rs— re-exports everything under the same tree so integration tests intests/can reach pipeline internals.src/cli.rs— clap root, tracing subscriber wiring (--verbosetogglesDEBUG),NO_COLOR/--no-colorhandling.src/commands/test/labeler/— thetest labelersubcommand. Seesrc/commands/test/labeler/CLAUDE.mdfor pipeline architecture. The report stage lives undersrc/commands/test/labeler/create_report/(self-mint DID doc server, sentinel run-id, pollution checks).src/commands/test/oauth/client/— thetest oauth clientsubcommand (discovery / metadata / JWKS static stages plus an optional interactive stage withscope_variationsanddpop_edgessub-stages). Contains an in-processfake_asaxum server module used in interactive mode. Seesrc/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. Seesrc/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. Seesrc/common/CLAUDE.md.src/common/oauth/— OAuth 2.0 / JOSE primitives shared by thetest oauth *family:clock(testable time source),jws(JWK parsing + ES256 signing viajsonwebtoken),relying_party(atproto-spec RP that drives PAR, PKCE S256, DPoP, private_key_jwt). Seesrc/common/CLAUDE.md.src/common/report.rs— shared report contract (CheckStatus,CheckResult,Stage,LabelerReport,RenderConfig, plus theblocked_by/skipped_with_reasonhelpers). Promoted out of the labeler tree in Phase 1 of the oauth client work; used by both labeler and oauth_client commands.Stageis a&'static strnewtype so commands can extend the stage space without a shared enum.src/common/diagnostics.rs— small helpers for buildingNamedSourceand 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'sFakeCreateReportTee/FakePdsXrpcClient) live intests/common/mod.rs, with fake smoke tests intests/common_fakes.rs.tests/fixtures/— JSON + binary CBOR fixtures per stage. Thetests/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 thefoo.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 inCargo.toml. Always useformat!("{x}"), neverformat!("{}", x).- All code comments end with a period. Sentence case for headings.
- Every
thiserror::Errorthat is rendered to users must also derivemiette::Diagnosticwith a stablecode = "..."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.lockis committed; update it throughcargoonly.