Repository navigation
[1.7.0] - 2026-04-30
v1.7 "PDS-side enforcement bridge & inbound XRPC gateway" closes
the loop between cairn-mod and the operator's PDS in two halves.
Outbound: a[pds_admin]config block declares operator-trusted
PDS credentials and an action-type-to-backend-method mapping;
cairn-mod's existing recordAction pipeline now propagates
account-state changes (takedown / temp suspension / restore) to
the configured PDS in lockstep with label emission. Inbound: a new
xrpc_gatewayaccepts proxiedtools.ozone.moderation.*calls
and PDS-forwardedcom.atproto.moderation.createReport— making
cairn-mod a usable Ozone replacement for operators on bsky-PDS.
Both halves preserve cairn-mod's audit-chain discipline (every
backend call hash-chains into the existing audit log; every
inbound mutation lands via the canonical recordAction path).
v1.7 ships bsky-PDS support; thePdsAdminBackendtrait
abstraction accommodates v1.8's Aurora-Locus backend without API
changes. Disabled by default — operators upgrading from v1.6 see
no behavior change unless they opt in.
Added
Changed
- [pds_admin] config block: parsing, validation, schema (#83)
- [pds_admin]
PdsAdminBackendtrait +BackendError+BackendActionId+Subjecttypes (#84) - [pds_admin]
pds_admin_audittable + unified hash chain spanningaudit_log(#85) - [pds_admin]
OzoneBackendskeleton: ctor + Basic-auth + xrpc-url helpers (#86) - [pds_admin]
OzoneBackend::takedown_accountbody viacom.atproto.admin.updateSubjectStatus+ recordAction dispatch (#87) - [audit]
cairn audit verifyextended to walk the unified chain acrossaudit_logandpds_admin_audit(#88) - [pds_admin]
OzoneBackend::suspend_account+restore_accountbodies; ISO-duration → days plumbing; revoke-action dispatch path (#89) - [pds_admin]
probe()trait method +OzoneBackend::probe(describeServer) + serve.rs startup wiring (#90) - [xrpc_gateway]
[xrpc_gateway]config block + module skeleton + 501 catch-all router (#91) - [xrpc_gateway] NSID allowlist enum + per-handler dispatch stubs + XRPC-shape 405 envelope (#92)
- [xrpc_gateway]
XrpcAuthService+ tower middleware: ATProto service-auth JWT verification (ES256K, claim validation; replay deferred to #94) (#93) - [xrpc_gateway] Replay cache +
xrpc_known_callers/xrpc_trusted_pdsesmembership tables + middleware composition + CLI subcommands; audit-verify extended to walk 4 tables (#94) - [xrpc_gateway]
tools.ozone.moderation.emitEventhandler body: dispatchesmodEventLabel/modEventTakedown/modEventReverseTakedown/modEventCommentinto the canonicalrecord_action/revoke_actionpipeline (§A14). EnforcescreatedBy == claims.issper §A8.1 defense-in-depth. Reservedxrpc-gateway-defaultreason code documented for events without naturalreason_codes(operators must declare in[moderation_reasons]). Unsupported$typevalues (Ozone has many beyond cairn-mod's four) return 400InvalidRequestnaming the unsupported type. (#95) - [xrpc_gateway]
com.atproto.moderation.createReporthandler body: PDS-signed inbound flow (§A10). Upstream PDSes forward user-filed reports; cairn-mod inserts into the existingreportstable for the §F11/§F12/§F17 resolution surface to handle unchanged. The lexicon'sreasonTypeis stored verbatim (cairn-mod'sreports.reason_typealready uses lexicon strings — no translation table). The user identity comes from the body'sreportedByfield (the PDS asserts it on behalf of the user); operators express trust in upstream PDSes by adding them toxrpc_trusted_pdses. Threat-model §4 entry 9 documents the transitive-trust expansion. (#96) - [xrpc_gateway]
tools.ozone.moderation.queryStatuseshandler body: paginated read endpoint that folds cairn-mod's action history (subject_actions+labels+reports) into Ozone'ssubjectStatusViewshape. Newhandlers/projections/submodule houses the field-by-field translation (the design-heavy piece) — pure functions, fully unit-tested. v1.7 supportssubject/limit/cursor/sortDirection/takendown/tags/appealedfilters; unsupported filters return 400InvalidRequestnaming the field rather than silently ignoring.appealed=truereturns empty (cairn-mod has no appeal flow);reviewStateis constant#reviewClosed. Cursor: base64url(JSON) of(updated_at_ms, subject_did, subject_uri)lex-comparable with the page query's sort key.XrpcGatewayStateextended withservice_didfor thelabels.srcfilter. (#97) - [xrpc_gateway]
tools.ozone.moderation.queryEventshandler body: paginated read endpoint that projects cairn-mod'saudit_log(joined withsubject_actions) into Ozone'smodEventViewshape. cairn-mod-internal audit entries are filtered out — the Ozone surface only seessubject_action_recorded/subject_action_revokedprojected tomodEventLabel/modEventTakedown/modEventComment/modEventReverseTakedown;pending_*,report_resolved,reporter_*,retention_sweep,service_record_*,label_applied/label_negatedare operator-tier and surface only via the CLI +cairn audit verify. Revocations of warnings/notes are also filtered (Ozone has no "reverse comment" event). v1.7 supported filters:subject/types/createdBy/sortDirection/createdAfter/createdBefore/limit/cursor/includeAllUserRecords; others → 400InvalidRequest. Cursor: base64url(JSON) of a singleaudit_log.id(simpler than #97's tuple cursor since the column is monotonic AUTOINCREMENT). Closes Phase D's inbound NSID surface for v1.7. (#98) - [xrpc_gateway] Retired the
build_routes_onlytest fixture: post-#98, every handler requiresExtension<XrpcGatewayState>, so the no-middleware variant is no longer testable in isolation. The router's structural shape tests (unknown-NSID fallback, case mismatch, wrong-method 405, two-field envelope) are migrated to the layered router viaspawn_authed. (#98) - [cli]
cairn pds-admin {takedown,suspend,restore}: manual escape hatch for the PDS-admin bridge (#87 / §F23 / §A13). HTTP-routed via the canonical recordAction / revokeAction admin XRPC; the writer's post-commit dispatch fires the configured backend automatically. Does not bypass strike accounting. Pre-flights[pds_admin].enabledand surfaces thepds_admin_auditoutcome in the response. Reserved reason codepds-admin-clifor manual escalations (operators must declare in[moderation_reasons]). (#99) - [cli]
cairn moderator add --with-xrpc-callers: convenience flag that adds the moderator DID toxrpc_known_callersin the same invocation, with--byrecording the operator running the command. Idempotent at the application layer (pre-checksis_known_callerto skip the duplicate add). Plaincairn moderator addis unchanged. (#99) - [cli]
cairn moderator events: operator-tier audit-events view that mirrorstools.ozone.moderation.queryEvents(#98) but exposes the FULL cairn-mod audit_log vocabulary (includingpending_*,retention_sweep,xrpc_*collaboration events,report_resolved, etc.). Default mode renders Ozone-eligible rows in their projected modEventView shape and cairn-mod-internal rows in a generic shape, intermixed in chronological order.--ozone-onlyapplies #98's filter-out policy; output is identical to what queryEvents would return for the same filters. Reuses theaudit_event::project_audit_eventprojection from #98. Direct-DB; supports--subject/--actor/--type/--from/--to/--limit/--cursor. (#99) - [docs] §F23 design-doc chapter — operator-facing reference for v1.7's PDS-side enforcement bridge and inbound XRPC gateway. 12 numbered subsections covering compatibility framing, outbound
pds_admin(trait surface, OzoneBackend, audit-chain integration, startup probe), inboundxrpc_gateway(NSID allowlist, 501/405 envelopes),XrpcAuthService(verification rules + replay cache), trust tables (xrpc_known_callersvsxrpc_trusted_pdses+ threat-model §4.9 cross-ref), inbound action integration + projection policy (subjectStatusView + modEventView field-by-field tables, filter-out policy, lexicon non-conformance notes), operator config blocks, operator-tier CLI surface (the five v1.7 commands), reserved reason codes (policy-threshold/xrpc-gateway-default/pds-admin-cli), operator-facing invariants (audit chain ordering, strict-monotonic timestamps, suspension duration encoding, strongRef.cid omission, replay cache scope, queryEvents filter-out), patterns established for v1.8+, and cross-references. (#100) - [docs] §18 roadmap update + §19.5 v1.7 deployment runbook. §18 collapses the v1.7+ foreshadowing bullet to a one-line shipped pointer to §F23 and refreshes the v1.x trajectory along two axes (backend coverage / Ozone parity floor); v1.8 = LocusBackend + retry policy + gateway refinements; v1.9 = review queue + extended event types + source management; v2.0 = web UI; future cycle = XRPC management of collaboration tables,
tools.ozone.communication.*/tools.ozone.team.*, action-time CIDs; enterprise-tier = multi-instance replay coordination + Postgres + multi-node. New §19.5 walks operators through enabling[pds_admin](action_map decision, env var, reserved reason code, restart, probe verification, manual-takedown verification), enabling[xrpc_gateway](service DID publication, bsky-PDS env var coordination, reserved reason code, seedingxrpc_known_callers+xrpc_trusted_pdses, probe call), the verification dance (cairn audit verify,cairn moderator events --ozone-only, end-to-end test), and the rollback path (disable + restart; rows preserved for re-enable). v1.7 doc-complete; release ceremony per §19.2 happens outside chainlink scope. (#101)
Fixed
rustfmtdrift inadmin_subject_actions.rs(7a7628f).DEFAULT_POLICY_REASON_CODErenamed frompolicy_thresholdto
policy-thresholdso the default substitution path produces a
valid reason identifier under the[a-z0-9-]reason-id validator
(b51a940; caught during Phase B verification).- §F22.1 TOML example used
repeated_violation(underscore);
renamed torepeated-violationso the documented operator
example produces a valid reason identifier (78edd9b). getSubjectHistorywire shape was missingactorKindand
triggeredByPolicyRulefields. v1.6 added these columns to
subject_actions(writer persists correctly per #73), but the
read-side projection, SELECT, lexicon def, and CLI formatter were
never extended — so the API returnednullfor both, defeating
forensic traceability of policy-recorded vs moderator-recorded
actions. Fixed across all four layers +cairn moderator history
tabular output gains an ACTOR column (a1c71cb; caught during Phase
B verification).- Startup panic on
[xrpc_gateway].enabled = true: both
src/server/create_report.rsandsrc/xrpc_gateway/router.rs
registeredPOST /xrpc/com.atproto.moderation.createReport, and
axum::Router::mergepanicked on the duplicate route at
serve.rs:257. Fix: gateway router no longer mounts createReport;
the user-directcreate_report_routeris the single mount point
and dispatches to the gateway path's logic when the JWT issuer is
inxrpc_trusted_pdses(PDS-forwarded reports skip pre-gates +
takereportedByfrom the body); other reports continue through
the user-direct path with pre-gates intact. §F23.5 + §19.5.3
updated to reflect the dispatch-not-mount architecture. v1.6 → v1.7
with[xrpc_gateway].enabled = falseis unchanged. (#102; caught
during Phase B verification)