# fed-sx Milestone 2 — Multi-actor + Federation

Real federation between two fed-sx instances. Per-actor state, signed
inbox delivery, Follow lifecycle, audience-resolving outbound queue, and
the rich verbs (Note, Announce, Endorse) needed for federated propagation.
Reference: `plans/fed-sx-design.md` (especially §9 identity, §13 federation,
§16 HTTP endpoints). Builds on Milestone 1 (see `plans/fed-sx-milestone-1.md`).

## Goal

Two cooperating fed-sx instances `A` and `B`, each hosting one or more
actors, can:

1. **Discover** each other's actors via webfinger + actor docs.
2. **Follow** across instances (`Follow` → `Accept` → state).
3. **Publish** a `Note` on `B` and have it land in every follower's
   `actor-state` projection on `A` via signed inbox delivery.
4. **Announce** a peer's activity, propagating it to followers of the
   announcer.
5. **Rotate keys** on either side without breaking historical sig
   verification (per §9.6).

Acceptance: the §11 smoke test (`smoke_federate.sh`) drives all of the
above against two locally-running kernel instances on distinct ports, no
human-in-the-loop, and exits 0.

## Non-goals (what milestone 2 deliberately does NOT do)

- **Real WAN federation.** Both instances run on `localhost:PortA` and
  `localhost:PortB`. Cross-instance HTTP is unencrypted plaintext.
  TLS, NAT traversal, and signed HTTP-message headers (per RFC 9421)
  are v3.
- **ActivityPub Mastodon interop.** No HTTP-signatures-2018 compat layer,
  no Linked-Data-Signatures, no JSON-LD canonicalisation. Cross-fed-sx
  only.
- **IPFS / S3 storage backends.** Still local files only.
- **Browser client + operator dashboard.** Curl-shaped API only.
- **Capability tokens / delegation.** Multi-actor means multi-user, not
  multi-device for a single actor. Capability tokens (per §9.5) defer.
- **Cross-host conformance.** Only OCaml/Erlang-on-SX host runs fed-sx
  in v2.
- **Performance work.** Functional correctness first.
- **Spam/abuse infrastructure.** Per §13.6 the layers are designed; v2
  implements signature verification + replay defense; reputation,
  rate-limiting, instance allowlists / blocklists are v3.
- **Operator quarantine UX.** Logs only.

## Architecture summary

```
                   Instance A                     Instance B
                   (port 9999)                    (port 9998)

  Outbox     ┌─────────────────┐               ┌─────────────────┐
  ────────▶ │ HTTP server      │               │ HTTP server      │
            │  POST /activity  │               │  POST /activity  │
            │  POST /inbox     │               │  POST /inbox     │
            │  GET  /actors/.. │               │  GET  /actors/.. │
            │  GET  /.well-    │               │  GET  /.well-    │
            │       known/*    │               │       known/*    │
            └────────┬─────────┘               └────────┬─────────┘
                     │                                  │
            ┌────────▼─────────┐               ┌────────▼─────────┐
            │ nx_kernel        │ ◀ HTTPS ▶    │ nx_kernel        │
            │ multi-actor      │   deliveries  │ multi-actor      │
            │  bucket map      │   (signed)    │  bucket map      │
            │   ActorA -> {…}  │               │   ActorB -> {…}  │
            │   ActorC -> {…}  │               │                  │
            └────────┬─────────┘               └────────┬─────────┘
                     │                                  │
            ┌────────▼─────────┐               ┌────────▼─────────┐
            │ Delivery queue   │               │ Delivery queue   │
            │ (one worker per  │               │ (one worker per  │
            │  peer instance)  │               │  peer instance)  │
            └──────────────────┘               └──────────────────┘
                     │
                     │ HTTP POST /inbox to peer
                     ▼
              (peer instance)
```

The federation transport is plain HTTP POST of canonical-bytes-signed
activities to each follower's actor inbox. Delivery is push (§13.1); pull
+ relay deferred to v3.

## Build order

Twelve steps in dependency order.

| Step | Title                                              | Depends on            |
|------|----------------------------------------------------|-----------------------|
| **1** | Per-actor state buckets in nx_kernel              | M1 closeout           |
| **2** | Actor lifecycle activities (Person/Service/Group) | Step 1                |
| **3** | Key rotation via Update + actor-state projection  | Steps 2, M1 §9.6      |
| **4** | Multi-actor HTTP routing (per-actor outbox/inbox) | Steps 1, M1 8b-start  |
| **5** | POST /inbox: peer signature verify + ingestion    | Steps 3, 4            |
| **6** | Follow lifecycle (Follow / Accept / Reject / Undo) | Step 5                |
| **7** | Audience-resolving delivery set computation       | Step 6                |
| **8** | Outbound delivery queue + retry/backoff           | Step 7                |
| **9** | Backfill modes on Follow accept                   | Steps 6, 8            |
| **10** | Discovery: webfinger + actor doc fetch           | Step 4                |
| **11** | Rich verbs as runtime artifacts (Note, Announce, Endorse) | Step 8        |
| **12** | Two-instance smoke test (`smoke_federate.sh`)    | Steps 1-11            |

