#!/usr/bin/env bash # host-on-sx live server launcher. Loads the kernel stdlib, the subsystem # libraries, and the host modules into one sx_server process, then calls # (host/serve PORT ...) which binds the native http-listen server to the # Dream-shaped host app. Runs in the FOREGROUND (http-listen blocks), so this # doubles as a container entrypoint and a local launcher. # # Usage: # bash lib/host/serve.sh # serve on $HOST_PORT (default 8910) # HOST_PORT=8920 bash lib/host/serve.sh # pick a port # # The module list is kept identical to lib/host/conformance.sh so what serves is # exactly what the suites verify. set -uo pipefail # Project root: SX_PROJECT_DIR in containers (set to /app by the compose stack), # else the git toplevel for local runs. cd "${SX_PROJECT_DIR:-$(git rev-parse --show-toplevel 2>/dev/null || echo .)}" SX_SERVER="${SX_SERVER:-hosts/ocaml/_build/default/bin/sx_server.exe}" if [ ! -x "$SX_SERVER" ]; then SX_SERVER="/root/rose-ash/hosts/ocaml/_build/default/bin/sx_server.exe" fi if [ ! -x "$SX_SERVER" ]; then echo "ERROR: sx_server.exe not found." >&2 exit 1 fi PORT="${HOST_PORT:-8910}" # Modules: every load line from conformance.sh's MODULES list, minus the ledger # (not needed to serve). server.sx supplies host/serve. MODULES=( "spec/stdlib.sx" "lib/r7rs.sx" "lib/apl/runtime.sx" "lib/datalog/tokenizer.sx" "lib/datalog/parser.sx" "lib/datalog/unify.sx" "lib/datalog/db.sx" "lib/datalog/builtins.sx" "lib/datalog/aggregates.sx" "lib/datalog/strata.sx" "lib/datalog/eval.sx" "lib/datalog/api.sx" "lib/datalog/magic.sx" "lib/acl/schema.sx" "lib/acl/facts.sx" "lib/acl/engine.sx" "lib/acl/explain.sx" "lib/acl/audit.sx" "lib/acl/federation.sx" "lib/acl/api.sx" "lib/relations/schema.sx" "lib/relations/engine.sx" "lib/relations/api.sx" "lib/relations/explain.sx" "lib/relations/federation.sx" "lib/relations/tree.sx" "lib/feed/normalize.sx" "lib/feed/stream.sx" "lib/feed/api.sx" "lib/persist/event.sx" "lib/persist/backend.sx" "lib/persist/log.sx" "lib/persist/kv.sx" "lib/persist/api.sx" "lib/persist/durable.sx" "spec/render.sx" "web/adapter-html.sx" "lib/dream/types.sx" "lib/dream/json.sx" "lib/dream/auth.sx" "lib/dream/error.sx" "lib/dream/form.sx" "lib/dream/session.sx" "lib/dream/router.sx" "lib/host/handler.sx" "lib/host/middleware.sx" "lib/host/session.sx" "lib/host/auth.sx" "lib/host/sxtp.sx" "lib/host/router.sx" "lib/host/feed.sx" "lib/host/relations.sx" "lib/host/blog.sx" "lib/host/server.sx" ) # Admin login credentials + session signing secret. Override via the container # env; the in-source defaults are dev-only. The blog write routes are now GUARDED # (session login or Bearer), so these gate publishing on blog.rose-ash.com. ADMIN_USER="${SX_ADMIN_USER:-admin}" ADMIN_PASS="${SX_ADMIN_PASSWORD:-letmein}" SESSION_SECRET="${SX_SESSION_SECRET:-rose-ash-host-dev-secret-change-me}" EPOCH=1 { for M in "${MODULES[@]}"; do echo "(epoch $EPOCH)"; echo "(load \"$M\")"; EPOCH=$((EPOCH+1)) done # Point the blog at the DURABLE file backend (persists under $SX_PERSIST_DIR), # then idempotently seed a welcome post (sx_content = SX element markup, the # editor's content model). Re-seeding is a no-op if the slug already exists. echo "(epoch $EPOCH)" echo "(eval \"(host/blog-use-store! (persist/durable-backend))\")" EPOCH=$((EPOCH+1)) # Rebuild the relations graph from the durable edge store. lib/relations holds # the graph in memory only, so without this, related/tags/types vanish on every # restart even though the posts persist. echo "(epoch $EPOCH)" echo "(eval \"(host/blog-load-edges!)\")" EPOCH=$((EPOCH+1)) # Sessions on the DURABLE store, LAZILY: only a logged-in session (one that # writes a field) persists, so a login survives a restart while anonymous / # crawler traffic leaves no rows. host/session-init! bumps the per-boot epoch # that keeps sids unique across restarts. Then the signing secret + admin # credentials, and grant admin "edit" on "blog" so a logged-in session passes # the ACL gate on the write routes. echo "(epoch $EPOCH)" echo "(eval \"(host/session-use-store! (persist/durable-backend))\")" EPOCH=$((EPOCH+1)) echo "(epoch $EPOCH)" echo "(eval \"(host/session-init!)\")" EPOCH=$((EPOCH+1)) echo "(epoch $EPOCH)" echo "(eval \"(host/session-set-secret! \\\"$SESSION_SECRET\\\")\")" EPOCH=$((EPOCH+1)) echo "(epoch $EPOCH)" echo "(eval \"(host/auth-set-admin! \\\"$ADMIN_USER\\\" \\\"$ADMIN_PASS\\\")\")" EPOCH=$((EPOCH+1)) echo "(epoch $EPOCH)" echo "(eval \"(acl/load! (list (acl-grant \\\"$ADMIN_USER\\\" \\\"edit\\\" \\\"blog\\\")))\")" EPOCH=$((EPOCH+1)) # Idempotently seed a welcome post (sx_content = SX element markup, the editor's # content model). Re-seeding is a no-op if the slug already exists. echo "(epoch $EPOCH)" echo "(eval \"(host/blog-seed! \\\"welcome\\\" \\\"Welcome to the SX host\\\" \\\"(article (h1 \\\\\\\"Welcome to the SX host\\\\\\\") (p \\\\\\\"Rendered by lib/host via render-to-html, from the durable SX store.\\\\\\\"))\\\" \\\"published\\\")\")" EPOCH=$((EPOCH+1)) # Seed the root type-posts (type, tag) — types ARE posts. Idempotent. echo "(epoch $EPOCH)" echo "(eval \"(host/blog-seed-types!)\")" EPOCH=$((EPOCH+1)) echo "(epoch $EPOCH)" # Anonymous reads (feed timeline + relations container reads + blog post detail) # plus the GUARDED blog write routes: POST /new (editor form ingest), POST/PUT/ # DELETE /posts behind host/require-user (session login OR Bearer) + ACL. make-app # auto-mounts /login + /logout and wraps everything in the signed-session # middleware, so a browser logs in then publishes. The bearer resolver is a stub # (no API tokens configured) — browser session is the live auth path for now. # blog-routes LAST — its GET /:slug catch-all must not shadow the rest. echo "(eval \"(host/serve $PORT (list host/feed-routes host/relations-routes (host/blog-write-routes (fn (tok) nil)) host/blog-routes))\")" } | exec "$SX_SERVER"