CLI app for developers prototyping atproto functionality
1
fork

Configure Feed

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

fix(create_report): target the reporter's PDS, not the labeler's

`createSession` / `getServiceAuth` must be dispatched against the PDS
that actually hosts the reporter's account. The pipeline was reusing
`IdentityFacts::pds_endpoint` — the PDS advertised by the *labeler's*
DID document — which surfaced as `InvalidToken: Token could not be
verified` whenever the reporter and the labeler lived on different
PDS shards (e.g., two different `*.host.bsky.network` hosts).

The pipeline now resolves `--handle` via the existing
`resolve_handle` → `resolve_did` → `find_service("#atproto_pds")`
chain and constructs `RealPdsXrpcClient` against the reporter's own
PDS. Resolution failures flatten to a string and ride through a new
`CreateReportRunOptions::pds_resolution_error` so both PDS-mediated
rows surface a `NetworkError` with a specific message instead of a
silent `Skipped`; the 10-row invariant and canonical ordering are
preserved.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

authored by

Jack Grigg
Claude Opus 4.7
and committed by
Tangled
c54185f3 d94371df

+89 -15
+12 -2
src/commands/test/labeler/CLAUDE.md
··· 168 168 `pds_service_auth_accepted` and `pds_proxied_accepted` checks require 169 169 `--handle` + `--app-password` (enforced as a symmetric clap `requires`). 170 170 The pipeline constructs a `RealPdsXrpcClient` only when credentials are 171 - present and identity produced a PDS endpoint; otherwise the checks 172 - emit `Skipped` with a reason. 171 + present; otherwise the checks emit `Skipped` with a reason. 172 + - **PDS client targets the reporter's PDS, not the labeler's**: 173 + `createSession` / `getServiceAuth` must be dispatched against the PDS 174 + that actually hosts the reporter's account. The pipeline resolves 175 + `--handle` (via `resolve_handle` → `resolve_did` → `find_service` for 176 + `#atproto_pds`) and uses the resulting URL for `RealPdsXrpcClient`. 177 + Using `IdentityFacts::pds_endpoint` (the labeler's PDS) would surface 178 + as `InvalidToken: Token could not be verified` when reporter and 179 + labeler live on different PDS shards. Resolution failures flatten to a 180 + string and ride through `CreateReportRunOptions::pds_resolution_error` 181 + so both PDS-mediated rows surface a `NetworkError` with a specific 182 + message instead of a silent `Skipped`. 173 183 - **PDS-mediated failures split by origin, not by variant**: a single 174 184 `PdsServiceAuthRejected` / `PdsProxiedRejected` diagnostic carries a 175 185 `ResponseOrigin` field so labeler-side rejections classify as
+20 -1
src/commands/test/labeler/create_report.rs
··· 631 631 pub self_mint_signer: Option<&'a self_mint::SelfMintSigner>, 632 632 pub pds_credentials: Option<&'a crate::commands::test::labeler::pipeline::PdsCredentials>, 633 633 pub pds_xrpc_client: Option<&'a dyn PdsXrpcClient>, 634 + /// Populated by the pipeline when `--handle` was supplied but resolving 635 + /// the handle to the reporter's PDS endpoint failed. Surfaced as a 636 + /// `NetworkError` on both PDS-mediated checks so the operator can tell 637 + /// a resolution failure apart from a missing-credentials skip. 638 + pub pds_resolution_error: Option<&'a str>, 634 639 pub run_id: &'a str, 635 640 } 636 641 ··· 1094 1099 let pds_gate_reason: &'static str = "requires --handle, --app-password, and --commit-report"; 1095 1100 let pds_ready = 1096 1101 opts.commit_report && opts.pds_credentials.is_some() && opts.pds_xrpc_client.is_some(); 1102 + // Distinguish "no credentials" (skip) from "credentials supplied but the 1103 + // reporter's PDS could not be resolved" (network error) so the operator 1104 + // sees a useful failure mode rather than a silent skip. 1105 + let pds_resolution_failed = opts.commit_report 1106 + && opts.pds_credentials.is_some() 1107 + && opts.pds_xrpc_client.is_none() 1108 + && opts.pds_resolution_error.is_some(); 1097 1109 1098 - if !pds_ready { 1110 + if pds_resolution_failed { 1111 + let msg = opts 1112 + .pds_resolution_error 1113 + .expect("pds_resolution_failed implies Some") 1114 + .to_string(); 1115 + results.push(Check::PdsServiceAuthAccepted.network_error(msg.clone())); 1116 + results.push(Check::PdsProxiedAccepted.network_error(msg)); 1117 + } else if !pds_ready { 1099 1118 results.push(Check::PdsServiceAuthAccepted.skip(pds_gate_reason)); 1100 1119 results.push(Check::PdsProxiedAccepted.skip(pds_gate_reason)); 1101 1120 } else {
+55 -12
src/commands/test/labeler/pipeline.rs
··· 18 18 CheckResult, CheckStatus, LabelerReport, ReportHeader, Stage, 19 19 }; 20 20 use crate::commands::test::labeler::subscription::{self, RealWebSocketClient}; 21 - use crate::common::identity::{Did, DnsResolver, HttpClient, is_local_labeler_hostname}; 21 + use crate::common::identity::{ 22 + Did, DnsResolver, HttpClient, find_service, is_local_labeler_hostname, resolve_did, 23 + resolve_handle, 24 + }; 22 25 23 26 /// A labeler target: either a resolvable identifier (handle or DID) or a raw endpoint URL. 24 27 #[derive(Debug, Clone)] ··· 441 444 } 442 445 443 446 // Construct the PDS XRPC client when credentials are supplied. The test 444 - // override takes precedence if provided; else construct the real client 445 - // from the PDS endpoint discovered by identity. 447 + // override takes precedence if provided; else we resolve the reporter's 448 + // own PDS endpoint from `--handle` and construct a real client against 449 + // it. The labeler's PDS (`identity_output.facts.pds_endpoint`) is NOT 450 + // the right target here: `createSession` / `getServiceAuth` must hit the 451 + // PDS that actually hosts the reporter's account, otherwise the PDS 452 + // rejects the access token (`InvalidToken: Token could not be verified`). 453 + let mut pds_resolution_error: Option<String> = None; 446 454 let pds_xrpc_client_owned: Option<create_report::RealPdsXrpcClient> = 447 455 if opts.pds_xrpc_client_override.is_some() { 448 456 // Test override supplied; don't construct real client. 449 457 None 450 - } else if opts.pds_credentials.is_some() { 451 - // Check that identity produced a PDS endpoint and that we have a real HTTP client. 452 - match (&identity_output.facts, &opts.create_report_tee) { 453 - (Some(facts), CreateReportTeeKind::Real(http_client)) => { 454 - Some(create_report::RealPdsXrpcClient::new( 455 - (*http_client).clone(), 456 - facts.pds_endpoint.clone(), 457 - )) 458 + } else if let (Some(creds), CreateReportTeeKind::Real(http_client)) = 459 + (opts.pds_credentials, &opts.create_report_tee) 460 + { 461 + match resolve_reporter_pds_endpoint(&creds.handle, opts.http, opts.dns).await { 462 + Ok(url) => Some(create_report::RealPdsXrpcClient::new( 463 + (*http_client).clone(), 464 + url, 465 + )), 466 + Err(msg) => { 467 + pds_resolution_error = Some(msg); 468 + None 458 469 } 459 - _ => None, 460 470 } 461 471 } else { 462 472 None ··· 475 485 self_mint_signer: opts.self_mint_signer, 476 486 pds_credentials: opts.pds_credentials, 477 487 pds_xrpc_client: opts.pds_xrpc_client_override.or(pds_xrpc_client_ref), 488 + pds_resolution_error: pds_resolution_error.as_deref(), 478 489 run_id: opts.run_id, 479 490 }; 480 491 let labeler_endpoint_for_report = labeler_endpoint.clone(); ··· 508 519 509 520 report.finish(); 510 521 report 522 + } 523 + 524 + /// Resolve the reporter's handle to the service endpoint of their PDS. 525 + /// 526 + /// This is the endpoint `createSession` and `getServiceAuth` must be 527 + /// dispatched against in the report stage's PDS-mediated modes. It is 528 + /// generally NOT the same as `IdentityFacts::pds_endpoint`, which is the 529 + /// PDS advertised by the *labeler's* DID document. 530 + /// 531 + /// Failures are flattened to a single human-readable string — the report 532 + /// stage surfaces it as a `NetworkError` on both PDS-mediated rows. 533 + async fn resolve_reporter_pds_endpoint( 534 + handle: &str, 535 + http: &dyn HttpClient, 536 + dns: &dyn DnsResolver, 537 + ) -> Result<Url, String> { 538 + let did = resolve_handle(handle, http, dns) 539 + .await 540 + .map_err(|e| format!("failed to resolve handle {handle} to a DID: {e}"))?; 541 + let raw_doc = resolve_did(&did, http) 542 + .await 543 + .map_err(|e| format!("failed to resolve DID {did} to a DID document: {e}"))?; 544 + let service = find_service(&raw_doc.parsed, "atproto_pds", "AtprotoPersonalDataServer") 545 + .ok_or_else(|| { 546 + format!("DID document for {did} does not advertise an #atproto_pds service") 547 + })?; 548 + Url::parse(&service.service_endpoint).map_err(|_| { 549 + format!( 550 + "DID document for {did} has a malformed #atproto_pds endpoint: {}", 551 + service.service_endpoint 552 + ) 553 + }) 511 554 } 512 555 513 556 /// Format a target for display in the report header.
+2
tests/labeler_report.rs
··· 141 141 self_mint_signer: None, 142 142 pds_credentials: None, 143 143 pds_xrpc_client: None, 144 + pds_resolution_error: None, 144 145 run_id: "test-run-id-0000", 145 146 } 146 147 } ··· 205 206 self_mint_signer: None, 206 207 pds_credentials: None, 207 208 pds_xrpc_client: None, 209 + pds_resolution_error: None, 208 210 run_id: "test-run-id-0000", 209 211 }; 210 212 let results = run_report_stage(&facts, &tee, opts).await;