Steps 1-3 are the multi-actor foundation. Steps 4-10 are the federation
core. Steps 11-12 close the proof points.

---

## Step 1 — Per-actor state buckets

Today `nx_kernel` holds one actor's state at the top of its property list.
Make it bucketed by ActorId so a single kernel can host any number of
actors.

**Deliverables:**

- [x] **1a** — Pure-functional bucket APIs. State shape becomes
  `[{actors, [{ActorId, ActorBucket}, ...]}, {next_actor_seq, N}]`
  with `ActorBucket = [{key_spec, KS}, {actor_state, AS}, {log, L},
  {projections, [Name]}, {next_published, N}]`. New exports: `new/0`,
  `add_actor/4`, `has_actor/2`, `actors/1`, `actor_count/1`,
  `next_actor_seq/1`, `actor_bucket/2`, `publish/3`, per-actor
  accessors (`actor_log_state/2`, `actor_log_tip/2`, `actor_key_spec/2`,
  `actor_state/2`, `actor_projections/2`, `actor_next_published/2`),
  `with_actor_projections/3`. Legacy single-actor accessors
  (`actor_id/1`, `key_spec/1`, `actor_state/1`, `log_state/1`,
  `log_tip/1`, `projections/1`, `next_published/1`,
  `with_projections/2`, legacy `publish/2`) continue to read from the
  first bucket — every M1 test passes via `bootstrap:start/3` →
  `new/3` → first-bucket lookup. `lists:keymember`/`keyfind` not in
  the substrate; local `has_keyed`/`find_keyed`/`set_keyed`/
  `set_bucket` helpers handle the keyed-list ops.
  `next/tests/nx_kernel_multi.sh` 17/17.
