Files
rose-ash/plans/fed-sx-host-primitives.md
giles 4548461bfc
Some checks failed
Test, Build, and Deploy / test-build-deploy (push) Failing after 2m50s
fed-prims: Phase I — handoff (RESOLVED blocker + primitive->BIF mapping)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-18 18:48:35 +00:00

291 lines
16 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# fed-sx host primitives — `hosts/ocaml/`
The single blocker between Erlang Phase 8 (FFI mechanism — done) and starting
fed-sx Milestone 1: the SX OCaml runtime exposes no crypto / CID / HTTP host
primitives for the Phase 8 BIF wrappers to call. This plan adds exactly that
surface, pure-OCaml where it must stay WASM-safe, native-only where it can't.
Reference: `plans/fed-sx-milestone-1.md` (build steps 1-8),
`plans/erlang-on-sx.md` Blockers ("SX runtime lacks platform primitives …").
## The hard constraint — WASM boundary
`hosts/ocaml/lib/` is the `sx` library. `hosts/ocaml/browser/dune` links it
with `(modes byte js wasm)`. **Anything added to `lib/sx_primitives.ml` must
compile under `js_of_ocaml` AND `wasm_of_ocaml`.** Therefore:
- **Pure OCaml only** for hash / CBOR / CID / Ed25519 / RSA. No `digestif`,
no `mirage-crypto`, no C stubs, no `Unix` dependency in these primitives.
(None of those libs are even installed — the switch has only
re/unix/yojson/otfm/js_of_ocaml. Pure OCaml is both required and hermetic.)
- **HTTP server is native-only**: it needs sockets/threads. Register it in
`bin/sx_server.ml` via `Sx_primitives.register` (precedent: `eval-in-env` at
`bin/sx_server.ml:721`), **not** in the shared lib. It must never enter the
WASM build.
- **`file-list-dir`** uses `Sys.readdir` (stdlib, WASM-stubbed) — safe in lib,
but the fed-sx server is native anyway; native registration is acceptable too.
**Every phase must prove the WASM build still links** (`sx_build target="wasm"`
or `bash hosts/ocaml/browser/test_boot.sh`) before its commit. A broken WASM
browser kernel is a hard regression and fails the phase.
## Primitive surface (what fed-sx Milestone 1 actually needs)
Mapped to `plans/fed-sx-milestone-1.md` build steps:
| Primitive (SX name) | Signature | fed-sx step | Host |
|---|---|---|---|
| `crypto-sha256` | `(bytes) -> hex-string` | 1, 2 | lib (pure) |
| `crypto-sha512` | `(bytes) -> hex-string` | 2 | lib (pure) |
| `crypto-sha3-256` | `(bytes) -> hex-string` | 1 (CID default) | lib (pure) |
| `cbor-encode` | `(sx-value) -> bytes` (dag-cbor, deterministic) | 1 | lib (pure) |
| `cbor-decode` | `(bytes) -> sx-value` | 1 (round-trip tests) | lib (pure) |
| `cid-from-bytes` | `(codec multihash-bytes) -> cid-string` | 1 | lib (pure) |
| `cid-from-sx` | `(sx-value) -> cid-string` (canonicalize→cbor→sha→mh→cidv1) | 1 | lib (pure) |
| `ed25519-verify` | `(pubkey-32 msg sig-64) -> bool` | 2 | lib (pure) |
| `rsa-sha256-verify` | `(der-spki msg sig) -> bool` (PKCS#1 v1.5) | 2 | lib (pure) |
| `file-list-dir` | `(path) -> (list string)` | 3 | lib/native |
| `http-listen` | `(port handler-fn) -> never` (handler: req-dict→resp-dict) | 8 | **native only** |
Deferred (not Milestone 1): `httpc-request` (HTTP client — federation is v2),
`sqlite-*` (Milestone 1 is file-on-disk; sqlite is v2 indexes).
## Registration pattern (established)
`lib/sx_primitives.ml`:
```ocaml
register "crypto-sha256" (fun args ->
match args with
| [String s] -> String (Sha2.sha256_hex s)
| _ -> raise (Eval_error "crypto-sha256: (bytes)"))
```
Errors: `raise (Eval_error "name: shape")`. Byte strings are OCaml `string`
(SX `String`). Lists are `Pair`/`Nil` per `sx_types.ml`. Native-only prims go in
`bin/sx_server.ml` the same way.
## Phasing — one feature per loop iteration
Dependency order. Each phase: implement → `dune build` (ocaml) → **WASM build
check** → tests → commit → tick box → Progress-log line → push.
### Phase A — SHA-2 (sha256 + sha512), pure OCaml ✅ DONE
- New `lib/sx_sha2.ml` (or inline in primitives if small): SHA-256 + SHA-512.
- Primitives `crypto-sha256`, `crypto-sha512` → lowercase hex string.
- Tests (`bin/run_tests.ml` or a dedicated `bin/test_crypto.ml`): NIST vectors —
`""`, `"abc"`, the 896-bit message, a 1MB "a" repetition.
- sha256("") = `e3b0c442…b7852b855`; sha256("abc") = `ba7816bf…f20015ad`
- sha512("abc") = `ddaf35a1…2a9ac94f…`
- **Acceptance:** vectors pass; WASM build links; OCaml conformance unchanged.
### Phase B — SHA-3 / Keccak-256, pure OCaml ✅ DONE
- Keccak-f[1600] + SHA3-256 padding. Primitive `crypto-sha3-256`.
- Tests: sha3-256("") = `a7ffc6f8…0f8434a`; sha3-256("abc") = `3a985da7…11431532`.
- **Acceptance:** NIST SHA-3 vectors pass; WASM links.
### Phase C — dag-cbor encoder + decoder, pure OCaml ✅ DONE
- RFC 8949 deterministic subset (RFC 8742 dag-cbor): unsigned/negative ints,
byte strings, text strings, arrays, maps with **keys sorted by
length-then-bytewise**, bool, null, tag 42 (CID link). No floats unless a
fed-sx shape needs them (defer; document).
- SX↔CBOR mapping: `Integer`→int, `String`→text str, `Bool`, `Nil`→null,
`Pair/Nil`→array, `Dict`→map (sorted keys), keyword/symbol→text str.
- Primitives `cbor-encode`, `cbor-decode`. Round-trip property tests + RFC 8949
appendix-A vectors + a "reordered dict keys → identical bytes" determinism test.
- **Acceptance:** vectors + round-trip + determinism pass; WASM links.
### Phase D — CID computation, pure OCaml ✅ DONE
- Multihash (sha2-256 = 0x12, sha3-256 = 0x16; varint code + varint len + digest).
- CIDv1 = `0x01 || codec-varint || multihash`. Codecs: dag-cbor 0x71, raw 0x55.
- Multibase base32 lower (`b` prefix, RFC 4648 no-pad).
- Primitives `cid-from-bytes` (codec, raw mh bytes), `cid-from-sx`
(canonicalize → cbor-encode → sha2-256 → multihash → cidv1 → base32).
- Tests: known IPFS CIDs — cross-check against `ipfs` CLI if present, else the
fixed vectors for `{}` dag-cbor and `"abc"` raw (hardcode expected strings).
Determinism: same SX value (whitespace/comment/key-order variants) → same CID.
- **Acceptance:** matches reference CIDs; determinism holds; WASM links. Satisfies
fed-sx Milestone 1 Step 1.
### Phase E — Ed25519 verify, pure OCaml ✅ DONE
- Curve25519/edwards25519 field arith (mod 2^255-19), point decompress,
SHA-512-based verify per RFC 8032 §5.1.7. (Reuse Phase A sha512.)
- Primitive `ed25519-verify (pubkey msg sig) -> bool`. Bad-length args → false,
not exception (verify is total).
- Tests: RFC 8032 §7.1 vectors (TEST 1-4 + the 1024-byte one). Tampered msg/sig
→ false. Wrong-length key → false.
- **Acceptance:** all RFC 8032 vectors pass; WASM links. Satisfies fed-sx Step 2
(Ed25519 sig-suite).
### Phase F — RSA-SHA256 verify (PKCS#1 v1.5), pure OCaml ✅ DONE
- Minimal pure-OCaml bignum (only need modexp + DER parse). Parse SPKI DER →
(n, e). RSASSA-PKCS1-v1_5 verify with SHA-256 (Phase A).
- Primitive `rsa-sha256-verify (der-spki msg sig) -> bool`.
- Tests: a generated 2048-bit keypair's signature (vectors hardcoded in the test
from a one-off openssl run, documented in a comment), tamper → false.
- **Acceptance:** vector verifies; tamper fails; WASM links. Satisfies fed-sx
Step 2 (rsa-sha256-2018 sig-suite). **Lower priority** than E — Ed25519 is the
modern default; RSA can land after the HTTP phase if time-boxed.
### Phase G — `file-list-dir`, native-safe ✅ DONE
- `Sys.readdir` → sorted SX list of names (no `.`/`..`). Errors → `enoent`/
`enotdir` classified like the existing `file-read` error mapping.
- Tests: list a known dir, missing dir → error, file-not-dir → error.
- **Acceptance:** passes; WASM build still links (Sys.readdir is stubbed there).
Satisfies fed-sx Step 3 segment replay.
### Phase H — HTTP/1.1 server, **native-only** (`bin/sx_server.ml`) ✅ DONE
- Minimal threaded HTTP/1.1: accept loop (`Unix` + `Thread`), parse request
line + headers + body (Content-Length), build an SX request dict
`{:method :path :query :headers :body}`, call the SX handler callable, take an
SX response dict `{:status :headers :body}`, write it. Connection: close
(keep-alive optional, defer). Bind `127.0.0.1:<port>`.
- Primitive `http-listen (port handler) -> never-returns` registered ONLY in
`bin/sx_server.ml`. Document that it is absent from the WASM kernel.
- Tests: `bin/test_http.sh` — start a server on a port with a tiny SX echo
handler in a subprocess, `curl` GET/POST/404/headers, assert responses, kill.
- **Acceptance:** curl test script green; WASM build untouched (prim not in lib).
Satisfies fed-sx Step 8 transport.
### Phase I — handoff ✅ DONE
- Flip the `plans/erlang-on-sx.md` Blockers entry "SX runtime lacks platform
primitives …" to **RESOLVED**, listing the exact SX primitive names so the
Erlang loop can one-line-wire its blocked Phase 8 BIFs (`crypto:hash/2`,
`cid:from_bytes/1`, `cid:to_string/1`, `file:list_dir/1`, plus note
`httpc`/`sqlite` still deferred). **Do not edit `lib/erlang/`** — that wiring
is the Erlang loop's job; this phase only updates the blocker text + this
plan's "Handoff" section with the primitive→BIF mapping.
- **Acceptance:** blocker text updated; fed-sx Milestone 1 Steps 1-3 + 8
prerequisites all green.
## Scope (hard)
- **Edit only:** `hosts/ocaml/lib/**`, `hosts/ocaml/bin/**`, this plan file.
- **Do NOT edit:** `lib/erlang/**` (Erlang loop owns BIF wiring), `spec/`,
`lib/` root, other `lib/<lang>/`, `plans/erlang-on-sx.md` *except* the one
Blockers entry in Phase I.
- **Pure OCaml for lib primitives.** No new opam deps. If a phase seems to need
one, stop and add a Blockers entry instead.
- **Prove WASM every phase.** No commit without `test_boot.sh` (or wasm build)
green.
- **Never push to `main` or `architecture`.** Branch `loops/fed-prims`, push
`origin/loops/fed-prims`.
- One feature per commit. Short factual messages: `fed-prims: SHA-256 + 4 NIST
vectors`. Tick the box, append a dated Progress-log line (newest first).
- **Never call `sx_build` with no timeout-awareness** — OCaml builds are slow;
use the MCP `sx_build target="ocaml"` / `target="wasm"` tools or
`dune build` with a generous timeout. If the build hangs >10min, Blockers +
stop.
## Build & test reference
```bash
cd hosts/ocaml && dune build bin/sx_server.exe 2>&1 | tail # native
bash hosts/ocaml/browser/test_boot.sh # WASM links + boots
cd hosts/ocaml && dune exec bin/run_tests.exe 2>&1 | tail # OCaml unit tests
SX_SERVER=hosts/ocaml/_build/default/bin/sx_server.exe \
timeout 400 bash lib/erlang/conformance.sh 2>&1 | tail -3 # no-regression gate
```
A primitive is reachable from SX via the epoch protocol:
```bash
printf '(epoch 1)\n(crypto-sha256 "abc")\n' | \
hosts/ocaml/_build/default/bin/sx_server.exe
```
## Handoff (Phase I fills this in)
| SX primitive | Erlang Phase 8 BIF it unblocks |
|---|---|
| `crypto-sha256` / `crypto-sha512` / `crypto-sha3-256` | `crypto:hash/2` |
| `cid-from-bytes` / `cid-from-sx` | `cid:from_bytes/1`, `cid:to_string/1` |
| `ed25519-verify` / `rsa-sha256-verify` | `crypto:verify` / sig-suites |
| `file-list-dir` | `file:list_dir/1` |
| `http-listen` | fed-sx kernel `http:listen/2` (Milestone 1 Step 8) |
**Status: DELIVERED (Phases AH, 2026-05-18).** All primitives are
registered and reachable from SX (`(eval "(crypto-sha256 \"abc\")")`
via the epoch protocol). Signatures the Erlang loop can one-line-wire:
- `(crypto-sha256 bytes) -> hex-string` — also `crypto-sha512`,
`crypto-sha3-256`. lib (`Sx_sha2`/`Sx_sha3`), WASM-safe.
- `(cbor-encode value) -> bytes` / `(cbor-decode bytes) -> value` —
deterministic dag-cbor, lib (`Sx_cbor`), WASM-safe.
- `(cid-from-bytes codec mh-bytes) -> cid-string` /
`(cid-from-sx value) -> cid-string` — lib (`Sx_cid`), WASM-safe.
- `(ed25519-verify pk msg sig) -> bool` /
`(rsa-sha256-verify spki msg sig) -> bool` — total (bad input →
false), lib (`Sx_ed25519`/`Sx_rsa`), WASM-safe.
- `(file-list-dir path) -> (list string)` — sorted, lib, WASM-stubbed.
- `(http-listen port handler) -> never` — **NATIVE ONLY**
(`bin/sx_server.ml`); absent from the WASM kernel by design.
Still **deferred** (not Milestone 1, not provided here): `httpc-request`
(HTTP client / federation v2), `sqlite-*` (v2 indexes). The Erlang loop
should leave `httpc`/`sqlite` BIFs blocked with that note.
## Progress log
_Newest first._
- 2026-05-18 — Phase I: handoff. `erlang-on-sx.md` Blockers gained one
RESOLVED entry (no "SX runtime lacks…" entry pre-existed; it read
"_(none yet)_") mapping every delivered primitive → its Phase 8 BIF,
with httpc/sqlite explicitly left deferred. Handoff section here
filled with signatures + native/WASM notes. Doc-only (no lib/erlang/
edits); Erlang 530/530 unchanged. **fed-sx Milestone 1 Steps 1-3 + 8
prerequisites all green — plan complete (Phases AI done).**
- 2026-05-18 — Phase H: `http-listen` primitive in `bin/sx_server.ml`
(NATIVE ONLY — Unix sockets + Thread per connection, Mutex around
the shared-runtime handler call; HTTP/1.1, Connection: close;
req {:method :path :query :headers :body} → resp {:status :headers
:body}). Test `bin/test_http.sh`: curl GET+query / POST+body / 404
/ custom header — 6/6. NOT in lib, so WASM kernel untouched (boot
green); run_tests 4897 unchanged; Erlang 530/530. Satisfies fed-sx
Milestone 1 Step 8 transport.
- 2026-05-18 — Phase G: `file-list-dir` primitive in
`lib/sx_primitives.ml` (Sys.readdir → sorted names, no "."/"..";
Sys_error prefixed like file-read, msg carries enoent/enotdir).
4 tests: sorted listing, missing dir, not-a-dir, arity. WASM boot
green (Sys.readdir stubbed there); Erlang 530/530; run_tests +4.
Satisfies fed-sx Step 3 segment replay.
- 2026-05-18 — Phase F: pure-OCaml `lib/sx_rsa.ml` (self-contained
bignum modexp, minimal DER SPKI reader, RFC 8017 §8.2.2 PKCS#1
v1.5 verify with SHA-256 DigestInfo prefix). Primitive
`rsa-sha256-verify` total. 5 tests on a fixed RSA-2048 vector
(one-off python-cryptography keygen, hardcoded): valid, tampered
msg/sig, garbage SPKI, non-string. WASM boot green with new lib
module; Erlang 530/530; run_tests +5. Satisfies fed-sx Step 2
(rsa-sha256-2018 sig-suite).
- 2026-05-18 — Phase E: pure-OCaml `lib/sx_ed25519.ml` (minimal
base-2^26 bignum, edwards25519 extended-coord points, RFC 8032
§5.1.7 cofactorless verify reusing Phase-A sha512). Primitive
`ed25519-verify` is total (bad/short/non-string args → false).
8 tests: RFC 8032 §7.1 TEST 1-3 (re-derived independently via
python-cryptography), tampered msg/sig, wrong-length, non-string.
WASM boot green with new lib module; Erlang 530/530; run_tests +8.
Satisfies fed-sx Milestone 1 Step 2 (Ed25519 sig-suite).
- 2026-05-18 — Phase D: pure-OCaml `lib/sx_cid.ml` (unsigned-varint,
multihash, CIDv1, multibase base32-lower), primitives `cid-from-bytes`
/ `cid-from-sx` (cbor→sha2-256→mh→cidv1, dag-cbor codec 0x71). 5 tests:
raw "abc"=bafkreif2pall7d…, raw ""=bafkreihdwdcefg…, dag-cbor {}=
bafyreigbtj4x7i… (all match canonical IPFS CIDs; no `ipfs` CLI so
vectors independently derived in Python), key-order determinism. WASM
boot green with new lib module; Erlang 530/530; run_tests +5.
- 2026-05-18 — Phase C: pure-OCaml `lib/sx_cbor.ml` (dag-cbor encode/
decode), primitives `cbor-encode`/`cbor-decode`. RFC 8949 Appendix-A
vectors, length-then-bytewise key sort + order-invariance determinism,
decode∘encode round-trip (30 tests). Floats unsupported (raise, no
fed-sx shape needs them); tag-42 decode = inner-item passthrough.
WASM boot green with new lib module; Erlang 530/530; run_tests +30.
- 2026-05-18 — Phase B: pure-OCaml `lib/sx_sha3.ml` (Keccak-f[1600] +
SHA-3 pad, domain 0x06), primitive `crypto-sha3-256`. 4 NIST FIPS 202
vectors pass (empty/abc/896-bit + 1600-bit 0xa3 multi-block). WASM boot
green with new lib module; Erlang conformance 530/530; run_tests +4.
- 2026-05-18 — Phase A: pure-OCaml `lib/sx_sha2.ml` (SHA-256 + SHA-512),
primitives `crypto-sha256`/`crypto-sha512`. 7 NIST FIPS 180-4 vectors pass
(empty/abc/896-bit/1M-'a' for sha256; empty/abc/896-bit for sha512). WASM
boot green with new lib module; Erlang conformance 530/530 unchanged.
## Blockers
- _(none yet)_