lumbda/examples/cuda-fanout/plans/bend-http-deploy.md
russell@unturf.com 88e16c0ce2
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.
2026-06-14 17:32:46 -04:00

6 KiB

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:

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):

# 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.

    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:

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:

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:

    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.