Closes Step 10 (10a discovery + 10b webfinger + 10c fetch). New
next/kernel/discovery_fetch.erl produces a 1-arity FetchFn closure
suitable for peer_actors:lookup_or_fetch_srv/2, completing the
discovery half that Step 5c's peer_actors cache stubbed out.
discovery_fetch API:
make_fetch_fn(Cfg) -> fun((PeerId) -> {ok, AS} | {error, _})
fetch(Url, Cfg) -> {ok, AS} | {error, _}
actor_doc_url(BaseUrl, PeerAtom) -> <Base>/actors/<peer>
accept_header/0 -> <<"application/vnd.fed-sx.actor-doc">>
decode_body(Body) -> {ok, AS} | {error, bad_actor_doc}
Closure GETs <base>/actors/<peer> via the Step 8e BIF with
Accept = application/vnd.fed-sx.actor-doc, decodes the response
body via term_codec:decode/1, returns the peer-actor-state
proplist (currently [{public_keys, [...]}]) in the shape
envelope:verify_signature consumes.
Cfg reuses dispatch_http's :peer_url / :peer_url_fn resolution so
a single Cfg threads through both delivery (8f) and discovery (10c).
Server side: http_server.erl extended to serve the same MIME.
- accept_format/1 matches application/vnd.fed-sx.actor-doc first
via the new actor_doc_prefix/0 — content negotiation atom is
`actor_doc`.
- content_type_for(actor_doc) emits the MIME on outbound.
- actor_doc_response_for/3 kernel-aware arm: with kernel + actor
-> 200 + term_codec:encode of nx_kernel:state_for/1 result.
Unknown actor -> not_found_response/0. Other formats fall
through to the existing /2 stub variants.
- actor_get/3 route dispatch threads Cfg to the /3 arm.
Port quirks documented:
* This Erlang doesn't support Mod:Fun(X) dispatch on a variable
module — kernel_actor_state/2 hardcodes nx_kernel; the Cfg
:kernel field is just a "no kernel wired" -> nil flag.
* nx_kernel:actor_state/1 is the LEGACY single-bucket accessor
that takes State (not ActorId); the server-side variant we
want is state_for/1 (gen_server:call wrapper). Easy mismatch,
documented in the comment.
Outcome mapping:
2xx + decodable body -> {ok, AS}
2xx + bad body -> {error, bad_actor_doc}
non-2xx -> {error, {status, N}}
resolver miss -> {error, no_peer_url}
transport -> {error, Reason} (BIF re-raises)
Test: next/tests/discovery_fetch.sh 11/11
Server side (in-process via http_server:actor_doc_response_for):
- Accept negotiation
- kernel + actor -> 200 + decodable body w/ :public_keys
- unknown actor -> 404
Closure side (live HTTP against background python stub returning
hand-crafted term_codec bytes):
- URL construction <base>/actors/X
- fetch live -> {ok, AS}
- make_fetch_fn closure -> {ok, AS} via static :peer_url map
- missing peer -> {error, no_peer_url}
- 404 path -> {error, {status, 404}}
- peer_actors:lookup_or_fetch/3 caches the result
Test setup note: Python term_codec encoder uses ELEMENT COUNT
(not byte length) for l/t headers — see encode/1 in term_codec.erl
which does integer_to_list(length(T)). Easy bug, documented in the
test's python source.
No-regression gates green: Erlang conformance 761/761,
httpc_request 10/10, dispatch_http 10/10, http_listen_bif 5/5,
peer_actors 19/19, discovery 12/12, http_accept 13/13,
http_actors 13/13.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
next — fed-sx Milestone 1 kernel
Single-instance, single-actor fed-sx server built as Erlang-on-SX modules.
See plans/fed-sx-design.md for the architecture and
plans/fed-sx-milestone-1.md for the build plan + per-step progress log.
Status
Both Step 9 smoke proof points are functional in-process:
- 9a-pure (verb extensibility) —
Create{DefineActivity{Pin}}registers Pin at runtime; subsequentPin{path, cid}activities fold into a pin-state projection. Zero kernel code between definition and use. Seenext/tests/smoke_pin_pure.sh. - 9b-pure (reactive application) — A trigger projection matches Notes
tagged
smoketestand derives aTestEchocarrying the source CID. Seenext/tests/smoke_app_pure.sh.
The remaining 9a-tcp / 9b-tcp deliverables layer TCP transport on top — see
Substrate gaps below.
Layout
next/
├── kernel/ Erlang-on-SX kernel modules (.erl)
├── genesis/ SX source files for the bootstrap bundle
├── tests/ Bash test scripts driving sx_server.exe via the epoch protocol
└── data/ Runtime state — gitignored
Module map
| Module | Role |
|---|---|
nx_cid.erl |
Canonical CID wrapper around the host cid:to_string BIF |
envelope.erl |
Activity envelope shape, canonical bytes, time-aware sig verify |
log.erl |
Per-actor in-memory append log (open / append / tip / replay / entries) |
registry.erl |
Pure-functional + gen_server-wrapped registry keyed by Kind |
pipeline.erl |
Validation driver + stage_envelope/signature/replay/schema |
projection.erl |
Pure projection driver + gen_server-per-projection wrapper |
outbox.erl |
Envelope construct + sign + publish orchestrator + broadcast |
bootstrap.erl |
Genesis read/build/verify/load + one-call start/3 kernel bring-up |
define_registry.erl |
Meta-projection fold for Create{Define*} → registry |
sandbox.erl |
eval_pure/2,3 try/catch envelope for projection folds |
nx_kernel.erl |
Long-lived runtime orchestrator; per-actor bucketed state (m2 Step 1a) |
http_server.erl |
route/1,2 + format-aware GET + POST + Accept header content negotiation |
Genesis bundle
next/genesis/ contains 31 SX files across 7 sections, all consumed as data
(read + serialised by bootstrap:populate_registry, not eval'd):
- 3 activity-types — Create, Update, Delete
- 10 object-types — SXArtifact, Note, Tombstone, 6 Define* meta-types, Snapshot
- 7 projections — activity-log, by-type, by-actor, by-object, actor-state, define-registry, audience-graph
- 3 validators — envelope-shape, signature, type-schema
- 3 codecs — dag-cbor, raw, dag-json
- 2 sig-suites — rsa-sha256-2018, ed25519-2020
- 3 audience predicates — Public, Followers, Direct
manifest.sx is the bundle root, listed in dependency-friendly order.
Tests
43 test suites, ~560+ assertions. Each script drives sx_server.exe via the
epoch protocol — loads the Erlang substrate, loads relevant kernel modules
via code:load_binary / erlang-load-module, then exercises behaviour
through erlang-eval-ast.
Conventions:
- Scripts marked
_pure.shexercise pure-functional state. - Scripts marked
_server.sh(or no suffix) exercise gen_server APIs and must inlinestart_linkwith operations — the Erlang-on-SX scheduler doesn't preserve spawned processes across separateerlang-eval-astinvocations. smoke_*_pure.share end-to-end smoke tests demonstrating the §Step 9 proof points without TCP / curl / JSON.
The Erlang-on-SX conformance gate (bash lib/erlang/conformance.sh, 729 /
729) is the no-regression contract — every commit on loops/fed-sx-m1
preserves it.
Substrate
Each .erl source file is hot-loaded at boot via
code:load_binary(Mod, Filename, SourceString) (Phase 7 BIF). Tests drive
the runtime via the epoch protocol:
printf '(epoch 1)\n(load "lib/erlang/runtime.sx")\n(epoch 2)\n<test-expr>\n' \
| hosts/ocaml/_build/default/bin/sx_server.exe
The kernel calls into these host primitives: crypto:hash/2,
cid:from_bytes/1, cid:to_string/1, file:read_file/1, file:write_file/2,
file:delete/1, file:list_dir/1, code:load_binary/3, plus http:listen/2
(the briefing's allowed scope exception, added to lib/erlang/runtime.sx).
Substrate gaps (parked work)
These three gaps block the remaining unchecked deliverables:
-
Term codec (
3b/3c) — all three substrate fixes done 2026-06-05:erlang:binary_to_list/1anderlang:list_to_binary/1registered inlib/erlang/runtime.sx(iolist-aware); the tokenizer's$Xbranch emits the decimal char code;atom_to_list/1andinteger_to_list/1now return Erlang charlists (standard Erlang semantics) withlist_to_atom/list_to_integeraccepting both charlists and SX strings for back-compat. 759/759 conformance. The full term-codec primitive set is in place — Step 3b on-disk segment writer can encode arbitrary Erlang activity terms (atoms, ints, binaries, tuples, lists) into byte sequences using only Erlang-native primitives. -
SX-source eval bridge — There's no BIF that lets Erlang call into the SX evaluator on a parsed source string. Blocks evaluating the
:schema/:fold/:predicate/:verifybodies from the genesis bundle. Erlang-fun stand-ins (pipeline:stage_schema,define_registry:fold, etc.) prove the API shapes; the bridge would let bundle bodies dispatch through them unchanged. -
Dict ↔ proplist marshalling for
http:listen/2— done 2026-06-05.er-bif-http-listenmarshals the native server's request dict ({:method :path :query :headers :body}) into the proplist shape[{method, Bin}, {path, Bin}, {query, Bin}, {headers, [{Name, Value}]}, {body, Bin}]thathttp_server:route/2consumes, and converts the handler's response proplist back to{:status :headers :body}for the native server to serialise. Helpers (er-request-dict-to-proplist,er-proplist-to-dict,er-of-sx-deep,er-to-sx-deep,er-dict-to-header-proplist,er-proplist-fill!) live alongside the BIF wrapper inlib/erlang/runtime.sx. The BIF also spawns the handler into a real Erlang process viaer-spawn-fun+er-sched-run-all!soself()/gen_server:callwork inside route handlers (the kernel and projection gen_servers reach the handler this way). Verified bynext/tests/http_marshal.shand the live TCP smokenext/tests/http_server_tcp.sh/http_server_start.sh. UnblocksStep 8b-start(TCP listener spawn) and the curl-driven 9a-tcp / 9b-tcp smoke tests.
Bringing up the kernel
For tests, bootstrap:start/3(ActorId, KeySpec, ActorState) is the
one-call boot:
KM = <<1,2,3,4>>,
KS = [{key_id, k1}, {algorithm, ed25519}, {value, KM}],
AS = [{public_keys, [[{id, k1}, {created, 0}, {value, KM}]]}],
Pid = bootstrap:start(alice, KS, AS),
%% nx_kernel + registry populated; you now have a kernel.
The HTTP layer (http_server) and nx_kernel:publish/1 flow through the
same in-process gen_servers; http_publish_fold.sh is the end-to-end proof
the chain works.
What's next (when work resumes)
In priority order:
- 8b-start —
http_server:start/1spawns a process hostinghttp:listen/2. (8b-bridge done — see Substrate gap #3.) - 9a-tcp / 9b-tcp — replace the in-process smoke scripts with curl-driven versions hitting the running server.
- Term codec / on-disk log — needs either a new BIF or a temp-file workaround; current in-memory log keeps everything functional otherwise.
- SX-source eval bridge — unlocks real
:schema/:foldbody evaluation from the genesis bundle.