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): attribute PDS-mediated failures to the right party

A PDS-side failure in Mode 2 (`getServiceAuth` refused) or Mode 3
(proxy rejected before forwarding) was being rendered with the
labeler-named diagnostic text, making the error read as if the
labeler had rejected a token that the labeler never saw.

Add a `ResponseOrigin` discriminator field to `PdsServiceAuthRejected`
and `PdsProxiedRejected` so one diagnostic per check can carry both
labeler-side (`SpecViolation`) and PDS-side (`NetworkError`)
outcomes, and route the four call sites accordingly. Mode 3 uses the
existing upstream-envelope heuristic (`UpstreamError` /
`UpstreamFailure` / 502 / 504) to pick the origin.

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

authored by

Jack Grigg
Claude Opus 4.7
and committed by
Tangled
d94371df a340be0b

+66 -13
+11 -2
src/commands/test/labeler/CLAUDE.md
··· 1 1 # test labeler 2 2 3 - Last verified: 2026-04-19 3 + Last verified: 2026-04-21 4 4 5 5 ## Purpose 6 6 ··· 53 53 CreateReportTee, RealCreateReportTee, RawCreateReportResponse, 54 54 PdsXrpcClient, RealPdsXrpcClient, RawPdsXrpcResponse, 55 55 PdsJwtFetcher, PdsProxiedPoster, CreateReportRunOptions, 56 - XrpcErrorEnvelope, RejectionShape}` plus per-check diagnostic structs 56 + XrpcErrorEnvelope, RejectionShape, ResponseOrigin}` plus per-check diagnostic structs 57 57 (`ContractMissing`, `UnauthenticatedAccepted`, `MalformedBearerAccepted`, 58 58 `WrongAudAccepted`, `WrongLxmAccepted`, `ExpiredAccepted`, `ShapeNot400`, 59 59 `SelfMintRejected`, `PdsServiceAuthRejected`, `PdsProxiedRejected`). ··· 170 170 The pipeline constructs a `RealPdsXrpcClient` only when credentials are 171 171 present and identity produced a PDS endpoint; otherwise the checks 172 172 emit `Skipped` with a reason. 173 + - **PDS-mediated failures split by origin, not by variant**: a single 174 + `PdsServiceAuthRejected` / `PdsProxiedRejected` diagnostic carries a 175 + `ResponseOrigin` field so labeler-side rejections classify as 176 + `SpecViolation` while PDS-side rejections (`getServiceAuth` refused; 177 + proxy rejected before forwarding) classify as `NetworkError`. Keeping 178 + one variant per check preserves the one-diagnostic-per-check shape the 179 + report stage documents, and the Mode-3 upstream-envelope heuristic 180 + (`UpstreamError` / `UpstreamFailure` / 502 / 504) is what 181 + discriminates the two origins on the PDS-proxied path. 173 182 - **Sentinel reason string and run-id**: every committed report body 174 183 carries a sentinel reason built by `create_report::sentinel::build` 175 184 that encodes the run-id (16 hex chars from `getrandom`) and an RFC 3339
+55 -11
src/commands/test/labeler/create_report.rs
··· 360 360 Self::Failed(resp) | Self::InvalidBody { resp, .. } | Self::MissingToken(resp) => { 361 361 let (source_code, span) = body_as_named_source_from_pds(&resp); 362 362 let diag = CreateReportDiagnostic::PdsServiceAuthRejected { 363 + origin: ResponseOrigin::Pds, 363 364 status: resp.status.as_u16(), 364 365 source_code, 365 366 span, ··· 1164 1165 Ok(resp) => { 1165 1166 let (source_code, span) = body_as_named_source(&resp); 1166 1167 let diag = CreateReportDiagnostic::PdsServiceAuthRejected { 1168 + origin: ResponseOrigin::Labeler, 1167 1169 status: resp.status.as_u16(), 1168 1170 source_code, 1169 1171 span, ··· 1194 1196 // PDS surfaced a non-2xx. Interpret per envelope to 1195 1197 // distinguish PDS-side vs labeler-side: 1196 1198 let (source_code, span) = body_as_named_source_from_pds(&resp); 1197 - let diag = CreateReportDiagnostic::PdsProxiedRejected { 1198 - status: resp.status.as_u16(), 1199 - source_code, 1200 - span, 1201 - }; 1202 1199 let envelope = XrpcErrorEnvelope::parse(&resp.raw_body); 1203 1200 let err_name = envelope.as_ref().and_then(|e| e.error.clone()); 1204 1201 let is_upstream_label_error = matches!( ··· 1208 1205 || resp.status.as_u16() == 504; 1209 1206 if is_upstream_label_error { 1210 1207 // AC6.2: labeler-side rejection surfaced by PDS. 1208 + let diag = CreateReportDiagnostic::PdsProxiedRejected { 1209 + origin: ResponseOrigin::Labeler, 1210 + status: resp.status.as_u16(), 1211 + source_code, 1212 + span, 1213 + }; 1211 1214 results.push(Check::PdsProxiedAccepted.spec_violation(diag)); 1212 1215 } else { 1213 1216 // AC6.3: PDS-side rejection of the proxy attempt. 1217 + let diag = CreateReportDiagnostic::PdsProxiedRejected { 1218 + origin: ResponseOrigin::Pds, 1219 + status: resp.status.as_u16(), 1220 + source_code, 1221 + span, 1222 + }; 1214 1223 results.push(CheckResult { 1215 1224 diagnostic: Some(Box::new(diag)), 1216 1225 ..Check::PdsProxiedAccepted.network_error(format!( ··· 1512 1521 span: SourceSpan, 1513 1522 }, 1514 1523 1515 - /// Diagnostic for AC5.2: labeler rejected PDS-minted service-auth JWT. 1516 - #[error("Labeler rejected PDS-minted service-auth JWT (status {status})")] 1524 + /// Diagnostic for AC5.2 / AC5.3: the PDS-mediated service-auth flow 1525 + /// produced a non-2xx response. `origin` identifies whether the 1526 + /// rejection came from the labeler (AC5.2 spec violation) or the 1527 + /// user's PDS during `getServiceAuth` (AC5.3 network error). 1528 + #[error("{origin} rejected PDS-minted service-auth createReport (status {status})")] 1517 1529 #[diagnostic( 1518 1530 code = "labeler::report::pds_service_auth_rejected", 1519 - help = "The PDS issued a service-auth JWT for this user bound to the labeler's DID and the createReport NSID; the labeler should have accepted it." 1531 + help = "When `origin` is `Labeler`, the PDS issued a service-auth JWT bound to the labeler's DID and the createReport NSID; the labeler should have accepted it. When `origin` is `PDS`, the user's PDS refused to mint the service-auth JWT — verify the handle and app password, and confirm `--handle` resolves to a PDS that can mint service-auth tokens for this user." 1520 1532 )] 1521 1533 PdsServiceAuthRejected { 1534 + /// Which party produced the non-2xx response. 1535 + origin: ResponseOrigin, 1522 1536 /// Observed HTTP status code. 1523 1537 status: u16, 1524 1538 /// Response body for context. ··· 1529 1543 span: SourceSpan, 1530 1544 }, 1531 1545 1532 - /// Diagnostic for AC6.2: labeler rejected PDS-proxied createReport. 1533 - #[error("Labeler rejected PDS-proxied createReport (status {status})")] 1546 + /// Diagnostic for AC6.2 / AC6.3: the PDS-proxied `createReport` flow 1547 + /// produced a non-2xx response. `origin` identifies whether the 1548 + /// rejection came from the labeler via upstream envelope (AC6.2 spec 1549 + /// violation) or the user's PDS refusing the proxy attempt before 1550 + /// forwarding (AC6.3 network error). 1551 + #[error("{origin} rejected PDS-proxied createReport (status {status})")] 1534 1552 #[diagnostic( 1535 1553 code = "labeler::report::pds_proxied_rejected", 1536 - help = "The PDS forwarded the createReport call on the user's behalf; the downstream labeler reached it but rejected the submission." 1554 + help = "When `origin` is `Labeler`, the PDS forwarded the createReport call on the user's behalf; the downstream labeler reached it but rejected the submission. When `origin` is `PDS`, the user's PDS rejected the proxied call before it could reach the labeler — verify the handle, app password, and that the PDS is configured to proxy moderation calls to the target labeler." 1537 1555 )] 1538 1556 PdsProxiedRejected { 1557 + /// Which party produced the non-2xx response. 1558 + origin: ResponseOrigin, 1539 1559 /// Observed HTTP status code. 1540 1560 status: u16, 1541 1561 /// Response body for context. ··· 1545 1565 #[label("rejected here")] 1546 1566 span: SourceSpan, 1547 1567 }, 1568 + } 1569 + 1570 + /// Identifies which party in the PDS-mediated flow produced a non-2xx 1571 + /// response. Used to discriminate labeler-side spec violations from 1572 + /// PDS-side network errors within a single diagnostic variant, keeping 1573 + /// the one-diagnostic-per-check shape the report stage documents. 1574 + #[derive(Debug, Clone, Copy, PartialEq, Eq)] 1575 + pub enum ResponseOrigin { 1576 + /// The labeler itself produced the response (either directly, or via 1577 + /// the PDS surfacing an upstream-labeler error envelope). 1578 + Labeler, 1579 + /// The user's PDS produced the response without the labeler being 1580 + /// reached (e.g., `getServiceAuth` refused, or proxy rejected before 1581 + /// forwarding). 1582 + Pds, 1583 + } 1584 + 1585 + impl std::fmt::Display for ResponseOrigin { 1586 + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 1587 + match self { 1588 + ResponseOrigin::Labeler => f.write_str("Labeler"), 1589 + ResponseOrigin::Pds => f.write_str("PDS"), 1590 + } 1591 + } 1548 1592 } 1549 1593 1550 1594 /// Construct a `NamedSource` and a span covering the whole body.