Files
rose-ash/plans/host-on-sx.md
giles d5a1c8370c host: Phase 1 — router + handler + GET /feed endpoint on Dream, 28/28
First migrated endpoint onto the SX host. lib/host is a thin wiring layer:
a host handler is a Dream handler (request->response) that calls a subsystem
public API and serialises via a shared JSON envelope.

- handler.sx: host/ok, host/ok-status, host/error, host/json-status (Dream's
  dream-json is 200-only), host/query-int
- router.sx: host/make-app assembles per-domain route groups + /health probe
  into one dream-router (reuses dr/flatten-routes)
- feed.sx: GET /feed reads feed/all + stream combinators, recent-first, with
  ?actor= filter and ?limit= cap
- 3 test suites incl. a golden test (body == subsystem recent stream + envelope)
- conformance.sh mirrors lib/dream's runner

Builds on dream-on-sx (merged, gate green 480/480) rather than a throwaway
native request model; collapses most of plan Phase 4 into Phase 1.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-07 19:36:55 +00:00

132 lines
7.6 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.
# host-on-sx: The SX web host — off Quart, onto the kernel (Dream-bound)
> **DRAFT outline.** The integration boundary that turns the subsystem libraries
> into running services, and the strangler path off Python/Quart. This is the
> dependency hub — it imports every subsystem. Decision recorded below: native
> server + SXTP **now**, `dream-on-sx` framework layer **next**, Python only at the
> external-integration edges.
The subsystems (`feed`, `search`, `acl`, `mod`, `flow`, `commerce`, `identity`,
`content`, `events`) are libraries. Something has to receive an HTTP request, route
it, call the right subsystem, and serialize the response. Today that's Python/Quart
— the one large non-SX component in the stack: separate runtime, deploy, and
failure mode. The goal is to move the web/host/domain layer onto the SX substrate
and retire Quart, **incrementally (strangler-fig), never big-bang.**
This is already underway: a native OCaml HTTP server is live in prod on
`sx.rose-ash.com` (~3ms cached, ~323 req/s, ~2MB RSS), `defhandler`/`defpage`
exist, and a partial **SXTP** protocol is specced. That is the unblocked near-term
host — no `ocaml-on-sx` dependency.
## Two layers, two timelines
1. **Now (unblocked): native server + SXTP adapter + SX handlers.** Route rose-ash
endpoints onto the SX host one at a time. Each migrated endpoint is an SX
handler dispatching to a subsystem; Quart proxies the rest until cut over.
2. **Next: `dream-on-sx` as the framework layer.** Dream gives Quart-grade
ergonomics — typed routing, middleware stacks, sessions, CSRF. It is gated on
`ocaml-on-sx` Phases 15 + minimal stdlib. **This plan is the concrete target
user that un-parks `dream-on-sx`** (see `plans/dream-on-sx.md`): "the subsystems
need an HTTP front door" is the real feature pulling Dream. Until then, do not
block migration on Dream — the native server is sufficient.
3. **Always: Python only at the edges.** External integrations — SumUp payments,
Ghost CMS, ActivityPub crypto, IPFS/Kubo — ride Python libraries today. They
stay as thin injected adapters (Python/FFI) behind subsystem interfaces until
native replacements exist. "Drop Quart" ≠ "drop every line of Python."
## Status (rolling)
`bash lib/host/conformance.sh`**28/28** (3 suites: handler, router, feed). Phase 1 DONE.
## Ground rules
- **Scope:** `lib/host/**` and `plans/host-on-sx.md`. May **import** every subsystem
+ the kernel's server/SXTP surface. Do **not** edit `spec/`, `hosts/`, `shared/`,
or subsystem internals — wire to their public APIs only. Host-primitive / server
changes belong in `hosts/` (out of scope) → Blockers.
- **Architecture:** a route maps (method, path) → handler; a handler is an SX fn
`request -> response` that calls subsystem APIs; middleware is composed handlers
(auth via `identity`, permission via `acl`, mute via subsystem prefs). SXTP is the
wire format between host and subsystem-as-service.
- **Migration discipline:** each endpoint moved must be behavior-equivalent to its
Quart original (golden-response test before flip). Keep a migration ledger.
- **Commits:** one feature per commit. Progress log + tick boxes.
## Architecture sketch
```
HTTP request HTTP response
│ ▲
▼ │
native OCaml http server (prod) ──────► lib/host/router.sx
(hosts/ — out of scope) — (method,path) → handler
│ ▲
▼ │
lib/host/middleware.sx lib/host/handler.sx
— auth(identity) ∘ acl ∘ mute ∘ ... — request → subsystem call → response
│ ▲
▼ │
lib/host/sxtp.sx subsystem APIs (feed/search/commerce/…)
— wire format, host↔service — called via public interfaces
└── external edges: SumUp / Ghost / AP / IPFS → injected Python/FFI adapters
```
## Phase 1 — Router + handler + one real endpoint
- [x] `router.sx``host/make-app` assembles per-domain route groups + a built-in
`/health` probe into one Dream router (reuses Dream's `dr/flatten-routes`)
- [x] `handler.sx` — JSON envelope (`host/ok`/`host/ok-status`/`host/error`),
status-carrying `host/json-status` (Dream's `dream-json` is 200-only), and
`host/query-int`. A host handler IS a Dream handler (request -> response).
- [x] migrate ONE read endpoint: `GET /feed` (`lib/host/feed.sx`) reads
`feed/all` + stream combinators, serialises recent-first; `?actor=` filter,
`?limit=` cap. Golden test asserts body == subsystem recent stream + envelope.
- [x] `conformance.sh` (mirrors `lib/dream`'s runner) — 28/28
## Phase 2 — Middleware + SXTP
- [ ] `middleware.sx` — composable auth/acl/mute/error layers
- [ ] `sxtp.sx` — host↔subsystem wire format (align with existing spec)
- [ ] migrate a write endpoint (auth + permission + action)
## Phase 3 — Strangler migration ledger
- [ ] enumerate Quart endpoints; track migrated vs proxied
- [ ] golden-response harness vs the live Quart responses
- [ ] cut over a whole domain (smallest: `likes` or `relations`) as proof
## Phase 4 — Dream framework layer (gated)
- [ ] gate: `ocaml-on-sx` Phases 15 + minimal stdlib green
- [ ] adopt `dream-on-sx` routing/middleware/session ergonomics over the same handlers
- [ ] re-home external adapters as native where replacements land
## Progress log
- **Phase 1 (DONE, 28/28).** `lib/host/{handler,router,feed}.sx` + three test
suites + `conformance.sh`. The host is a thin wiring layer: a host handler is a
Dream handler that calls a subsystem public API and serialises the result via a
shared JSON envelope. First migrated endpoint: `GET /feed`.
- **Decision — build on Dream from Phase 1, not a throwaway native model.** The
plan front-matter gated Dream to Phase 4, but `dream-on-sx` is merged
(commit fe958bda) and its gate (`ocaml-on-sx` P15+P6) is green (480/480), so
reinventing request/response + routing would be pure duplication. Host reuses
Dream's `types.sx` (request/response dicts), `json.sx` (encode), and
`router.sx` (`dream-router`/`dream-get`/`dr/flatten-routes`). Phase 4's
"adopt Dream ergonomics" is therefore largely already satisfied; what remains
for Phase 4 is the live wiring against the real OCaml HTTP server + session.
- The OCaml server handing a `dream-request`-shaped dict to SX handlers is a
`hosts/` change (out of scope) — tracked under Blockers as the eventual
live-wiring step. For now the host layer is exercised purely via conformance.
## Blockers
- **Live wiring to the native OCaml HTTP server** (Phase 3/4): the prod server in
`hosts/` must hand SX handlers a `dream-request` dict and serialise the returned
`dream-response`. That is a `hosts/` change (out of scope for this loop, which is
`lib/host/**` only). Until then, endpoints are verified via `conformance.sh`, not
HTTP. Not blocking Phase 2 (middleware + SXTP + a write endpoint).
- **Worktree tooling:** in this `loops/host` worktree every sx-tree *write* tool
(`sx_write_file`, `sx_replace_node`, …) raises `yojson "Expected string, got
null"` at the MCP layer — same class as the `loops/dream` worktree gotcha, but
here even `sx_write_file` fails. Read-side sx-tree tools work. New `.sx` files
were created with the `Write` tool (the .sx hook is inactive in this worktree)
and each validated afterwards with `sx_validate` to keep the parse guarantee.