# 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. - [ ] **2b** — Actor-state projection fold (Erlang-fun stand-in, mirrors Step 5d-pure's `define_registry`): - On `Create{Person|Service|Group}`: register the actor's profile in `{ActorId => #{type, name, preferredUsername, summary, icon, public_keys, created}}`. - On `Update{Person|Service|Group, patch}`: deep-merge the patch. - On `Move`: record `:movedTo` pointer. - `next/kernel/actor_state.erl` with `fold_fn/0` plugging into `projection:start_link/3`. Pure-functional + gen_server-bridged tests as a single `actor_state_pure.sh`. - [ ] **2c** — `nx_kernel:bootstrap_actor/4(ActorId, Profile, KeySpec, State)` — publishes `Create{Person{...}}` as the actor's first activity, exercising the full pipeline. Integration test in `actor_lifecycle.sh` ties 2a artefacts (SX files), 2b projection, and 2c bootstrap together. **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:** - Update fold extension: `Update{Person, patch: {add_publicKey: K, supersede: {OldId, NewId}}}`. - A `key-history` view on actor-state. - `envelope:verify_signature/2` already does time-aware lookup (M1 §Step 2c); confirm it works against the projection-driven actor-state. **Tests:** - Rotation publishes a new key; old key marked superseded. - Pre-rotation activities verify against the old key. - Post-rotation activities verify against the new key. - A rotation activity must itself be signed by an active key with appropriate purpose (`sign-activity` or `rotate-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/ # actor doc GET /actors//outbox # OrderedCollection GET /actors//outbox?page=N # page POST /actors//inbox # peer delivery to this actor GET /actors//followers # follower list GET /actors//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:** - New route prefixes: `/actors//inbox`, `/actors//followers`, `/actors//following`. - `http_server:route/3` (Cfg → Cfg+Kernel) so handlers can look up actor state. - Cfg's `:publish_token` becomes `:tokens => #{Token => ActorId}` map. - `cid_response_for/2` already format-aware; per-actor outbox listing uses the same machinery. **Tests:** - GET /actors/alice → 200 with actor doc. - GET /actors/unknown → 404. - POST /activity with alice's token publishes to alice. - POST /activity with bob's token publishes to bob. - Two actors' outboxes are independent. **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//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//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//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@` returns the actor URL. - `GET /actors/` 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: }` | Propagate a peer's activity to followers | | `Endorse` | `{object: , 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":""}' # 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/.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 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.