Skip to content

v1.7.0 — PDS-side enforcement bridge & inbound XRPC gateway

Latest

Choose a tag to compare

@skydeval skydeval released this 30 Apr 06:31
· 3 commits to main since this release
v1.7.0
6ccec83

[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_gateway accepts proxied tools.ozone.moderation.* calls
and PDS-forwarded com.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; the PdsAdminBackend trait
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] PdsAdminBackend trait + BackendError + BackendActionId + Subject types (#84)
  • [pds_admin] pds_admin_audit table + unified hash chain spanning audit_log (#85)
  • [pds_admin] OzoneBackend skeleton: ctor + Basic-auth + xrpc-url helpers (#86)
  • [pds_admin] OzoneBackend::takedown_account body via com.atproto.admin.updateSubjectStatus + recordAction dispatch (#87)
  • [audit] cairn audit verify extended to walk the unified chain across audit_log and pds_admin_audit (#88)
  • [pds_admin] OzoneBackend::suspend_account + restore_account bodies; 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_pdses membership tables + middleware composition + CLI subcommands; audit-verify extended to walk 4 tables (#94)
  • [xrpc_gateway] tools.ozone.moderation.emitEvent handler body: dispatches modEventLabel / modEventTakedown / modEventReverseTakedown / modEventComment into the canonical record_action / revoke_action pipeline (§A14). Enforces createdBy == claims.iss per §A8.1 defense-in-depth. Reserved xrpc-gateway-default reason code documented for events without natural reason_codes (operators must declare in [moderation_reasons]). Unsupported $type values (Ozone has many beyond cairn-mod's four) return 400 InvalidRequest naming the unsupported type. (#95)
  • [xrpc_gateway] com.atproto.moderation.createReport handler body: PDS-signed inbound flow (§A10). Upstream PDSes forward user-filed reports; cairn-mod inserts into the existing reports table for the §F11/§F12/§F17 resolution surface to handle unchanged. The lexicon's reasonType is stored verbatim (cairn-mod's reports.reason_type already uses lexicon strings — no translation table). The user identity comes from the body's reportedBy field (the PDS asserts it on behalf of the user); operators express trust in upstream PDSes by adding them to xrpc_trusted_pdses. Threat-model §4 entry 9 documents the transitive-trust expansion. (#96)
  • [xrpc_gateway] tools.ozone.moderation.queryStatuses handler body: paginated read endpoint that folds cairn-mod's action history (subject_actions + labels + reports) into Ozone's subjectStatusView shape. New handlers/projections/ submodule houses the field-by-field translation (the design-heavy piece) — pure functions, fully unit-tested. v1.7 supports subject / limit / cursor / sortDirection / takendown / tags / appealed filters; unsupported filters return 400 InvalidRequest naming the field rather than silently ignoring. appealed=true returns empty (cairn-mod has no appeal flow); reviewState is constant #reviewClosed. Cursor: base64url(JSON) of (updated_at_ms, subject_did, subject_uri) lex-comparable with the page query's sort key. XrpcGatewayState extended with service_did for the labels.src filter. (#97)
  • [xrpc_gateway] tools.ozone.moderation.queryEvents handler body: paginated read endpoint that projects cairn-mod's audit_log (joined with subject_actions) into Ozone's modEventView shape. cairn-mod-internal audit entries are filtered out — the Ozone surface only sees subject_action_recorded / subject_action_revoked projected to modEventLabel / modEventTakedown / modEventComment / modEventReverseTakedown; pending_*, report_resolved, reporter_*, retention_sweep, service_record_*, label_applied / label_negated are 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 → 400 InvalidRequest. Cursor: base64url(JSON) of a single audit_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_only test fixture: post-#98, every handler requires Extension<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 via spawn_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].enabled and surfaces the pds_admin_audit outcome in the response. Reserved reason code pds-admin-cli for manual escalations (operators must declare in [moderation_reasons]). (#99)
  • [cli] cairn moderator add --with-xrpc-callers: convenience flag that adds the moderator DID to xrpc_known_callers in the same invocation, with --by recording the operator running the command. Idempotent at the application layer (pre-checks is_known_caller to skip the duplicate add). Plain cairn moderator add is unchanged. (#99)
  • [cli] cairn moderator events: operator-tier audit-events view that mirrors tools.ozone.moderation.queryEvents (#98) but exposes the FULL cairn-mod audit_log vocabulary (including pending_*, 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-only applies #98's filter-out policy; output is identical to what queryEvents would return for the same filters. Reuses the audit_event::project_audit_event projection 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), inbound xrpc_gateway (NSID allowlist, 501/405 envelopes), XrpcAuthService (verification rules + replay cache), trust tables (xrpc_known_callers vs xrpc_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, seeding xrpc_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

  • rustfmt drift in admin_subject_actions.rs (7a7628f).
  • DEFAULT_POLICY_REASON_CODE renamed from policy_threshold to
    policy-threshold so 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 to repeated-violation so the documented operator
    example produces a valid reason identifier (78edd9b).
  • getSubjectHistory wire shape was missing actorKind and
    triggeredByPolicyRule fields. 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 returned null for 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.rs and src/xrpc_gateway/router.rs
    registered POST /xrpc/com.atproto.moderation.createReport, and
    axum::Router::merge panicked on the duplicate route at
    serve.rs:257. Fix: gateway router no longer mounts createReport;
    the user-direct create_report_router is the single mount point
    and dispatches to the gateway path's logic when the JWT issuer is
    in xrpc_trusted_pdses (PDS-forwarded reports skip pre-gates +
    take reportedBy from 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 = false is unchanged. (#102; caught
    during Phase B verification)

Removed

Security