From 42149d8e0232ea4d4fded67b8de6c5be06d651e4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ullrich=20Sch=C3=A4fer?= Date: Mon, 13 Jul 2026 23:54:56 +0200 Subject: [PATCH] chore(openspec): archive federation-hardening MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit All 13 tasks complete (implementation shipped in #570–573; staging verification in #574). Archive via `openspec archive` (CLI 1.6.0): - Creates openspec/specs/federation-operations/spec.md — the new capability: durable federation queue, inbound replay defense, instance blocklist, published protocol doc, delivery observability (Purpose filled in; CLI leaves a TBD placeholder). - Applies the MODIFIED requirement to openspec/specs/social-federation: "Push delivery on local activity create" now guarantees persistent queueing + a "Fan-out survives a deploy" scenario. - Moves the change to openspec/changes/archive/2026-07-13-federation-hardening/. Both specs pass `openspec validate --strict`. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../.openspec.yaml | 0 .../design.md | 0 .../proposal.md | 0 .../specs/federation-operations/spec.md | 0 .../specs/social-federation/spec.md | 0 .../2026-07-13-federation-hardening}/tasks.md | 0 openspec/specs/federation-operations/spec.md | 45 +++++++++++++++++++ openspec/specs/social-federation/spec.md | 8 +++- 8 files changed, 51 insertions(+), 2 deletions(-) rename openspec/changes/{federation-hardening => archive/2026-07-13-federation-hardening}/.openspec.yaml (100%) rename openspec/changes/{federation-hardening => archive/2026-07-13-federation-hardening}/design.md (100%) rename openspec/changes/{federation-hardening => archive/2026-07-13-federation-hardening}/proposal.md (100%) rename openspec/changes/{federation-hardening => archive/2026-07-13-federation-hardening}/specs/federation-operations/spec.md (100%) rename openspec/changes/{federation-hardening => archive/2026-07-13-federation-hardening}/specs/social-federation/spec.md (100%) rename openspec/changes/{federation-hardening => archive/2026-07-13-federation-hardening}/tasks.md (100%) create mode 100644 openspec/specs/federation-operations/spec.md diff --git a/openspec/changes/federation-hardening/.openspec.yaml b/openspec/changes/archive/2026-07-13-federation-hardening/.openspec.yaml similarity index 100% rename from openspec/changes/federation-hardening/.openspec.yaml rename to openspec/changes/archive/2026-07-13-federation-hardening/.openspec.yaml diff --git a/openspec/changes/federation-hardening/design.md b/openspec/changes/archive/2026-07-13-federation-hardening/design.md similarity index 100% rename from openspec/changes/federation-hardening/design.md rename to openspec/changes/archive/2026-07-13-federation-hardening/design.md diff --git a/openspec/changes/federation-hardening/proposal.md b/openspec/changes/archive/2026-07-13-federation-hardening/proposal.md similarity index 100% rename from openspec/changes/federation-hardening/proposal.md rename to openspec/changes/archive/2026-07-13-federation-hardening/proposal.md diff --git a/openspec/changes/federation-hardening/specs/federation-operations/spec.md b/openspec/changes/archive/2026-07-13-federation-hardening/specs/federation-operations/spec.md similarity index 100% rename from openspec/changes/federation-hardening/specs/federation-operations/spec.md rename to openspec/changes/archive/2026-07-13-federation-hardening/specs/federation-operations/spec.md diff --git a/openspec/changes/federation-hardening/specs/social-federation/spec.md b/openspec/changes/archive/2026-07-13-federation-hardening/specs/social-federation/spec.md similarity index 100% rename from openspec/changes/federation-hardening/specs/social-federation/spec.md rename to openspec/changes/archive/2026-07-13-federation-hardening/specs/social-federation/spec.md diff --git a/openspec/changes/federation-hardening/tasks.md b/openspec/changes/archive/2026-07-13-federation-hardening/tasks.md similarity index 100% rename from openspec/changes/federation-hardening/tasks.md rename to openspec/changes/archive/2026-07-13-federation-hardening/tasks.md diff --git a/openspec/specs/federation-operations/spec.md b/openspec/specs/federation-operations/spec.md new file mode 100644 index 0000000..1ab9f85 --- /dev/null +++ b/openspec/specs/federation-operations/spec.md @@ -0,0 +1,45 @@ +# federation-operations Specification + +## Purpose +Operational hardening of the Journal's ActivityPub federation: a durable (Postgres-backed) message queue so deliveries and retries survive restarts, inbound replay defense, a domain-keyed instance blocklist enforced at inbox/delivery/outbox boundaries, published protocol documentation (`FEDERATION.md`), and delivery observability metrics. + +## Requirements +### Requirement: Durable federation queue +Fedify's message queue SHALL be backed by PostgreSQL so that queued activities, inbox processing tasks, and retry state survive process restarts and deploys. + +#### Scenario: Delivery survives a restart +- **WHEN** deliveries are queued and the journal container restarts before they complete +- **THEN** the deliveries execute after restart without loss + +#### Scenario: Retry state persists +- **WHEN** a delivery has failed twice with backoff pending and the process restarts +- **THEN** the retry continues on schedule rather than starting over or disappearing + +### Requirement: Inbound replay defense +The Journal SHALL record processed inbound activity IRIs (retained at least 30 days) and SHALL drop duplicate activities idempotently before any side effects. + +#### Scenario: Replayed activity is a no-op +- **WHEN** the same signed Like activity is delivered twice +- **THEN** the second delivery produces no state change and is counted as a dropped duplicate + +### Requirement: Instance blocklist +The Journal SHALL support blocking federation instances by domain, enforced at inbox processing (activities dropped without an error oracle), delivery enqueue (recipients filtered), and outbox polling/actor fetch (refused). Blocking SHALL be manageable via a documented operator procedure. + +#### Scenario: Blocked instance is inert in both directions +- **WHEN** a domain is added to the blocklist +- **THEN** its activities are silently dropped, no deliveries are sent to it, and its actors/outboxes are not fetched + +### Requirement: Published federation protocol documentation +The repository SHALL contain a `FEDERATION.md` documenting actor discovery, object and activity types with JSON examples, addressing rules, signature and deduplication expectations, delivery retry policy, and moderation semantics, kept current as federation capabilities change. + +#### Scenario: Third-party implementer can interoperate +- **WHEN** another project implements against `FEDERATION.md` +- **THEN** the documented flows (follow, create fan-out, dedup) behave as described + +### Requirement: Federation delivery observability +The Journal SHALL expose metrics for delivery outcomes, queue depth, and inbox drops (duplicate/blocked), surfaced on the monitoring dashboard. + +#### Scenario: Operator sees delivery health +- **WHEN** deliveries to an unreachable instance keep failing +- **THEN** failure counts and queue depth are visible on the dashboard + diff --git a/openspec/specs/social-federation/spec.md b/openspec/specs/social-federation/spec.md index 926cf4a..4813351 100644 --- a/openspec/specs/social-federation/spec.md +++ b/openspec/specs/social-federation/spec.md @@ -2,7 +2,6 @@ ## Purpose ActivityPub federation for the Journal, scoped to the follow graph between trails.cool instances: per-user actor objects with WebFinger discovery, per-user signing keypairs, a narrow follow-only inbox, an outbox of public activities, push delivery to remote followers on local activity create, outbound follows restricted to other trails instances, outbox-poll ingestion of remote trails activities, and audience-aware feed filtering. Local follow lifecycle and the feed surface itself live in `social-follows`; profile pages live in `public-profiles`. - ## Requirements ### Requirement: Per-user actor objects with WebFinger discovery The Journal SHALL serve an ActivityPub `Person` actor object at the user's canonical URL for any user with `profile_visibility = 'public'`, and SHALL serve a WebFinger endpoint at `/.well-known/webfinger` resolving `acct:user@domain` to that actor IRI. @@ -61,7 +60,7 @@ The Journal SHALL serve a paginated outbox at `https://{DOMAIN}/users/:username/ - **THEN** the response includes only activities with `visibility = 'public'`; `unlisted` and `private` activities never appear ### Requirement: Push delivery on local activity create -The Journal SHALL deliver a `Create(Note)` activity to every accepted remote follower's inbox when a local user with `profile_visibility = 'public'` creates a new `public` activity. +The Journal SHALL deliver a `Create(Note)` activity to every accepted remote follower's inbox when a local user with `profile_visibility = 'public'` creates a new `public` activity. Delivery queueing and retry state SHALL be persistent: queued deliveries and scheduled retries survive process restarts. #### Scenario: New public activity fans out - **WHEN** a local user with N accepted remote followers creates a new public activity @@ -71,6 +70,10 @@ The Journal SHALL deliver a `Create(Note)` activity to every accepted remote fol - **WHEN** multiple deliveries target the same remote host - **THEN** they are rate-limited so we never exceed 1 request per second per remote host (configurable; chosen for safety, not throughput) +#### Scenario: Fan-out survives a deploy +- **WHEN** a deploy restarts the journal while fan-out deliveries are queued or awaiting retry +- **THEN** the deliveries complete after the restart without loss + ### Requirement: Outbound follows restricted to other trails instances The Journal SHALL accept outbound follow requests against remote actor IRIs only when the target host self-identifies as a trails.cool instance. Follows targeting Mastodon, Pleroma, or other non-trails ActivityPub servers SHALL be refused at the API layer with a clear error and a link to the documented v1 limitation. @@ -107,3 +110,4 @@ Activities cached from remote trails actors SHALL be tagged with their audience #### Scenario: Followers-only remote content reaches only the right viewer - **WHEN** a remote actor publishes a followers-only activity, two local users A and B both have rows in the activity cache for that actor, but only A holds an accepted follow against the actor - **THEN** A sees the activity in `/feed` and B does not, even though the row exists in our database +