- [x] **1b** — Multi-actor gen_server. `start_link/3` still seeds
  bucket 0; new exports `add_actor/3`, `publish_to/2(ActorId,
  Request)`, `log_tip_for/1`, `actors/0`, `state_for/1`,
  `bucket_for/1`, `with_projections_for/2` delegate to the pure-
  functional bucket APIs via fresh `handle_call` branches. Existing
  `publish/1`/`log_tip/0`/`with_projections/1` route through bucket
  0 unchanged. Per-actor mailbox concurrency (one gen_server per
  bucket so distinct-actor publishes don't serialise) is forward-
  looking — deferred to Step 4 (multi-actor HTTP routing) where it
  actually pays off. `nx_kernel_multi.sh` extended with 9 gen_server
  cases (26 total), every M1 nx_kernel-adjacent + http suite still
  green (134 / 134 across 12 suites).

**Acceptance:** `bash next/tests/nx_kernel_multi.sh` passes 12+ cases.

---

## Step 2 — Actor lifecycle activities

Per design §9.1, an actor is a Person, Service, or Group object,
created by `Create{Person{...}}`. The kernel needs to fold this into
an actor-state projection that downstream code can read for keys,
publicKey rotation history, profile fields, follower counts, etc.

**Deliverables:**

- [x] **2a** — Genesis additions: `DefineObject{Person}` /
  `DefineObject{Service}` / `DefineObject{Group}` — three new SX
  files in `next/genesis/object-types/` plus manifest entries (now
  13 object-types total, 34 total genesis entries). Each defines
  `:name`, `:doc`, `:schema (fn (obj) (string? (-> obj :name)))`.
  `next/tests/genesis_parse.sh` extended +7 cases (head form +
  :name + manifest membership), now 57/57. Bootstrap suite
  count assertions bumped (`bootstrap_read.sh` 15/15,
  `bootstrap_load.sh` 15/15, `bootstrap_populate.sh` 14/14,
  `bootstrap_start.sh` 10/10). `bootstrap_build.sh` 12/12 picks
  up the new bundle CID dynamically.
- [x] **2b** — Actor-state projection fold (Erlang-fun stand-in,
  mirrors Step 5d-pure's `define_registry`). `next/kernel/actor_state.erl`
  with state shape `[{ActorId, Profile}, ...]` where `Profile` is a
  proplist with `:type / :name / :preferredUsername / :summary /
  :icon / :public_keys / :moved_to / :created`. Maps `#{}` aren't
  registered in the substrate, so the profile is a property list
  (same shape choice as the kernel's bucket / registry state).
  Folding rules:
  - `Create{Person|Service|Group}` (from a known `:actor`):
    captures profile fields + `:created` (=`:published` seq).
    Duplicate Creates are no-overwrite.
  - `Update{Person|Service|Group, patch}`: merges `:patch` into the
    profile last-write-wins per key.
  - `Move`: records `:moved_to` on the profile.
  Other activity types and non-actor object Creates pass through.
  `fold_fn/0` plugs into `projection:start_link/3`. Local
  `find_keyed/has_keyed/set_keyed` helpers (same gap as 1a — no
  `lists:keyfind`/`keymember` in the substrate). 19 cases in
  `actor_state_pure.sh`.
- [x] **2c** — `nx_kernel:bootstrap_actor/4(ActorId, Profile,
  KeySpec, State)` — adds an actor bucket and publishes
  `Create{Person|Service|Group}` as the bucket's first activity in
  one call. Profile carries `:type` (defaults to `person`), `:name`,
  `:preferredUsername`, `:summary`, `:icon`, `:public_keys`; the
  function builds the Create's `:object` from the profile and the
  kernel-side AS from `:public_keys`. gen_server variant
  `bootstrap_actor/3` for live-kernel use; integration test in
  `actor_lifecycle.sh` ties 2a artefacts, 2b projection, and 2c
  bootstrap together end-to-end (pure + gen_server + projection
  capture for all three actor types). 15/15.

**Acceptance:** `bash next/tests/actor_lifecycle.sh` passes 10+ cases.

---

## Step 3 — Key rotation via Update + actor-state

Per §9.2: rotation is itself an activity. The actor-state projection
keeps the full key history (with `created` / `superseded_at`) so
`envelope:verify_signature/2` continues to find historical keys when
verifying activities published before the rotation.

**Deliverables:**

- [x] **3** — `actor_state.erl` `fold_update` now routes patches
  through `apply_patch/3`, which special-cases two rotation patch
  entries:
  - `{add_publicKey, KeyProplist}` appends the key to `:public_keys`,
    defaulting `:created` to the activity's `:published` if unset.
  - `{supersede, OldKeyId}` marks the matching key with
    `:superseded_at` = activity's `:published` (idempotent: existing
    `:superseded_at` is preserved; unknown ids are no-ops).
  Other patch entries fall through to last-write-wins per key
  (preserving Step 2b semantics; verified by extra
  `actor_state_pure.sh` cases).
  New exports `key_history/1` (full list incl. superseded entries),
  `active_keys_at/2` (subset active at time T, mirroring envelope's
  `is_active_at` semantics — envelope keeps its predicate private,
  so a local copy lives here), and `find_key_by_id/2`.
  Rotation-purpose schema gating per §9.6 ("rotation activity must
  itself be signed by an active key with `rotate-key` purpose") is
  deferred to Step 5 (peer-side `stage_signature` will plumb the
  purpose check through pipeline). 16 cases in `key_rotation.sh`
  cover rotation arithmetic, `key_history` preservation, and live
  `envelope:verify_signature/2` round-trips for pre / post / mid
  rotation activities — including the negative case (post-rotation
  K1-signed activity returns `{error, no_active_key}`).

**Acceptance:** `bash next/tests/key_rotation.sh` passes 12+ cases.

---

## Step 4 — Multi-actor HTTP routing

Per-actor URLs per design §16.1:

```
GET  /actors/<id>                  # actor doc
GET  /actors/<id>/outbox           # OrderedCollection
GET  /actors/<id>/outbox?page=N    # page
POST /actors/<id>/inbox            # peer delivery to this actor
GET  /actors/<id>/followers        # follower list
GET  /actors/<id>/following        # following list
POST /activity                     # authenticated publisher API (existing)
```

`POST /activity` still picks the publishing actor from the bearer
token; the token now maps to an `:actor_id` rather than a fixed `alice`.

**Deliverables:**

- [x] **4a** — Per-actor URL routing. New `split_first_slash/1`
  helper splits the `/actors/` suffix into `{Id, SubPath}`.
  GET dispatch routes `outbox` / `inbox` / `followers` / `following`
  sub-paths to four new content-negotiated response functions
  (`actor_outbox_response_for/2`, `actor_inbox_get_response_for/2`,
  `actor_followers_response_for/2`,
  `actor_following_response_for/2`) — text / json / activity_json /
  sx variants per existing format pattern. POST dispatch routes
  `inbox` to a 202 Accepted stub (`actor_inbox_post_response/0` +
  `accepted_response/1`). Unknown sub-paths under `/actors/<id>/`
  return 404. Bare `/actors/<id>` keeps the M1 `actor_doc_response_for`
  arm. 17 cases in `http_multi_actor.sh`.
- [x] **4b** — Token → ActorId map. New `resolve_token/2` reads
  `:tokens` from Cfg (proplist `[{Token, ActorId}, ...]`) and
  returns `{ok, ActorId}` on match. Falls back to the M1
  `:publish_token` single-token field on miss (returns
  `{ok, legacy}`, route through `nx_kernel:publish/1` to bucket 0
  unchanged). Cfg with both fields: `:tokens` wins for matched
  tokens; `:publish_token` only consulted on `:tokens` miss.
  `handle_post_activity` now threads the resolved `ActorRef` to
  `publish_if_kernel/3` which dispatches `publish_to/2` for
  explicit actor ids and `publish/1` for the `legacy` atom.
  No-kernel auth-only path unchanged. The dead M1
  `expected_token/1` helper is gone. 8 new cases in
  `http_multi_actor.sh` (25/25 total).
- [ ] **4c** — `http_server:route/3(Req, Cfg, Kernel)` — the Cfg
  carries opaque `:kernel` reference (or accepts the registered
  `nx_kernel` atom) so per-actor handlers can call
  `nx_kernel:state_for/1`, `actor_log_state/2`, projections etc.
- [ ] **4d** — Per-actor outbox listing reads from the named
  bucket's log entries via `nx_kernel:actor_log_state/2`, content-
  negotiates as today (text / json / sx). `?page=N` pagination
  layered on top using `log:replay/3`.
- [ ] **4e** — POST /actors/<id>/inbox stays a 202 stub for 4a-4d.
  Step 5 lands the real ingestion pipeline (sig verify + inbox-
  bucket append + projection broadcast).

**Acceptance:** `bash next/tests/http_multi_actor.sh` passes 14+ cases.

---

## Step 5 — POST /inbox: signature verify + ingestion

The receiving side of federation. A peer instance POSTs a signed activity
to `/actors/<id>/inbox`; the kernel verifies the signature, runs the
inbound validation pipeline, appends to the receiving actor's log
(separate from outbox — the inbox is its own log for activities the
actor *received*), and broadcasts to projections.

**Deliverables:**

- New per-actor log: `actor_inbox`. Same shape as outbox; activities
  marked `:received_from => PeerActorId`.
- Inbound pipeline: `stage_envelope` → `stage_signature` (against
  peer's actor-state, not local) → `stage_replay`.
- Peer signature verification needs `:public_keys` from the peer's
  actor-state. v2 fetches the peer's actor doc lazily on first
  contact, caches it in a `peer-actors` projection. Stale-key
  invalidation deferred to v3.
- HTTP handler: `POST /actors/<id>/inbox` returns 202 on accept,
  401 on bad sig, 422 on replay or validation failure.

**Tests:**

- POST /inbox with valid signed activity → 202, activity in inbox log.
- POST /inbox with tampered envelope → 401.
- POST /inbox with unknown actor target → 404.
- POST /inbox with replay → 422.
- Activity broadcast to receiving actor's projections.

**Acceptance:** `bash next/tests/inbox.sh` passes 16+ cases.

---

## Step 6 — Follow lifecycle

Per §13.2:

```sx
(activity 'Follow                            ;; from A → B
  :object actor-id-B
  :to (list actor-id-B))
```

B responds with `Accept` (or `Reject`); A's follower-graph projection
tracks the state. `Undo{Follow}` reverses it.

**Deliverables:**

- New activity-types (runtime via DefineActivity, ideally):
  Follow, Accept, Reject, Undo.
- Follower-graph projection (Erlang-fun stand-in): tracks
  `{ActorId => #{following => [PeerId], followers => [PeerId],
                 pending_outbound => [PeerId], pending_inbound => [PeerId]}}`.
- Accept-handling fold logic: when A receives `Accept{Follow A→B}`,
  move B from `pending_outbound` to `following`.
- Reciprocal: when B receives `Follow A→B`, automatically queue an
  outbound `Accept` (auto-accept policy; manual moderation v3).

**Tests:**

- Follow → 202; sender's pending_outbound includes target.
- Auto-Accept on receiving Follow; both sides' graphs update.
- Reject leaves no following relationship.
- Undo{Follow} removes the following.
- Self-follow rejected.

**Acceptance:** `bash next/tests/follow_lifecycle.sh` passes 14+ cases.

---

## Step 7 — Audience-resolving delivery set

For each outbound activity, compute the set of inbox URLs to POST to.
Sources: explicit `:to` + `:cc` recipients, plus `Public` / `Followers`
expansion via the audience predicates from M1's genesis bundle.

**Deliverables:**

- `outbox:delivery_set/2(Activity, KernelState) -> [InboxUrl]`.
- Public expansion: every known peer instance's shared inbox (or every
  follower of the publishing actor — both modes supported).
- Followers expansion: follower-graph lookup.
- Self-delivery suppression (don't POST to your own inbox).
- Returns a list of `{PeerInstanceUrl, ActorId}` tuples.

**Tests:**

- Activity with `:to: [bob]` → delivery set is bob's inbox.
- Activity with `:to: [Followers]` → set is current followers' inboxes.
- Activity with `:to: [Public]` → set is public reach.
- Self-deliveries excluded.
- Empty audience → empty set.

**Acceptance:** `bash next/tests/delivery_set.sh` passes 12+ cases.

---

## Step 8 — Outbound delivery queue

Per §13.4: every queued delivery has retry semantics. v2 uses one
gen_server-per-peer-instance worker holding a small queue. Failures
back off exponentially; permanent failures (HTTP 410, bad TLS) move to
a dead-letter list visible via `/admin/dead-letter`.

**Deliverables:**

- `delivery_worker.erl`: gen_server per-peer queue with `enqueue/2`
  and a private retry loop.
- Backoff schedule: 30s / 5m / 30m / 6h / 24h then dead-letter.
- Delivery state stored as a projection (`delivery-state`) so it
  survives kernel restarts.
- `outbox:publish/2` augmented: after `log:append`, dispatch to the
  delivery worker for each delivery-set entry.
- HTTP client: extend the existing native httpc primitive to
  carry signed envelope bytes + the right Content-Type.

**Tests:**

- Successful delivery → worker queue empties.
- Failed delivery → backoff schedule respected.
- Dead-letter after max attempts.
- Cross-restart: queue restored from delivery-state projection.
- Concurrent deliveries to multiple peers don't serialise.

**Acceptance:** `bash next/tests/delivery_queue.sh` passes 16+ cases.

---

## Step 9 — Backfill on Follow accept

Per §13.3: A wants B's history when A first follows B. Four modes:

| Mode      | Behavior                                    |
|-----------|---------------------------------------------|
| `none`    | New follower sees only forward-going content |
| `last-N`  | Backfill last N activities                  |
| `last-T`  | Backfill last T duration of activities      |
| `full`    | Backfill entire outbox                      |

**Deliverables:**

- Follow activity may carry `:backfill {:mode :last-N :limit 100}`.
- On Accept, B's outbox is GET-paged with appropriate filters.
- `GET /actors/<id>/outbox?since=Cid&limit=N` returns a paged response.
- Backfill bodies wrap the original activities in `:backfilled true`
  so projections can decide whether to re-fold or skip.

**Tests:**

- `last-N` mode delivers exactly N most-recent activities.
- `last-T` mode delivers everything published since `now - T`.
- `full` mode delivers everything, page by page.
- `none` mode delivers nothing.
- Backfilled activities preserve original `:id` (CID).

**Acceptance:** `bash next/tests/backfill.sh` passes 12+ cases.

---

## Step 10 — Discovery

Per §13.7: webfinger plus actor doc fetch.

**Deliverables:**

- `GET /.well-known/webfinger?resource=acct:alice@<host>` returns the
  actor URL.
- `GET /actors/<id>` returns the actor doc (already exists from
  M1 Step 8c-actors).
- Peer-actor cache: when verifying a peer's signature for the first
  time, fetch their actor doc, store in `peer-actors` projection.
- `discovery:resolve/1("acct:alice@host:port")` returns the actor URL.

**Tests:**

- Webfinger for known actor → 200 with `links[].href`.
- Webfinger for unknown → 404.
- Cross-instance: A resolves an acct on B → fetch succeeds.
- Actor-doc fetch caches the result.
- Cache invalidation on key rotation (v3 — for now, no TTL).

**Acceptance:** `bash next/tests/discovery.sh` passes 12+ cases.

---

## Step 11 — Rich verbs as runtime artifacts

Per the verb-extensibility proof point (M1 §9a), new verbs land as
`DefineActivity` artifacts published into the genesis-equivalent boot
log, not as kernel code changes. v2 adds:

| Verb    | Object shape                          | Use case                              |
|---------|---------------------------------------|---------------------------------------|
| `Note`  | `{content, tags?}`                    | Short authored message                |
| `Announce` | `{object: <ActivityCid>}`          | Propagate a peer's activity to followers |
| `Endorse` | `{object: <Cid>, kind: like|share}` | Cross-actor signaling                 |

Announce is the critical one for federation — it lets one actor
re-broadcast another actor's content to their own followers.

**Deliverables:**

- Three new SX files in a `next/genesis/runtime-verbs/` directory.
- Each is shipped to a fresh instance via a bootstrap manifest entry
  *or* published as the first activity on the actor's outbox; either
  works because of the verb-extensibility mechanism.
- Announce-specific delivery: the announced activity's CID is included
  in the Announce; followers can re-fetch the referenced activity from
  the original instance if their projection wants to fold the body.

**Tests:**

- Define + publish Note works end-to-end.
- Define + publish Announce wraps another activity by CID.
- Announce delivery: A announces B's Note; A's followers see the
  Announce; their `feed` projection optionally fetches the wrapped Note.
- Endorse increments an endorsement counter on the target Activity.
- Verb registration is observable in the `define-registry` projection.

**Acceptance:** `bash next/tests/rich_verbs.sh` passes 14+ cases.

---

## Step 12 — Two-instance smoke test

**The proof point.** `next/tests/smoke_federate.sh` spins up two kernel
instances on distinct ports, walks them through the full federation
flow, and exits 0.

**Test outline:**

```bash
# 0. Start two instances: A on 9999, B on 9998
./next/scripts/start_pair.sh

# 1. Bootstrap two actors: alice@A, bob@B
curl -X POST :9999/activity \
  -H "Authorization: Bearer $TOKEN_A" \
  -d '{"type":"Create","object":{"type":"Person","name":"alice"}}'

curl -X POST :9998/activity \
  -H "Authorization: Bearer $TOKEN_B" \
  -d '{"type":"Create","object":{"type":"Person","name":"bob"}}'

# 2. alice@A discovers bob@B via webfinger
curl :9999/.well-known/webfinger?resource=acct:bob@localhost:9998

# 3. alice follows bob
curl -X POST :9999/activity \
  -d '{"type":"Follow","object":"http://localhost:9998/actors/bob"}'

# 4. Expect alice's follower-graph: pending_outbound includes bob
curl :9999/actors/alice/following | jq -e '.[] | select(.id == "bob")'

# 5. Expect bob auto-accepts; alice's pending_outbound clears
sleep 1
curl :9999/actors/alice/following | jq -e '.[] | select(.id == "bob")'

# 6. bob publishes a Note
curl -X POST :9998/activity -d '{"type":"Create","object":{"type":"Note","content":"hi"}}'

# 7. alice's inbox receives the Note
sleep 1
curl :9999/actors/alice/inbox?page=true | jq -e '.orderedItems[] | .type == "Create" and .object.type == "Note"'

# 8. alice's actor-state projection has the new Note
curl :9999/projections/feed | jq -e ". | length > 0"

# 9. Key rotation: bob rotates keys
curl -X POST :9998/activity -d '{"type":"Update","object":"bob","patch":{...}}'

# 10. alice still verifies older Notes against the old key
#     (via actor-state's key history)

# 11. Announce: alice announces bob's Note
curl -X POST :9999/activity -d '{"type":"Announce","object":"<bob-note-cid>"}'

# 12. Verify Announce delivers to alice's followers (zero in v1 but
#     the activity should be in alice's outbox)

# 13. Shutdown both instances; restart; verify state survives
./next/scripts/stop_pair.sh
./next/scripts/start_pair.sh
curl :9999/actors/alice/following | jq -e '.[] | select(.id == "bob")'
```

**Acceptance for Step 12:** `smoke_federate.sh` exits 0. The full flow
runs without any human-in-the-loop coordination, both instances'
projections converge, and a restart preserves all federation state.

---

## Acceptance criteria for milestone 2

All of:

1. **Each step's test suite passes** (`bash next/tests/<step>.sh`).
2. **The federation smoke test passes** (`bash next/tests/smoke_federate.sh`).
3. **Milestone 1 baseline preserved** — the entire M1 test suite still
   passes (~560 assertions across 50 suites).
4. **Erlang-on-SX conformance** — adding multi-actor + federation kernel
   code in `next/kernel/*.erl` doesn't break Phase 1-8 conformance
   (currently 761/761).
5. **Restart durability** — kill both instances mid-delivery, restart,
   queues resume, projections converge, no log corruption.
6. **Manual real Mastodon poke** — point a Mastodon account at
   `https://next-A.rose-ash.com/actors/alice` and verify the actor
   doc fetches. (Read-only AP interop only — Mastodon Follow is v3
   gating on HTTP-Signatures-2018 compat.)

## What lands when

Steps 1-3 are sequential (multi-actor foundation). Steps 4-10 are
mostly sequential within the federation core but some can parallelise:
4-6 are sequential; 7-9 can interleave after 6 lands.

```
M1 closeout (HEAD) ──┐
                     │
                     ▼
              ┌─── Step 1 ──┬─── Step 2 ──┬─── Step 3
              │             │             │
              └─────────────┼─── Step 4 ──┘
                            │
                            └─── Step 5 ────┐
                                            │
                                  Step 6 ───┤
                                            │
                                  Step 7 ───┤
                                  Step 8 ───┤
                                  Step 9 ───┤
                                            │
                                  Step 10 ──┤
                                            │
                                  Step 11 ──┤
                                            │
                                  Step 12 ──┘
```

Estimated effort: ~40-60 commits across all 12 steps. A focused agent
loop (`loops/fed-sx-m2`) should be able to land this with the same
discipline as M1.

## What's deferred to milestone 3

- **rose-ash port** (the headline of M3). Blog, market, events,
  federation hub, account, orders — all delivered as fed-sx
  applications. Each existing rose-ash domain becomes
  `DefineApplication{...}` artifacts.
- **TLS / HTTP-Signatures-2018 / RFC 9421**. Real Mastodon interop.
- **Multi-instance over real WAN.** Cross-instance over TLS, NAT
  traversal, peer instance allowlists.
- **IPFS / S3 storage backends** as `DefineStorage` entries.
- **Browser client + operator dashboard.** Probably Elm-on-SX.
- **Cross-host conformance** — Python / JS / Haskell hosts running
  fed-sx with the same conformance corpus.
- **OpenTimestamps proofs** as `DefineProof` entries.
- **Reputation, allowlists, rate-limiting** — full §13.6 abuse
  posture.
- **Performance work** — JIT-compiled folds, snapshot acceleration,
  federation batching, mailbox prioritisation.
- **Capability tokens / delegation** — multi-device for a single
  actor.

---

## Appendix A: open questions for milestone 2

Things still under-specified; resolve as work begins.

1. **Inbox-side stage_signature key fetching.** When A receives a
   POST /inbox from peer instance B for the first time, A needs B's
   actor doc to verify the signature. Synchronous fetch vs. queue-
   and-retry? Synchronous is simpler but blocks the inbox handler;
   queue-and-retry needs deferred validation state. Probably
   synchronous with a 5s timeout for v2.

2. **Backfill granularity for `last-N`.** N counts forward (oldest
   first) or backward (newest first)? Forward matches projection-fold
   semantics; backward matches user expectation. Probably forward
   for v2, document the choice.

3. **Auto-Accept policy on Follow.** v2 ships open-world: every
   Follow is auto-accepted. Manual moderation (held in a `pending`
   list, accepted via /admin/) is v3 with the operator dashboard.

4. **Delivery worker per peer instance vs. per peer actor.** Per
   instance is simpler (one HTTPS connection pool) but throttles
   inter-actor bandwidth on busy peers. v2 starts with per-instance;
   per-actor sharding is a perf tweak in §15.

5. **Two-instance test harness.** How do we start a pair of kernels
   in one bash test? Probably `bootstrap:start/3` twice with different
   ActorIds + ports + base paths. Need to confirm `nx_kernel` can be
   started under different registered atoms (`nx_kernel_a`, `nx_kernel_b`)
   for the test. Process registration in this port supports arbitrary
   atom names (verified in M1).

6. **Multi-host conformance.** Adding cross-host tests for federation
   requires Python/JS hosts to implement the v2 spec corpus too.
   Deferred to v3; v2 conformance is one-host only.

7. **Storage of received activities.** When A receives a Note from B
   via /inbox, does A keep B's signed envelope verbatim (for re-broadcast
   on Announce), or does A re-construct + re-sign with A's own key?
   AP-canon: keep verbatim. Confirm at Step 5.

---

## Blockers

Pre-existing regressions inherited from the M1 closeout. Out of m2
scope (substrate, not `next/**`), tracked here so iteration can
proceed.

1. **`next/tests/http_server_tcp.sh` 0/5** — pre-existing regression
   introduced by `78eae9ef` (`fed-sx-m1: 8b-bridge cleanup`).
   `lib/erlang/runtime.sx:1593` still references `er-http-resp-to-sx`
   and `er-http-req-of-sx` in `er-bif-http-listen`'s sx-handler body,
   but the cleanup commit removed both helpers without rewriting the
   BIF. Listener binds (TCP socket accepts), but every request handler
   crashes on first call to the undefined helpers — curl gets 000 /
   empty body. Fix needs to rewrite the sx-handler body around the
   live `er-request-dict-to-proplist` / `er-proplist-to-dict`
   helpers (which the cleanup commit's message claimed are already
   in use, but which the BIF body never picked up). Substrate work,
   belongs on `loops/erlang`. m2 work continues against the in-process
   HTTP layer (`http_marshal.sh` 10/10, `http_publish_fold.sh` 10/10)
   until resolved. Confirmed pre-existing by stashing 1a's changes and
   re-running on the unmodified m1 closeout HEAD.

---

## Progress log

Newest first.

- **2026-06-06** — Step 4b: token -> ActorId map. Cfg's `:tokens`
  proplist (`[{Token, ActorId}, ...]`) maps bearer tokens to
  per-actor publishers. `handle_post_activity` threads the
  resolved `ActorRef` to `publish_if_kernel/3` which calls
  `nx_kernel:publish_to/2` for explicit actor ids and `publish/1`
  for the back-compat `legacy` atom (M1's `:publish_token`
  single-token field still works as-is). When both fields are
  present, `:tokens` takes precedence; `:publish_token` is the
  fallback on miss. Dead `expected_token/1` helper removed. 8
  new cases in `http_multi_actor.sh` (25/25 total) covering
  two-actor token routing, log-tip isolation, interleaved
  publishes, bad-token 401, back-compat coexistence, no-kernel
  stub path. Conformance 761/761 preserved. 116/116 across 10
  Step-4-adjacent suites.

- **2026-06-06** — Step 4a: per-actor HTTP sub-paths. New
  `split_first_slash/1` helper lets GET / POST `/actors/<id>/...`
  paths route on the sub-segment (`outbox`, `inbox`, `followers`,
  `following`). Four new content-negotiated response stubs
  (`actor_outbox_response_for/2`, `actor_inbox_get_response_for/2`,
  `actor_followers_response_for/2`, `actor_following_response_for/2`)
  with text / json / activity_json / sx variants, mirroring the
  existing `actor_doc_response_for/2` shape. POST
  `/actors/<id>/inbox` returns a 202 Accepted stub
  (`actor_inbox_post_response/0` + `accepted_response/1`); real
  ingestion pipeline lands in Step 5. Unknown sub-paths return
  404. Bare `/actors/<id>` keeps the M1 actor-doc arm intact —
  `http_route` and `http_post_format` regression suites unchanged
  (10/10 each). 17/17 in `http_multi_actor.sh`. Conformance
  761/761 preserved. 120/120 across 10 Step-4-adjacent suites.

- **2026-06-06** — Step 3 (closes Step 3): key rotation via Update.
  `actor_state.erl` `fold_update` routes patches through
  `apply_patch/3` which special-cases `{add_publicKey, KeyProplist}`
  (append + default `:created` to activity's `:published`) and
  `{supersede, OldKeyId}` (mark `:superseded_at`, idempotent).
  Other patch entries still last-write-wins per key. New exports
  `key_history/1`, `active_keys_at/2`, `find_key_by_id/2` give the
  projection-driven view that `envelope:verify_signature/2`
  consumes for time-aware lookup. Rotation-purpose schema gating
  (`rotate-key` purpose check on the rotation activity itself)
  deferred to Step 5 (peer-side stage_signature). `key_rotation.sh`
  16/16 covers rotation arithmetic, key_history preservation,
  active_keys_at at T=pre, T=rotation, T=post, and live
  `envelope:verify_signature/2` round-trips for pre / post / cross
  scenarios including the negative-case post-rotation K1 sig.
  Conformance 761/761 preserved. 132/132 across 9 Step-3-adjacent
  suites (key_rotation, actor_state_pure, actor_lifecycle,
  envelope_sig, envelope_shape, envelope_canonical, nx_kernel_multi,
  bootstrap_start, smoke_app_pure).

- **2026-06-06** — Step 2c (closes Step 2): `bootstrap_actor/4` +
  end-to-end `actor_lifecycle.sh`. New pure-functional export
  `nx_kernel:bootstrap_actor/4(ActorId, Profile, KeySpec, State)`
  adds an actor bucket via `add_actor/4`, derives the kernel AS
  proplist from `Profile`'s `:public_keys`, builds a Create
  envelope wrapping the profile's `:type` (defaults `person`) +
  field set, and calls `publish/3`. gen_server variant
  `bootstrap_actor/3` for live-kernel use plus a corresponding
  `handle_call` branch. `actor_lifecycle.sh` 15/15 covers pure
  bootstrap (log_tip advances, Create-shape, dup detection),
  two-actor independence, gen_server bootstrap, and
  `actor_state` projection capture for Person + Service + Group.
  Step 2 fully closed (2a + 2b + 2c). Conformance 761/761.
  146/146 across 10 Step-2-adjacent suites.

- **2026-06-06** — Step 2b: actor-state projection Erlang module.
  New `next/kernel/actor_state.erl` with `fold/2` over Create / Update
  / Move activities. Profile is a property list of `:type / :name /
  :preferredUsername / :summary / :icon / :public_keys / :moved_to /
  :created`. Create captures fields and `:published` as `:created`;
  duplicate Create is no-overwrite; non-actor Creates and `:actor`-
  less envelopes pass through. Update last-write-wins per patch key.
  Move records `:moved_to`. `fold_fn/0` is a 2-arity Erlang fun for
  `projection:start_link/3` (structural twin of `define_registry`).
  `next/tests/actor_state_pure.sh` 19/19. Conformance 761/761.
  Step-2-adjacent no-regression gate 106/106 across 6 suites.

- **2026-06-06** — Step 2a: genesis Person/Service/Group object-
  types. Three new SX files in `next/genesis/object-types/` with
  the same shape as `note.sx` / `sx-artifact.sx` (`:name`, `:doc`,
  `:schema` checking `(string? (-> obj :name))`). Manifest extended
  to 13 object-types / 34 total entries. `genesis_parse.sh` +7
  cases (57/57). Hardcoded counts bumped in `bootstrap_read.sh`,
  `bootstrap_load.sh`, `bootstrap_populate.sh`, `bootstrap_start.sh`
  (66/66 across those four). `bootstrap_build.sh` 12/12 (bundle CID
  computed dynamically). Conformance 761/761 preserved. 211 / 211
  across 12 Step-2-adjacent suites.

- **2026-06-06** — Step 1b: gen_server multi-actor calls.
  `nx_kernel` exports `add_actor/3`, `publish_to/2`, `log_tip_for/1`,
  `actors/0`, `state_for/1`, `bucket_for/1`,
  `with_projections_for/2` — each is a `gen_server:call` delegating
  to the pure-functional bucket API from 1a. Existing single-actor
  calls untouched. `nx_kernel_multi.sh` extended with 9 gen_server
  cases (26 total); 134 / 134 across 12 nx_kernel-adjacent + http
  suites. Conformance 761/761 preserved. Per-actor mailbox sharding
  noted as forward-looking — current single gen_server serialises
  publishes across actors, which is fine for Steps 1-3 (single-actor
  HTTP endpoints) and is naturally untangled by Step 4's per-actor
  routing.

- **2026-06-06** — Step 1a: per-actor bucket refactor of `nx_kernel`.
  State shape now `[{actors, [{Id, Bucket}, …]}, {next_actor_seq, N}]`;
  added pure-functional multi-actor APIs (`new/0`, `add_actor/4`,
  `has_actor/2`, `actors/1`, `publish/3`, per-actor accessors,
  `with_actor_projections/3`). Legacy single-actor accessors
  preserved as bucket-0 lookups so every M1 test continues to
  pass via `bootstrap:start/3` → `new/3` → first-bucket read.
  Local `has_keyed`/`find_keyed`/`set_keyed`/`set_bucket` helpers
  cover the keyed-list ops since `lists:keymember`/`keyfind` aren't
  registered in this substrate. New test suite
  `next/tests/nx_kernel_multi.sh` 17/17; all M1 nx_kernel-adjacent
  suites green (`bootstrap_start`, `nx_kernel_server`, `http_publish`,
  `smoke_app_pure`, `http_post_format`, `http_publish_fold`,
  `http_marshal`). Erlang conformance 761/761 preserved.
