bend: dual-port worker (8320 wire + 8321 http) — playground onramp
Each gpu-worker.lsp now listens on both wire-TCP (existing :8320) and HTTP/1.1+CORS (new :8321), sharing one handle-request dispatcher. Lets a tab on https://lumbda.com/playground/ POST to its own machine via http://localhost:8321/ — browsers permit localhost from HTTPS origins without TLS, so no proxy, no cert, no fox-owned infra required for the decentralized run-your-own-bend story. main() forks at startup: child runs http-run-loop on :8321, parent keeps existing run-loop on :8320. Adding a new op-head to handle-request exposes it over both transports automatically. Binary modes (BSHK/BCGB/BSCP/BSRT/BSB3) stay wire-only — they exist for native callers who already cache the binary locally; browser callers send S-expression recipes the worker dispatches the same way. Two latent defects fixed to make CPU-only and Python-tier hosts work: - vram-used-mib now file-exists? guards /usr/bin/nvidia-smi. Python tier's spawn-process-stdio raises FileNotFoundError on missing binary, not returning #f as the prior code expected, which crashed every worker on a CPU-only laptop. - fork-self return discriminated via (number? pid) not (eq? pid 0). Python tier's (eq? 0 #f) returns #t because == conflates int 0 with bool False; pre-existing run-loop has the same risk but C/asm tier (identity eq?) masks it for the production case. Phase 2 (server-side factory ops: compile uploaded .lsp recipes into .bin before bending — the foxhop champion-circuit workflow) deferred until authentication lands; today a worker on the public internet would let any caller occupy our GPU. Operational Caddy + DNS proposals in plans/bend-http-deploy.md cover the personal-remote-access endpoint chain (proxy.unturf.com edge → ai.foxhop.net Caddy → 3090-ai:8321) gated by trusted-IP allowlist — applied separately. Also codifies the playground "CSS Grid only, never flexbox" rule in CLAUDE.md: all www/ and wasm/ stylesheets are already grid-only; documenting the invariant so future edits don't drift. Tests: smoke-bend-http.sh — (ping)→(ok pong), unknown-op fallback, OPTIONS CORS preflight — all PASS. Wire path unchanged, verified round-trip via 8-digit-prefix framing.
This commit is contained in:
parent
e34fb1f7dc
commit
88e16c0ce2
8 changed files with 771 additions and 30 deletions
71
CLAUDE.md
71
CLAUDE.md
|
|
@ -35,6 +35,48 @@ Then ask fox about our mission.
|
|||
- Every implementation (Python, C, GNU asm) carries a dedicated architecture diagram
|
||||
- When explaining architecture, draft or reference a dot diagram first
|
||||
|
||||
## Web styling — CSS Grid only, never flexbox
|
||||
|
||||
Every page in `www/` and `wasm/` (homepage, whitepaper, playground,
|
||||
REPL, bend demo, 404) lays out multi-child regions with **CSS Grid**.
|
||||
Flexbox is **banned** as a layout primitive. One layout language across
|
||||
every page; no mode-switching in our head while we read or edit.
|
||||
|
||||
**Forbidden** anywhere in our CSS (source or generated):
|
||||
|
||||
- `display: flex`, `display: inline-flex`
|
||||
- `flex-direction`, `flex-wrap`, `flex-flow`
|
||||
- `flex-grow`, `flex-shrink`, `flex-basis`, shorthand `flex:`
|
||||
- `order` (use grid-area / source order instead)
|
||||
|
||||
**Allowed** (works for grid too — keep on grid containers only):
|
||||
|
||||
- `gap`, `row-gap`, `column-gap`
|
||||
- `align-items`, `justify-items`, `place-items`
|
||||
- `align-content`, `justify-content`, `place-content`
|
||||
- `align-self`, `justify-self`, `place-self`
|
||||
|
||||
**Patterns that look like they need flex but don't:**
|
||||
|
||||
- Horizontal toolbar → `display: grid; grid-auto-flow: column; gap: …`
|
||||
- Tab row → same as above; sticky positioning composes fine
|
||||
- Centered single child → `display: grid; place-items: center`
|
||||
- Sidebar + main → `display: grid; grid-template-columns: auto 1fr`
|
||||
- Wrapping chip cloud → `display: grid; grid-template-columns: repeat(auto-fit, minmax(N, max-content))`
|
||||
|
||||
Every source CSS file under `www/` and `wasm/{app,repl,dist,dist-repl}/`
|
||||
opens with the banner `/* No flexbox. Every multi-child layout uses
|
||||
CSS Grid. */`. Keep that banner intact when editing; add it when
|
||||
introducing a new stylesheet.
|
||||
|
||||
**Audit before commit** when CSS changed:
|
||||
|
||||
```bash
|
||||
grep -rn 'display:[[:space:]]*\(inline-\)\?flex\|flex-direction\|flex-wrap\|flex-grow\|flex-shrink\|flex-basis' www/ wasm/ && exit 1 || echo "grid-only OK"
|
||||
```
|
||||
|
||||
A hit fails our audit. Convert to grid before committing.
|
||||
|
||||
## Implementations
|
||||
|
||||
| Impl | Path | Build | Test | REPL |
|
||||
|
|
@ -48,12 +90,18 @@ Then ask fox about our mission.
|
|||
|
||||
`examples/cuda-fanout/` ships `(bend ...)` — runtime decides per call
|
||||
whether to evaluate locally or ship to a CUDA worker over our wire
|
||||
protocol. Two wire modes:
|
||||
protocol. **Each worker listens on two ports out of the box:**
|
||||
|
||||
- **S-expression mode** (text) — for small payloads. Slow above ~1k
|
||||
inputs because parser cost dominates.
|
||||
- **Binary mode** (magic `BSHK` + raw bytes) — for huge payloads. 150x
|
||||
faster than S-exp at 1M inputs; bends past host hashlib by 12x.
|
||||
- **8320 — wire-TCP** (length-prefixed S-expressions + binary magic
|
||||
`BSHK`/`BCGB`/`BSCP`/`BSRT`/`BSB3` blobs). The native path: used
|
||||
by `bend.lsp`, asm clients, and anything that can open a raw
|
||||
socket. Fastest. Binary mode is 150× faster than S-exp at 1M inputs.
|
||||
- **8321 — HTTP/1.1 + CORS** (POST body is the S-expression, response
|
||||
body is the result text). The browser path: lets a tab on
|
||||
`https://lumbda.com/playground/` POST to `http://localhost:8321/`
|
||||
via the browser's localhost-exception (no TLS required, no proxy
|
||||
needed). Same `handle-request` dispatcher fires on both ports, so
|
||||
every op-head ships once and lights up on both transports.
|
||||
|
||||
Workers run on any tier (`make gpu-worker LUMBDA={c,python,asm}`).
|
||||
C tier ~9x faster than Python on small calls; binary mode equalizes
|
||||
|
|
@ -67,6 +115,19 @@ to dispatch real-scale candidate scoring to a GPU worker. The Phase B
|
|||
1-8 secp256k1 arithmetic landed on the foxhop side this session, so
|
||||
the substrate has every piece it needs.
|
||||
|
||||
**Phase 2 (deferred until auth lands):** server-side factory ops
|
||||
that take an `.lsp` recipe via HTTP, compile a `.bin` on the worker
|
||||
filesystem, then bend it. Killer use case: upload a foxhop champion
|
||||
ECDSA circuit recipe from the playground/REPL and get the result
|
||||
back from a GPU cluster. Blocked on authentication — today a worker
|
||||
without auth would let any caller occupy our GPU.
|
||||
|
||||
**Run your own bend** (the decentralized story we encourage): any
|
||||
user runs `make gpu-worker LUMBDA=python` on their machine and
|
||||
pastes `http://localhost:8321/` into the playground bend field.
|
||||
CPU works (kernels fall back to host execution); GPU faster. No
|
||||
fox-owned infra required.
|
||||
|
||||
## Test Suites
|
||||
|
||||
- Python unit/integration: `tests.py` (571 tests)
|
||||
|
|
|
|||
|
|
@ -94,5 +94,11 @@ test: shake256-fanout test_roundtrip.py
|
|||
bench: shake256-fanout bench.py
|
||||
python3 bench.py ./shake256-fanout
|
||||
|
||||
# Dual-port smoke: gpu-worker.lsp boots on test wire+http port pair,
|
||||
# curl POSTs (ping) + OPTIONS preflight, asserts response + CORS.
|
||||
# No CUDA deps; runs anywhere lumbda runs.
|
||||
smoke-bend-http: gpu-worker.lsp http-listener.lsp smoke-bend-http.sh
|
||||
./smoke-bend-http.sh
|
||||
|
||||
clean:
|
||||
rm -f shake256-fanout /tmp/cf-in.portal /tmp/cf-out.portal
|
||||
|
|
|
|||
|
|
@ -90,6 +90,7 @@ cd examples/cuda-fanout
|
|||
python3 -u ../../lumbda.py /tmp/launch-worker.lsp
|
||||
# → gpu-worker: ready cuda-shake-fanout ← ./shake256-fanout
|
||||
# → gpu-worker listening on port 8320 (BEND mnemonic — see below)
|
||||
# → gpu-worker HTTP listening on port 8321 (browser path — see below)
|
||||
|
||||
# In another shell, drive via bend!:
|
||||
python3 ../../lumbda.py smoke-bend.lsp
|
||||
|
|
@ -176,9 +177,45 @@ rest of the repo.
|
|||
make all # compile via nvcc
|
||||
make test # round-trip vs hashlib.shake_256
|
||||
make bench # device vs host timing grid
|
||||
make smoke-bend-http # HTTP/8321 path: ping/CORS, no CUDA
|
||||
python3 bench_daemon.py ./shake256-fanout # daemon vs per-spawn
|
||||
```
|
||||
|
||||
## HTTP path — browser callers (port 8321)
|
||||
|
||||
`gpu-worker.lsp` listens on **two** ports out of the box:
|
||||
|
||||
- `8320` — wire-TCP (length-prefixed S-exp + binary `BSHK`/`BCGB`/
|
||||
`BSCP`/`BSRT`/`BSB3` modes). Used by native callers (`bend.lsp`,
|
||||
asm clients).
|
||||
- `8321` — HTTP/1.1 + CORS (POST body is the S-expression, response
|
||||
body is the result text). Used by browser callers — the
|
||||
`https://lumbda.com/playground/` bend-url field POSTs here via
|
||||
XHR. Browsers can't open raw TCP, but the browser localhost
|
||||
exception permits HTTP from an HTTPS origin without TLS.
|
||||
|
||||
Same `handle-request` dispatcher fires on both ports. Adding a new
|
||||
op-head (e.g. a server-side factory recipe in Phase 2) exposes it on
|
||||
both transports automatically.
|
||||
|
||||
```bash
|
||||
# Smoke test — boots the worker on test ports, asserts ping + CORS.
|
||||
make smoke-bend-http
|
||||
|
||||
# Hit the HTTP port directly:
|
||||
curl -sS -X POST -H 'Content-Type: text/plain' \
|
||||
--data-binary '(ping)' http://127.0.0.1:8321/
|
||||
# → (ok pong)
|
||||
```
|
||||
|
||||
Binary modes (`BSHK` etc.) are NOT exposed over HTTP — they exist
|
||||
for native callers who already have the binary cached locally.
|
||||
Browser callers either use S-expression op-heads today (`ping`,
|
||||
`echo`, `cuda-shake-fanout` with inline inputs, `cuda-sim-ops-bin`
|
||||
with a path the worker can read) or, once Phase 2 lands (gated on
|
||||
auth), upload an `.lsp` recipe that the server compiles into a
|
||||
`.bin` before bending it.
|
||||
|
||||
## Measured performance — where the GPU actually wins (and loses)
|
||||
|
||||
**Setup:** RTX 3090 (10,496 CUDA cores, sm_86) + i9-12900K host,
|
||||
|
|
|
|||
|
|
@ -29,8 +29,15 @@
|
|||
;;; Requires the cuda binaries on disk; paths below.
|
||||
|
||||
(load "wire.lsp")
|
||||
(load "http-listener.lsp")
|
||||
|
||||
(define *worker-port* 8320)
|
||||
;; HTTP listener port — defaults to wire-port + 1 so the same worker
|
||||
;; serves both the native (raw wire frames) and browser (HTTP POST)
|
||||
;; entry points. Browser callers (e.g. www/playground bend-url field)
|
||||
;; can't open raw TCP, only HTTP/WebSocket, so the HTTP path closes
|
||||
;; the on-ramp gap. Same handle-request dispatcher fires for both.
|
||||
(define *worker-http-port* 8321)
|
||||
(define *binary-shake-fanout*
|
||||
;; Override via env or per host.
|
||||
"./shake256-fanout")
|
||||
|
|
@ -82,6 +89,16 @@
|
|||
(or (string->number (car (cdr rest))) *worker-port*))
|
||||
(else (loop (cdr rest))))))
|
||||
|
||||
;; HTTP port defaults to wire-port + 1. Explicit override via --http-port.
|
||||
(define (parse-http-port-arg args default)
|
||||
(let loop ((rest args))
|
||||
(cond
|
||||
((null? rest) default)
|
||||
((null? (cdr rest)) default)
|
||||
((and (string? (car rest)) (string=? (car rest) "--http-port"))
|
||||
(or (string->number (car (cdr rest))) default))
|
||||
(else (loop (cdr rest))))))
|
||||
|
||||
(define *daemons* '()) ; alist (op-name . (stdin-port . stdout-port))
|
||||
|
||||
;;; -- daemon pool (per-tier stubs marked) -----------------------
|
||||
|
|
@ -707,9 +724,16 @@
|
|||
;; record-cell-vram! in child still updates running max if larger seen.
|
||||
(define *vram-per-cell-max-mib* 2500)
|
||||
|
||||
;; Fail-open on hosts without nvidia-smi (CPU-only users running their
|
||||
;; own bend from a laptop). Python tier's spawn-process-stdio raises
|
||||
;; FileNotFoundError on a missing binary instead of returning #f, which
|
||||
;; would crash the worker; check file-exists? before the spawn.
|
||||
(define (vram-used-mib)
|
||||
;; nvidia-smi --query-gpu=memory.used --format=csv,noheader,nounits
|
||||
;; returns one integer line per GPU; sum if multi-GPU host.
|
||||
(cond
|
||||
((not (file-exists? "/usr/bin/nvidia-smi")) 0)
|
||||
(else
|
||||
(let* ((pair (spawn-process-stdio
|
||||
"/usr/bin/nvidia-smi"
|
||||
'("--query-gpu=memory.used"
|
||||
|
|
@ -724,7 +748,7 @@
|
|||
((eof-object? line) (close-port (cdr pair)) sum)
|
||||
(else
|
||||
(let ((n (string->number (string-trim line))))
|
||||
(loop (+ sum (cond (n n) (else 0)))))))))))))
|
||||
(loop (+ sum (cond (n n) (else 0)))))))))))))))
|
||||
|
||||
(define (string-trim s)
|
||||
;; trim leading + trailing whitespace.
|
||||
|
|
@ -858,6 +882,78 @@
|
|||
(wire-send client resp)
|
||||
(tcp-close client))))))
|
||||
|
||||
;;; -- HTTP path -------------------------------------------------
|
||||
;;;
|
||||
;;; Mirrors the TCP path's fork-per-accept model so concurrent HTTP
|
||||
;;; clients run isolated children with the same VRAM admission
|
||||
;;; gating as wire-frame clients. handle-http-client decodes the
|
||||
;;; HTTP request, dispatches the body S-expression through the
|
||||
;;; SAME handle-request dispatcher used by the TCP else-branch, and
|
||||
;;; writes an HTTP/1.1 response with CORS headers so the browser
|
||||
;;; playground (https://lumbda.com/playground/) can POST directly
|
||||
;;; to a worker running on the user's own machine via
|
||||
;;; http://localhost:8321/ (browsers permit localhost without TLS).
|
||||
;;;
|
||||
;;; Binary-mode forms (BSHK/BCGB/BSCP/BSRT/BSB3) are NOT exposed
|
||||
;;; over HTTP — those exist for native callers who already have
|
||||
;;; the binary cached locally. HTTP callers either use S-expression
|
||||
;;; forms (echo / ping / cuda-shake-fanout / cuda-sim-ops-bin with
|
||||
;;; an on-disk path) or, once Phase 2 lands, server-side factory
|
||||
;;; ops that compile recipes into binaries before bending them.
|
||||
|
||||
(define (handle-http-client client)
|
||||
(let ((req (read-http-request client)))
|
||||
(cond
|
||||
((eq? req #f)
|
||||
(tcp-close client))
|
||||
(else
|
||||
(let ((method (car req))
|
||||
(body (car (cdr (cdr req)))))
|
||||
(cond
|
||||
((string=? method "OPTIONS")
|
||||
(write-http-options-response client)
|
||||
(tcp-close client))
|
||||
((string=? method "POST")
|
||||
(let* ((sexp (read-from-string body))
|
||||
(resp (handle-request sexp))
|
||||
(resp-text (write-to-string resp)))
|
||||
(write-http-response client 200 resp-text
|
||||
"text/plain; charset=utf-8")
|
||||
(tcp-close client)))
|
||||
(else
|
||||
(write-http-response client 405
|
||||
"Only POST and OPTIONS are supported.\n"
|
||||
"text/plain; charset=utf-8")
|
||||
(tcp-close client))))))))
|
||||
|
||||
;; NOTE — fork-self discrimination:
|
||||
;; Python tier's (eq? 0 #f) returns #t (== conflates int 0 with bool
|
||||
;; False). C tier (eq? identity) returns #f correctly. To stay portable
|
||||
;; we use (number? pid) to distinguish "fork failed (#f)" from "child
|
||||
;; (numeric 0)". Don't switch to (eq? pid 0) here — works on C/asm but
|
||||
;; runs the failed-fork branch in every Python-tier child.
|
||||
(define (http-run-loop server)
|
||||
(waitpid-nonblock)
|
||||
(wait-admit *vram-per-cell-max-mib*)
|
||||
(let ((client (tcp-accept server)))
|
||||
(cond
|
||||
((eq? client #f) (http-run-loop server))
|
||||
(else
|
||||
(let ((pid (fork-self)))
|
||||
(cond
|
||||
((not (number? pid))
|
||||
;; fork failed — fall back to serial handle
|
||||
(handle-http-client client)
|
||||
(http-run-loop server))
|
||||
((= pid 0)
|
||||
;; child: handle one HTTP client, then exit
|
||||
(handle-http-client client)
|
||||
(exit 0))
|
||||
(else
|
||||
;; parent: close our copy of client fd, loop to accept
|
||||
(tcp-close client)
|
||||
(http-run-loop server))))))))
|
||||
|
||||
;; Optional registration — only spawn the daemon when its binary is
|
||||
;; reachable. Lets a worker host serve a subset of forms without
|
||||
;; failing to start because some bend form's daemon isn't installed.
|
||||
|
|
@ -878,9 +974,19 @@
|
|||
;; lands (see plans/form-A-day4-progress.md).
|
||||
(define *secp-daemon-extra-args* '("--window-w" "4"))
|
||||
|
||||
;; Dual-port main — wire-TCP on *worker-port* (default 8320), HTTP on
|
||||
;; *worker-http-port* (default port+1 = 8321). One fork at startup
|
||||
;; splits the process: child runs http-run-loop, parent runs the
|
||||
;; existing run-loop. Same handle-request dispatcher fires for both.
|
||||
;;
|
||||
;; If the HTTP listener fails (port in use, etc.), the worker falls
|
||||
;; back to wire-only — the wire path stays the source-of-truth and
|
||||
;; the HTTP path is a convenience for browser callers.
|
||||
(define (main)
|
||||
(let ((port (parse-port-arg *argv*)))
|
||||
(let* ((port (parse-port-arg *argv*))
|
||||
(http-port (parse-http-port-arg *argv* (+ port 1))))
|
||||
(set! *worker-port* port)
|
||||
(set! *worker-http-port* http-port)
|
||||
(maybe-register-daemon! 'cuda-shake-fanout *binary-shake-fanout*)
|
||||
(maybe-register-daemon! 'cuda-bignum-cgbn *binary-cgbn-batch*)
|
||||
(maybe-register-daemon! 'cuda-secp256k1-batched-mul
|
||||
|
|
@ -888,15 +994,42 @@
|
|||
*secp-daemon-extra-args*)
|
||||
(maybe-register-daemon! 'cuda-radix-sort *binary-radix-sort*)
|
||||
(maybe-register-daemon! 'cuda-blake3-tree *binary-blake3-fanout*)
|
||||
(let ((server (tcp-listen port)))
|
||||
(let ((wire-server (tcp-listen port))
|
||||
(http-server (tcp-listen http-port)))
|
||||
(cond
|
||||
((eq? server #f)
|
||||
(display ";;; ERROR -- tcp-listen failed on port ")
|
||||
((eq? wire-server #f)
|
||||
(display ";;; ERROR -- tcp-listen failed on wire port ")
|
||||
(display port) (newline))
|
||||
(else
|
||||
((eq? http-server #f)
|
||||
(display ";;; WARN -- tcp-listen failed on http port ")
|
||||
(display http-port) (display "; running wire-only") (newline)
|
||||
(display "gpu-worker listening on port ")
|
||||
(display port) (newline)
|
||||
(run-loop server))))))
|
||||
(run-loop wire-server))
|
||||
(else
|
||||
;; See note above http-run-loop: use (number? pid) not
|
||||
;; (eq? pid 0) to discriminate Python-tier's int-0 from #f.
|
||||
(let ((http-pid (fork-self)))
|
||||
(cond
|
||||
((not (number? http-pid))
|
||||
(display ";;; WARN -- fork-self failed; running wire-only")
|
||||
(newline)
|
||||
(tcp-close http-server)
|
||||
(display "gpu-worker listening on port ")
|
||||
(display port) (newline)
|
||||
(run-loop wire-server))
|
||||
((= http-pid 0)
|
||||
;; child: HTTP loop
|
||||
(tcp-close wire-server)
|
||||
(display "gpu-worker HTTP listening on port ")
|
||||
(display http-port) (newline)
|
||||
(http-run-loop http-server))
|
||||
(else
|
||||
;; parent: wire-TCP loop (existing behavior)
|
||||
(tcp-close http-server)
|
||||
(display "gpu-worker listening on port ")
|
||||
(display port) (newline)
|
||||
(run-loop wire-server)))))))))
|
||||
|
||||
;; (main) ; uncomment to run; needs the cuda-shake-fanout binary
|
||||
;; on disk + spawn-process-stdio primitive per tier
|
||||
|
|
|
|||
194
examples/cuda-fanout/http-listener.lsp
Normal file
194
examples/cuda-fanout/http-listener.lsp
Normal file
|
|
@ -0,0 +1,194 @@
|
|||
;;; http-listener.lsp -- minimal HTTP/1.1 request/response on top of
|
||||
;;; tcp-recv / tcp-send. Used by gpu-worker.lsp to expose its
|
||||
;;; handle-request dispatcher on a second port so browser callers
|
||||
;;; (XHR POST from the playground) can hit bend without speaking
|
||||
;;; the raw wire framing.
|
||||
;;;
|
||||
;;; Scope: just enough HTTP for our use case --
|
||||
;;; - POST <body> -> dispatch body as S-expression
|
||||
;;; - OPTIONS -> 204 + CORS preflight
|
||||
;;; - Connection: close after every response (no keep-alive)
|
||||
;;; - Content-Length is the only body-framing we understand
|
||||
;;; (no chunked encoding; the playground client doesn't use it)
|
||||
;;;
|
||||
;;; Portable across all three tiers: uses only tcp-recv / tcp-send /
|
||||
;;; substring / string=? / string-length / string-append /
|
||||
;;; string->number / number->string. Avoids string-contains and
|
||||
;;; string-index, which the asm tier doesn't ship.
|
||||
;;;
|
||||
;;; License: AGPLv3 (matches lumbda).
|
||||
|
||||
;;; -- portable substring search ---------------------------------
|
||||
|
||||
;; find-substring haystack needle start -> index of first match at >= start,
|
||||
;; or -1 if not found. O(n*m) — fine for HTTP header parsing.
|
||||
(define (find-substring haystack needle start)
|
||||
(let ((hlen (string-length haystack))
|
||||
(nlen (string-length needle)))
|
||||
(cond
|
||||
((= nlen 0) start)
|
||||
((> (+ start nlen) hlen) -1)
|
||||
(else
|
||||
(let loop ((i start))
|
||||
(cond
|
||||
((> (+ i nlen) hlen) -1)
|
||||
((string=? (substring haystack i (+ i nlen)) needle) i)
|
||||
(else (loop (+ i 1)))))))))
|
||||
|
||||
;; skip-spaces s i end -> advance i past ASCII space and tab chars.
|
||||
(define (skip-spaces s i end)
|
||||
(cond
|
||||
((>= i end) end)
|
||||
((or (string=? (substring s i (+ i 1)) " ")
|
||||
(string=? (substring s i (+ i 1)) "\t"))
|
||||
(skip-spaces s (+ i 1) end))
|
||||
(else i)))
|
||||
|
||||
;;; -- request-line parsers --------------------------------------
|
||||
|
||||
;; "POST /path HTTP/1.1" -> "POST"
|
||||
(define (request-line-method line)
|
||||
(let ((sp (find-substring line " " 0)))
|
||||
(cond
|
||||
((< sp 0) line)
|
||||
(else (substring line 0 sp)))))
|
||||
|
||||
;; "POST /path HTTP/1.1" -> "/path"
|
||||
(define (request-line-path line)
|
||||
(let ((sp1 (find-substring line " " 0)))
|
||||
(cond
|
||||
((< sp1 0) "/")
|
||||
(else
|
||||
(let ((sp2 (find-substring line " " (+ sp1 1))))
|
||||
(cond
|
||||
((< sp2 0) (substring line (+ sp1 1) (string-length line)))
|
||||
(else (substring line (+ sp1 1) sp2))))))))
|
||||
|
||||
;;; -- Content-Length scan ---------------------------------------
|
||||
|
||||
;; Accepts either capitalized "Content-Length:" or lowercase
|
||||
;; "content-length:" — the only two casings browsers and curl send.
|
||||
(define (find-content-length-idx headers-str)
|
||||
(let ((a (find-substring headers-str "Content-Length:" 0))
|
||||
(b (find-substring headers-str "content-length:" 0)))
|
||||
(cond
|
||||
((and (>= a 0) (>= b 0)) (cond ((< a b) a) (else b)))
|
||||
((>= a 0) a)
|
||||
((>= b 0) b)
|
||||
(else -1))))
|
||||
|
||||
(define (find-content-length headers-str)
|
||||
(let ((idx (find-content-length-idx headers-str)))
|
||||
(cond
|
||||
((< idx 0) 0)
|
||||
(else
|
||||
(let* ((line-end (find-substring headers-str "\r\n" idx))
|
||||
(eol (cond ((< line-end 0) (string-length headers-str))
|
||||
(else line-end)))
|
||||
(colon (find-substring headers-str ":" idx))
|
||||
(val-start (cond ((or (< colon 0) (>= colon eol)) eol)
|
||||
(else (+ colon 1))))
|
||||
(trimmed (skip-spaces headers-str val-start eol))
|
||||
(val (substring headers-str trimmed eol))
|
||||
(n (string->number val)))
|
||||
(cond ((eq? n #f) 0)
|
||||
((< n 0) 0)
|
||||
(else n)))))))
|
||||
|
||||
;;; -- request reader --------------------------------------------
|
||||
|
||||
;; read-http-request sock
|
||||
;; -> (list method path body) on success
|
||||
;; -> #f on malformed / closed
|
||||
;;
|
||||
;; Loops tcp-recv until we have the full header block (terminator
|
||||
;; "\r\n\r\n"), parses request-line + Content-Length, then reads
|
||||
;; the body (Content-Length bytes; may already be partly in the
|
||||
;; recv buffer past the header terminator).
|
||||
;;
|
||||
;; Caps headers at 64 KiB to refuse oversized payloads — same cap
|
||||
;; as nginx default. Body is capped only by Content-Length itself.
|
||||
(define *http-max-header-bytes* 65536)
|
||||
|
||||
(define (read-http-request sock)
|
||||
(let loop ((acc ""))
|
||||
(cond
|
||||
((>= (string-length acc) *http-max-header-bytes*) #f)
|
||||
(else
|
||||
(let ((sep (find-substring acc "\r\n\r\n" 0)))
|
||||
(cond
|
||||
((>= sep 0) (parse-http-request-with-body acc sep sock))
|
||||
(else
|
||||
(let ((chunk (tcp-recv sock 4096)))
|
||||
(cond
|
||||
((eq? chunk #f) #f)
|
||||
((= (string-length chunk) 0) #f)
|
||||
(else (loop (string-append acc chunk))))))))))))
|
||||
|
||||
(define (parse-http-request-with-body acc sep sock)
|
||||
(let* ((headers-str (substring acc 0 sep))
|
||||
(body-prefix (substring acc (+ sep 4) (string-length acc)))
|
||||
(req-line-end (find-substring headers-str "\r\n" 0))
|
||||
(req-line (cond ((>= req-line-end 0) (substring headers-str 0 req-line-end))
|
||||
(else headers-str)))
|
||||
(method (request-line-method req-line))
|
||||
(path (request-line-path req-line))
|
||||
(clen (find-content-length headers-str)))
|
||||
(cond
|
||||
((= clen 0) (list method path ""))
|
||||
(else
|
||||
(let ((have (string-length body-prefix)))
|
||||
(cond
|
||||
((>= have clen)
|
||||
(list method path (substring body-prefix 0 clen)))
|
||||
(else
|
||||
(let ((more (recv-exact sock (- clen have))))
|
||||
(cond
|
||||
((eq? more #f) #f)
|
||||
(else (list method path (string-append body-prefix more))))))))))))
|
||||
|
||||
;;; -- response writer -------------------------------------------
|
||||
|
||||
(define *http-cors-headers*
|
||||
(string-append
|
||||
"Access-Control-Allow-Origin: *\r\n"
|
||||
"Access-Control-Allow-Methods: POST, OPTIONS\r\n"
|
||||
"Access-Control-Allow-Headers: Content-Type\r\n"
|
||||
"Access-Control-Max-Age: 86400\r\n"))
|
||||
|
||||
(define (http-status-text code)
|
||||
(cond
|
||||
((= code 200) "200 OK")
|
||||
((= code 204) "204 No Content")
|
||||
((= code 400) "400 Bad Request")
|
||||
((= code 405) "405 Method Not Allowed")
|
||||
((= code 500) "500 Internal Server Error")
|
||||
(else (string-append (number->string code) " Status"))))
|
||||
|
||||
;; write-http-response sock status-code body content-type
|
||||
;; Sends a fully-formed HTTP/1.1 response (status line + CORS +
|
||||
;; Content-Length + Connection: close + body). Caller closes sock.
|
||||
(define (write-http-response sock status body content-type)
|
||||
(let* ((body-len (string-length body))
|
||||
(response
|
||||
(string-append
|
||||
"HTTP/1.1 " (http-status-text status) "\r\n"
|
||||
*http-cors-headers*
|
||||
"Content-Type: " content-type "\r\n"
|
||||
"Content-Length: " (number->string body-len) "\r\n"
|
||||
"Connection: close\r\n"
|
||||
"\r\n"
|
||||
body)))
|
||||
(tcp-send sock response)))
|
||||
|
||||
;; write-http-options-response sock
|
||||
;; CORS preflight — 204 No Content + CORS headers, no body.
|
||||
(define (write-http-options-response sock)
|
||||
(let ((response
|
||||
(string-append
|
||||
"HTTP/1.1 " (http-status-text 204) "\r\n"
|
||||
*http-cors-headers*
|
||||
"Content-Length: 0\r\n"
|
||||
"Connection: close\r\n"
|
||||
"\r\n")))
|
||||
(tcp-send sock response)))
|
||||
175
examples/cuda-fanout/plans/bend-http-deploy.md
Normal file
175
examples/cuda-fanout/plans/bend-http-deploy.md
Normal file
|
|
@ -0,0 +1,175 @@
|
|||
# Bend HTTP deploy — Caddy + DNS proposals (NOT applied)
|
||||
|
||||
Phase 1 of bend's HTTP path (port 8321) lands inside the lumbda repo
|
||||
in this commit: `gpu-worker.lsp` now listens on both `:8320` wire and
|
||||
`:8321` HTTP, sharing one `handle-request` dispatcher. This document
|
||||
holds the *operational* deltas — to be applied by fox in
|
||||
`~/git/foxhop-pillar`, `~/git/foxhop-states`, and
|
||||
`~/git/proxy.unturf.com` after review.
|
||||
|
||||
Two access tiers, both supported, both compatible:
|
||||
|
||||
| Audience | URL | Gate | What changes here |
|
||||
|---|---|---|---|
|
||||
| Anyone (public adoption) | `http://localhost:8321/` (their own machine) | Browser localhost exception | Nothing in fox's infra — users run `make gpu-worker` on their own host. |
|
||||
| Fox (personal remote access) | `https://bend.unturf.com/` (proposed) | proxy.unturf.com `@trusted remote_ip` allowlist | proxy.unturf.com vhost + ai.foxhop.net vhost + DNS A record |
|
||||
|
||||
The playground placeholder stays `http://localhost:8321/` to teach
|
||||
the right pattern to visitors. Fox personally pastes the remote URL
|
||||
into the field when traveling.
|
||||
|
||||
## 1. Public DNS — `bend.unturf.com` A record
|
||||
|
||||
Whatever zone-authority manages `unturf.com` (per CLAUDE.md the
|
||||
PowerDNS master lives on `proxy.uncloseai.com:22222`):
|
||||
|
||||
```
|
||||
bend.unturf.com. 3600 IN A 142.93.73.64
|
||||
```
|
||||
|
||||
The IP is the existing `proxy.unturf.com` edge (the same address the
|
||||
`@trusted` matcher in `ai-foxhop-net.sls` already accepts).
|
||||
|
||||
Apply via:
|
||||
```bash
|
||||
ssh -i ~/.ssh/digitalocean -p 22222 root@proxy.uncloseai.com \
|
||||
"pdnsutil add-record unturf.com bend A 3600 142.93.73.64 && \
|
||||
pdns_control notify unturf.com"
|
||||
```
|
||||
|
||||
## 2. proxy.unturf.com — new public vhost
|
||||
|
||||
**File:** `~/git/proxy.unturf.com/ingress/Caddyfile`
|
||||
|
||||
Insert a new vhost block (place near the other `*.unturf.com` ones;
|
||||
exact location not load-bearing since Caddy matches by host):
|
||||
|
||||
```caddy
|
||||
# bend.unturf.com — personal remote access to fox's bend mesh.
|
||||
# NOT public: gated by a trusted-IP allowlist of fox's known IPs.
|
||||
# Routes Host-rewritten through ai.foxhop.net so the existing
|
||||
# @trusted edge-IP pattern at the LAN Caddy continues to gate.
|
||||
bend.unturf.com {
|
||||
@trusted_fox {
|
||||
# TODO(fox): replace with fox's known IPs. Examples:
|
||||
# remote_ip <home-public-ip>
|
||||
# remote_ip 100.64.0.0/10 # Tailscale CGNAT range
|
||||
# remote_ip <work-public-ip>
|
||||
remote_ip <FOX-IP-PLACEHOLDER>
|
||||
}
|
||||
|
||||
handle @trusted_fox {
|
||||
reverse_proxy https://ai.foxhop.net {
|
||||
header_up Host bend.foxhop.net
|
||||
header_up X-Real-IP {http.request.remote.host}
|
||||
header_up X-Forwarded-For {http.request.remote.host}
|
||||
header_up X-Forwarded-Proto {http.request.scheme}
|
||||
transport http {
|
||||
tls
|
||||
tls_server_name ai.foxhop.net
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
handle {
|
||||
respond "Access denied — personal endpoint" 403
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 3. ai.foxhop.net Caddy — new LAN vhost
|
||||
|
||||
**File:** `~/git/foxhop-pillar/caddy/ai-foxhop-net.sls`
|
||||
|
||||
Insert between the existing `home.foxhop.net` block (lines 74-88)
|
||||
and the next vhost. Mirrors the `home.foxhop.net` pattern exactly —
|
||||
edge-IP gated, reverse_proxies to a specific worker on the LAN.
|
||||
|
||||
```yaml
|
||||
bend.foxhop.net {
|
||||
|
||||
# === Trusted sources - ONLY edge proxy ===
|
||||
@trusted {
|
||||
remote_ip 142.93.73.64 # edge / proxy.unturf.com
|
||||
}
|
||||
|
||||
handle @trusted {
|
||||
# Forward HTTP/8321 of the bend worker mesh.
|
||||
# Single backend today (3090-ai); fan out across the mesh
|
||||
# once the single-host path is verified.
|
||||
reverse_proxy 3090-ai.foxhop.net:8321
|
||||
}
|
||||
|
||||
handle {
|
||||
respond "Access denied - bend backend (edge proxy required)" 403
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Apply via the usual pillar workflow:
|
||||
|
||||
```bash
|
||||
cd ~/git/foxhop-pillar
|
||||
git add caddy/ai-foxhop-net.sls
|
||||
git commit -m "caddy: bend.foxhop.net vhost — proxy to 3090-ai:8321"
|
||||
git push
|
||||
# then trigger highstate on ai.foxhop.net via salt-master (home.foxhop.net)
|
||||
```
|
||||
|
||||
## 4. Worker hosts — start bend on every GPU server
|
||||
|
||||
Per the discussion: each GPU server runs `gpu-worker.lsp` on both
|
||||
8320 + 8321 so the Caddy vhost above can reverse_proxy to whichever
|
||||
host is healthy.
|
||||
|
||||
Today the proposal pins backend to `3090-ai.foxhop.net:8321` (single
|
||||
host MVP). To verify the dual-port mode is live on 3090-ai:
|
||||
|
||||
```bash
|
||||
ssh 3090-ai.foxhop.net "ss -ltn | grep -E ':832[01]'"
|
||||
# expect both 8320 and 8321 in LISTEN
|
||||
```
|
||||
|
||||
A systemd unit (or whatever process manager 3090-ai uses for the
|
||||
current wire-only worker) needs the unit's ExecStart to keep the
|
||||
same args — no flag changes required since both ports come up by
|
||||
default. Confirm `gpu-worker.lsp` was redeployed after this commit
|
||||
lands in lumbda.
|
||||
|
||||
## 5. Future — fan-out across the 3-node mesh
|
||||
|
||||
Once 3090-ai HTTP path is verified, the ai.foxhop.net vhost can
|
||||
upgrade to round-robin / failover across the existing mesh:
|
||||
|
||||
```yaml
|
||||
bend.foxhop.net {
|
||||
@trusted { remote_ip 142.93.73.64 }
|
||||
handle @trusted {
|
||||
reverse_proxy 3090-ai.foxhop.net:8321 \
|
||||
ai.foxhop.net:8321 \
|
||||
cammy.foxhop.net:8321 {
|
||||
lb_policy least_conn
|
||||
health_uri / # or a dedicated /healthz once added
|
||||
health_interval 10s
|
||||
}
|
||||
}
|
||||
handle { respond "Access denied" 403 }
|
||||
}
|
||||
```
|
||||
|
||||
The fleet logic in `examples/cuda-fanout/bend.lsp` (round-robin +
|
||||
failover for native callers) and Caddy's `lb_policy` are
|
||||
independent — both can fan across the same mesh; neither needs to
|
||||
know about the other.
|
||||
|
||||
## What we explicitly chose NOT to do
|
||||
|
||||
- **No public-internet bend endpoint without auth.** `bend.unturf.com`
|
||||
is fox-only via trusted-IP. Public-internet bend would need
|
||||
authentication first (Phase 2 dependency).
|
||||
- **No bend.foxhop.net split-horizon DNS for public access.** Keeping
|
||||
`bend.foxhop.net` LAN-only and `bend.unturf.com` as the personal-
|
||||
remote name avoids the split-horizon confusion that bites every
|
||||
six months.
|
||||
- **No factory recipe handling in this commit.** Phase 2 lands
|
||||
server-side compilation of `.lsp` recipes after auth ships.
|
||||
105
examples/cuda-fanout/smoke-bend-http.sh
Executable file
105
examples/cuda-fanout/smoke-bend-http.sh
Executable file
|
|
@ -0,0 +1,105 @@
|
|||
#!/bin/bash
|
||||
# smoke-bend-http.sh — exercise gpu-worker.lsp's HTTP/8321 path.
|
||||
#
|
||||
# Boots gpu-worker on a test port pair (avoids prod 8320/8321), then:
|
||||
# 1. curl -X POST '(ping)' → expect "(ok pong)"
|
||||
# 2. curl -X POST '(echo "hello")' → expect "(echo \"hello\")"
|
||||
# (current handle-request has no 'echo head, returns unknown-op —
|
||||
# so we expect (error (unknown-op echo)) instead, which still
|
||||
# round-trips the wire correctly)
|
||||
# 3. curl -X OPTIONS preflight → expect 204 + CORS headers
|
||||
#
|
||||
# Memory-discipline pattern lifted from lumbda CLAUDE.md (asm heap
|
||||
# never shrinks): kernel cap, trap cleanup, wall-clock timeout,
|
||||
# explicit PID kill, straggler verify.
|
||||
|
||||
set -e
|
||||
ulimit -v 524288 # 512 MB kernel cap — process gets SIGKILL at cap
|
||||
|
||||
# Test ports — high so they never collide with prod 8320/8321
|
||||
WIRE_PORT=18320
|
||||
HTTP_PORT=18321
|
||||
|
||||
# Pick tier: env override > default Python (always available, has GC)
|
||||
LUMBDA="${LUMBDA:-$(cd "$(dirname "$0")/../.." && pwd)/lumbda.py}"
|
||||
case "$LUMBDA" in
|
||||
*.py) LUMBDA_RUN="python3 $LUMBDA" ;;
|
||||
*) LUMBDA_RUN="$LUMBDA" ;;
|
||||
esac
|
||||
|
||||
trap 'kill -9 $WORKER_PID 2>/dev/null || true;
|
||||
pkill -9 -u "$USER" -f "gpu-worker.lsp --port $WIRE_PORT" 2>/dev/null || true' \
|
||||
EXIT INT TERM
|
||||
|
||||
cd "$(dirname "$0")"
|
||||
|
||||
echo "=== smoke-bend-http (tier: $LUMBDA) ==="
|
||||
|
||||
# Boot worker — script-mode .lsp that loads gpu-worker.lsp + calls (main)
|
||||
cat > /tmp/smoke-bend-http-boot.lsp <<EOF
|
||||
(load "gpu-worker.lsp")
|
||||
(main)
|
||||
EOF
|
||||
|
||||
# Background the worker (foreground would block the test).
|
||||
timeout 30 $LUMBDA_RUN /tmp/smoke-bend-http-boot.lsp \
|
||||
--port $WIRE_PORT --http-port $HTTP_PORT > /tmp/smoke-bend-http-worker.log 2>&1 &
|
||||
WORKER_PID=$!
|
||||
|
||||
# Wait up to 5s for HTTP port to come up
|
||||
for i in 1 2 3 4 5 6 7 8 9 10; do
|
||||
if exec 3<>/dev/tcp/127.0.0.1/$HTTP_PORT 2>/dev/null; then
|
||||
exec 3<&-; exec 3>&-
|
||||
break
|
||||
fi
|
||||
sleep 0.5
|
||||
done
|
||||
|
||||
# ---- test 1: ping -----------------------------------------------
|
||||
echo "[1/3] POST (ping) → expect (ok pong) ..."
|
||||
PING_RESP=$(curl -sS -X POST -H 'Content-Type: text/plain' \
|
||||
--data-binary '(ping)' "http://127.0.0.1:$HTTP_PORT/" --max-time 5)
|
||||
echo " got: $PING_RESP"
|
||||
if [ "$PING_RESP" != "(ok pong)" ]; then
|
||||
echo " FAIL: expected (ok pong), got [$PING_RESP]"
|
||||
echo " worker log:"
|
||||
cat /tmp/smoke-bend-http-worker.log
|
||||
exit 1
|
||||
fi
|
||||
echo " PASS"
|
||||
|
||||
# ---- test 2: unknown-op gracefully ------------------------------
|
||||
echo "[2/3] POST (asdf 1 2) → expect (error (unknown-op asdf)) ..."
|
||||
UNK_RESP=$(curl -sS -X POST -H 'Content-Type: text/plain' \
|
||||
--data-binary '(asdf 1 2)' "http://127.0.0.1:$HTTP_PORT/" --max-time 5)
|
||||
echo " got: $UNK_RESP"
|
||||
case "$UNK_RESP" in
|
||||
*unknown-op*) echo " PASS" ;;
|
||||
*) echo " FAIL: expected unknown-op error, got [$UNK_RESP]"; exit 1 ;;
|
||||
esac
|
||||
|
||||
# ---- test 3: OPTIONS preflight + CORS headers -------------------
|
||||
echo "[3/3] OPTIONS / → expect 204 + CORS headers ..."
|
||||
PREFLIGHT=$(curl -sSI -X OPTIONS "http://127.0.0.1:$HTTP_PORT/" --max-time 5)
|
||||
echo "$PREFLIGHT" | head -5 | sed 's/^/ /'
|
||||
case "$PREFLIGHT" in
|
||||
*"204 No Content"*) ;;
|
||||
*) echo " FAIL: expected 204, got [$PREFLIGHT]"; exit 1 ;;
|
||||
esac
|
||||
case "$PREFLIGHT" in
|
||||
*"Access-Control-Allow-Origin"*) echo " PASS (CORS present)" ;;
|
||||
*) echo " FAIL: missing CORS headers"; exit 1 ;;
|
||||
esac
|
||||
|
||||
# Clean teardown
|
||||
kill -9 $WORKER_PID 2>/dev/null || true
|
||||
wait $WORKER_PID 2>/dev/null || true
|
||||
|
||||
# Straggler check — verify nothing left behind before reporting success
|
||||
if pgrep -u "$USER" -f "gpu-worker.lsp --port $WIRE_PORT" >/dev/null; then
|
||||
echo "STRAGGLER: gpu-worker on port $WIRE_PORT still running"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
rm -f /tmp/smoke-bend-http-boot.lsp /tmp/smoke-bend-http-worker.log
|
||||
echo "=== smoke-bend-http: PASS ==="
|
||||
|
|
@ -30,19 +30,49 @@
|
|||
make gpu-worker
|
||||
# → builds examples/cuda-fanout/shake256-fanout
|
||||
# → builds the C tier (~10× faster wire orchestration than Python)
|
||||
# → launches gpu-worker.lsp on port 8320 (BEND)
|
||||
# → launches gpu-worker.lsp on TWO ports:
|
||||
# 8320 — wire-TCP protocol (native callers: bend.lsp, asm clients)
|
||||
# 8321 — HTTP/1.1 + CORS (browser callers: the playground/REPL tabs)
|
||||
|
||||
# Port 8320 = BEND mnemonic:
|
||||
# 8 ~= B (implied infinity B flattened; bake a cake; baby & me)
|
||||
# 3 ~= E (backward)
|
||||
# 2 ~= N (pivoted 90 degrees)
|
||||
# 0 ~= D (flattened)
|
||||
# Port 8321 = 8320 + 1 (HTTP is always wire-port + 1, override via --http-port).
|
||||
|
||||
# Override tier (default port stays 8320 / BEND):
|
||||
make gpu-worker LUMBDA=python # easier debugging on port 8320
|
||||
make gpu-worker LUMBDA=asm # smallest footprint on port 8320
|
||||
make gpu-worker LUMBDA=python # easier debugging
|
||||
make gpu-worker LUMBDA=asm # smallest footprint
|
||||
# Override port too (only when running a second worker on the same host):
|
||||
make gpu-worker LUMBDA=python PORT=8321 # second worker, off-BEND port</code></pre>
|
||||
make gpu-worker LUMBDA=python PORT=9320 # second worker (wire 9320, http 9321)</code></pre>
|
||||
</section>
|
||||
|
||||
<section id="run-your-own">
|
||||
<h2>Run your own bend → use it from a browser tab</h2>
|
||||
<p>Browsers cannot open raw TCP sockets, but they can <code>fetch</code> / XHR to <code>http://localhost:<port></code> even from an HTTPS page (a permissive "potentially trustworthy" exception every major browser ships). So <strong>the playground field accepts a URL that points at a bend worker running on your own machine.</strong> No fox-owned ingress, no certificate, no proxy hop.</p>
|
||||
|
||||
<pre><code># 1. Start a worker on your laptop / desktop / homelab
|
||||
git clone https://lumbda.com/lumbda.git && cd lumbda
|
||||
make gpu-worker LUMBDA=python # CPU works; CUDA path lights up if nvcc is present
|
||||
# → "gpu-worker listening on port 8320"
|
||||
# → "gpu-worker HTTP listening on port 8321"
|
||||
|
||||
# 2. In a separate shell, smoke-test the HTTP path
|
||||
curl -sS -X POST -H 'Content-Type: text/plain' \
|
||||
--data-binary '(ping)' http://localhost:8321/
|
||||
# → (ok pong)
|
||||
|
||||
# 3. Open https://lumbda.com/playground/ and paste into the ⚡ bend field:
|
||||
# http://localhost:8321/
|
||||
# Save, then in the REPL:
|
||||
# λ> (bend!-call '(ping))</code></pre>
|
||||
|
||||
<p>Both ports share the same <code>handle-request</code> dispatcher: whatever an op-head (<code>ping</code>, <code>echo</code>, <code>cuda-shake-fanout</code>, <code>cuda-sim-ops-bin</code>, …) returns over wire is what HTTP returns. Adding a new op-head exposes it on both transports automatically.</p>
|
||||
|
||||
<p>HTTP scope today is the <strong>S-expression text path</strong> — for browser-friendly recipes the server understands today, with no binary blobs crossing the wire. Binary modes (<code>BSHK</code> / <code>BCGB</code> / <code>BSCP</code> / <code>BSRT</code> / <code>BSB3</code>) stay wire-only because they exist for native callers who already hold the binary locally.</p>
|
||||
|
||||
<p><em>Phase 2 (deferred until authentication lands):</em> upload an <code>.lsp</code> recipe describing a champion circuit, dispatch into a server-side factory that compiles the <code>.bin</code> on the worker's filesystem and bends it. Same playground field, same single endpoint. Auth gates write access; today a worker on the public internet would let any caller occupy your GPU.</p>
|
||||
</section>
|
||||
|
||||
<section id="call">
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue