Compare commits

...

82 Commits

Author SHA1 Message Date
will.anderson e82425a829 Merge pull request 'deploy: fix HAVE_CURL verification' (#62) from dev into stage
Stage — Build, push & deploy to marketing-stage / deploy-stage (push) Successful in 3m14s
2026-05-11 01:07:49 +00:00
will.anderson e480aba2f1 Merge pull request 'fix: HAVE_CURL verification — use strings not nm' (#61) from fix/checkout-auth-reveal into dev
Dev — Build & local smoke test / build-smoke (push) Successful in 2m25s
2026-05-11 01:07:39 +00:00
will.anderson feee40c34b ci: fix HAVE_CURL verification — use strings check not nm
Dev — Build & local smoke test / build-smoke (pull_request) Successful in 1m54s
2026-05-10 20:07:21 -05:00
will.anderson c4cdb31529 Merge pull request 'deploy: fix HAVE_CURL — enable chat proxy to soul-demo' (#60) from dev into stage
Stage — Build, push & deploy to marketing-stage / deploy-stage (push) Failing after 57s
deploy: fix HAVE_CURL — enable chat proxy to soul-demo
2026-05-11 01:03:41 +00:00
will.anderson e6e89a1f4d Merge pull request 'fix: relink neuron-web with HAVE_CURL (chat proxy)' (#59) from fix/checkout-auth-reveal into dev
Dev — Build & local smoke test / build-smoke (push) Successful in 2m15s
fix: relink neuron-web with HAVE_CURL (chat proxy)
2026-05-11 01:03:22 +00:00
will.anderson 8b8cb2f580 ci: relink neuron-web with HAVE_CURL after elb build
Dev — Build & local smoke test / build-smoke (pull_request) Successful in 1m45s
elb does not pass -DHAVE_CURL when compiling el_runtime.c, so all
http_get/http_post calls from El code return the no-op error string
instead of making real HTTP requests. This breaks the chat proxy to
soul-demo at localhost:7772.

After elb runs (and generates all intermediate .c files in dist/),
recompile el_runtime.c with -DHAVE_CURL and relink the entire binary
from those generated files. Verifies curl_easy_init is present in the
output binary before proceeding.
2026-05-10 20:03:00 -05:00
will.anderson a1c0cc090d Merge pull request 'Deploy: replace k3s with direct soul-demo watchdog' (#58) from dev into stage
Stage — Build, push & deploy to marketing-stage / deploy-stage (push) Successful in 2m54s
Merge dev into stage
2026-05-11 00:47:16 +00:00
will.anderson 4d359ff021 Merge pull request 'Replace k3s with direct soul-demo watchdog' (#57) from fix/checkout-auth-reveal into dev
Dev — Build & local smoke test / build-smoke (push) Successful in 2m26s
Merge fix/checkout-auth-reveal into dev
2026-05-11 00:46:56 +00:00
will.anderson cd1c6737e8 Replace k3s with direct soul-demo watchdog in Cloud Run container
Dev — Build & local smoke test / build-smoke (pull_request) Successful in 2m11s
Cloud Run gen2 doesn't provide eth0 with a unicast IP, causing k3s flannel
to crash on every container start. k3s was also wrong architecture for
Cloud Run (HPA inside a container, k3s overhead for one process).

Changes:
- entrypoint.sh: replace k3s server with a bash watchdog loop that starts
  soul-demo directly and restarts it on crash (3s backoff)
- Dockerfile.stage: remove k3s binary, soul-demo-image.tar, k3s manifests
  and their associated dirs/envvars; keep soul-demo binary only
- stage.yaml: remove 'Download k3s binary' step; rename and simplify
  soul-demo build step to compile binary only (no OCI image/tar)
- dev.yaml: update soul-demo placeholder step (binary not tar)
- manifest.el: document HAVE_CURL requirement since manifest.el has no
  c_flags/link_flags directive support
2026-05-10 19:46:35 -05:00
will.anderson 7df96a2273 Merge pull request 'Deploy: fix envelope truncation' (#56) from dev into stage
Stage — Build, push & deploy to marketing-stage / deploy-stage (push) Successful in 3m50s
2026-05-11 00:23:38 +00:00
will.anderson f27fc2622c Merge pull request 'Fix envelope truncation in http_response when called after fs_read' (#55) from fix/checkout-auth-reveal into dev
Dev — Build & local smoke test / build-smoke (push) Failing after 2m46s
2026-05-11 00:23:28 +00:00
will.anderson 0433fe8c0f Fix http_response() truncating envelope via stale _tl_fs_read_len
Dev — Build & local smoke test / build-smoke (pull_request) Failing after 2m22s
http_response() builds a JSON envelope wrapping the body. If the caller
previously called fs_read() (which sets _tl_fs_read_len = file_size),
http_worker used that stale value as the response copy length — truncating
the larger envelope to the original file size before it reached
http_send_response. The truncated envelope had the body field cut mid-string;
jp_parse_string_raw failed, env_body = "", and http_send_all sent file_size
bytes of garbage past the empty string.

Fix: reset _tl_fs_read_len = 0 at the start of http_response(). The hint
was set for the raw file bytes; the envelope is a new string and must use
strlen() for its length.
2026-05-10 19:23:10 -05:00
will.anderson d3b890b739 Merge pull request 'Deploy: fix JS served as JSON envelope' (#54) from dev into stage
Stage — Build, push & deploy to marketing-stage / deploy-stage (push) Successful in 3m11s
Deploy: fix JS served as JSON envelope
2026-05-10 22:34:57 +00:00
will.anderson 9da4d50883 Merge pull request 'Fix JS files served as JSON envelope (checkout/Stripe/auth all broken)' (#53) from fix/checkout-auth-reveal into dev
Dev — Build & local smoke test / build-smoke (push) Failing after 2m4s
Fix JS files served as JSON envelope (checkout/Stripe/auth all broken)
2026-05-10 22:34:32 +00:00
will.anderson c99ca82302 Fix JS files served as raw JSON envelope instead of JavaScript
Dev — Build & local smoke test / build-smoke (pull_request) Failing after 1m36s
http_parse_envelope() called json_parse() on the entire response envelope
(~47KB when body is obfuscated JS). The parser failed on large/complex content,
so is_envelope=0 and the raw JSON was sent — browsers got {"el_http_response":1,...}
instead of executable JavaScript, silently breaking all client-side code.

Fix: replace json_parse-of-full-envelope with a direct field scanner:
- "status" extracted via strtol
- "headers" object extracted via brace-depth scan, then json_parse only that
  small substring (always safe — headers are simple k/v string pairs < 1KB)
- "body" string extracted via jp_parse_string_raw — no intermediate allocation

Also: /js/* route now returns http_response(200, js_headers_json(), content)
with explicit Content-Type: application/javascript so the browser doesn't
apply the json-heuristic (obfuscated JS starting with '[' was detected as JSON,
which with X-Content-Type-Options: nosniff blocks script execution).
2026-05-10 17:32:45 -05:00
will.anderson 3f069eeb79 Merge pull request 'Fix checkout auth (dev → stage)' (#52) from dev into stage
Stage — Build, push & deploy to marketing-stage / deploy-stage (push) Successful in 3m14s
Fix checkout auth (dev → stage)
2026-05-10 22:01:17 +00:00
will.anderson e292453905 Merge pull request 'Fix checkout auth: free-success panel + Stripe auto-init for paid plans' (#51) from fix/checkout-auth-reveal into dev
Dev — Build & local smoke test / build-smoke (push) Failing after 2m12s
Fix checkout auth: free-success panel + Stripe auto-init for paid plans
2026-05-10 22:00:55 +00:00
will.anderson 0263e51407 Fix checkout: show free-success when logged in; init Stripe without auth on paid plans
Dev — Build & local smoke test / build-smoke (pull_request) Failing after 1m47s
- revealPaymentForm: for free plan, show #free-success panel (was doing nothing,
  leaving page blank when user already had a Supabase session)
- checkExistingSession: for paid plans with no session, call initStripe immediately —
  auth is optional, the payment form shouldn't wait indefinitely
- Guard _formRevealed: prevent double-call from handleAuthRedirect + checkExistingSession
2026-05-10 16:59:51 -05:00
will.anderson 8676751ed6 Merge pull request 'Fix http handler registration (dev → stage)' (#50) from dev into stage
Stage — Build, push & deploy to marketing-stage / deploy-stage (push) Successful in 3m40s
Merge PR #50: Fix http handler registration (dev → stage)
2026-05-10 18:37:16 +00:00
will.anderson b4935ed880 Merge pull request 'Fix http handler not found: pre-register via constructor' (#49) from fix/entrypoint-k3s-nonblocking into dev
Dev — Build & local smoke test / build-smoke (push) Failing after 2m29s
Merge PR #49: Fix http handler not found
2026-05-10 18:36:47 +00:00
will.anderson 9a6f0defd1 Fix http handler not found: pre-register via el_runtime_register_handler
Dev — Build & local smoke test / build-smoke (pull_request) Failing after 1m54s
elb links without -rdynamic so dlsym(RTLD_DEFAULT, "handle_request")
returns NULL at runtime. http_set_handler stores the name as active but
never finds a function pointer, causing every request to return
"el-runtime: no http handler registered" even after http_serve is called.

Fix: add a __attribute__((constructor)) in web_stubs.c that calls
el_runtime_register_handler("handle_request", handle_request) directly,
bypassing dlsym entirely. The handler is in the registry before main()
runs, so http_lookup_active() finds it on the first request.
2026-05-10 13:36:05 -05:00
will.anderson a4f5312069 Merge pull request 'Fix GLIBC_2.38 mismatch: switch base image to ubuntu:24.04' (#48) from dev into stage
Stage — Build, push & deploy to marketing-stage / deploy-stage (push) Successful in 3m37s
Fix GLIBC_2.38 mismatch: switch base image to ubuntu:24.04
2026-05-10 18:02:14 +00:00
will.anderson ee0147869e Merge pull request 'Fix GLIBC_2.38 mismatch: switch base image to ubuntu:24.04' (#47) from fix/entrypoint-k3s-nonblocking into dev
Dev — Build & local smoke test / build-smoke (push) Failing after 2m23s
Fix GLIBC_2.38 mismatch: switch base image to ubuntu:24.04
2026-05-10 18:01:57 +00:00
will.anderson 740382fca1 Fix GLIBC_2.38 mismatch: switch base image to ubuntu:24.04
Dev — Build & local smoke test / build-smoke (pull_request) Failing after 2m13s
CI runner (Ubuntu 24.04, glibc 2.39) produces binaries that require
GLIBC_2.38+. debian:bookworm-slim ships glibc 2.36 which doesn't have
the GLIBC_2.38 versioned symbols — container crashes immediately with
"version GLIBC_2.38 not found". Switch to ubuntu:24.04 (glibc 2.39)
to match the build environment. Also updates libcurl4/libssl3 package
names to their Ubuntu 24.04 canonical t64 forms.
2026-05-10 13:01:38 -05:00
will.anderson c76e5a19eb Merge pull request 'Non-blocking entrypoint + k3s --flannel-iface fix' (#46) from dev into stage
Stage — Build, push & deploy to marketing-stage / deploy-stage (push) Failing after 4m8s
Non-blocking entrypoint + k3s flannel-iface fix
2026-05-10 17:55:12 +00:00
will.anderson 25f6631049 Merge pull request 'Non-blocking entrypoint: start neuron-web before k3s is ready' (#45) from fix/entrypoint-k3s-nonblocking into dev
Dev — Build & local smoke test / build-smoke (push) Failing after 2m43s
Non-blocking entrypoint: start neuron-web before k3s is ready
2026-05-10 17:54:54 +00:00
will.anderson 180acc92a0 Non-blocking entrypoint: start neuron-web before k3s is ready
Dev — Build & local smoke test / build-smoke (pull_request) Failing after 2m11s
k3s fails to start in Cloud Run gen2 with "unable to select an IP from
default routes" because Cloud Run's network sandbox doesn't expose a
standard default route for k3s to detect. The blocking wait on k3s
prevented neuron-web from ever binding port 8080, causing Cloud Run's
startup probe to time out and terminate the container.

Two changes:
1. Add --flannel-iface=eth0 so k3s pins to Cloud Run's eth0 rather than
   walking the routing table to detect a default-route interface.
2. Start neuron-web immediately after launching k3s in background.
   soul-demo becomes available asynchronously; neuron-web handles it
   being temporarily unavailable gracefully.
2026-05-10 12:54:26 -05:00
will.anderson 58b7b32cdd Single-stage Dockerfile.stage: pre-download k3s on host runner
Stage — Build, push & deploy to marketing-stage / deploy-stage (push) Failing after 12s
2026-05-10 16:27:07 +00:00
will.anderson 689062fc87 Single-stage Dockerfile.stage: pre-download k3s on host runner
Dev — Build & local smoke test / build-smoke (push) Failing after 1m20s
2026-05-10 16:26:46 +00:00
will.anderson e6fd110073 Single-stage Dockerfile.stage: pre-download k3s on host runner
Dev — Build & local smoke test / build-smoke (pull_request) Failing after 1m37s
The multi-stage Docker builder (which installed build-essential, compiled
soul-demo, and downloaded k3s inside Docker) was causing RWLayer nil
corruption on the runner's overlay2 driver. Every affected run failed at
apt-get install in the runtime stage after the builder stage completed.

Fix: move k3s download to the CI host runner (same pattern as soul-demo
compilation, which now passes reliably). Dockerfile.stage becomes single-
stage: no apt-get in a builder stage, no network downloads, just COPY of
pre-built binaries. Also adds --no-cache to the main docker build for
consistency with the soul-demo step fix.
2026-05-10 11:26:23 -05:00
will.anderson 0fdabcce86 Merge pull request 'promote: dev → stage' (#42) from dev into stage
Stage — Build, push & deploy to marketing-stage / deploy-stage (push) Failing after 4m46s
promote: dev → stage
2026-05-10 15:57:36 +00:00
will.anderson 5e1344af42 Merge pull request 'Fix soul-demo Docker build: --no-cache to avoid corrupted overlay2 layers' (#41) from fix/stage-source-check into dev
Dev — Build & local smoke test / build-smoke (push) Successful in 4m42s
Fix soul-demo Docker build: --no-cache to avoid corrupted overlay2 layers
2026-05-10 15:57:13 +00:00
will.anderson d8acb126f5 Fix soul-demo Docker build: --no-cache to avoid corrupted overlay2 layers
Dev — Build & local smoke test / build-smoke (pull_request) Failing after 27s
2026-05-10 10:56:44 -05:00
will.anderson 79de47de2c Merge pull request 'promote: dev → stage' (#40) from dev into stage
Stage — Build, push & deploy to marketing-stage / deploy-stage (push) Failing after 10m59s
2026-05-10 02:26:35 +00:00
will.anderson 87ac67a70e Merge pull request 'Selective Docker prune (preserve build cache) + k3s retry' (#39) from fix/stage-source-check into dev
Dev — Build & local smoke test / build-smoke (push) Successful in 4m10s
2026-05-10 02:22:08 +00:00
will.anderson f838e0c8a7 Selective Docker prune to preserve build cache; retry k3s download
Dev — Build & local smoke test / build-smoke (pull_request) Successful in 4m2s
2026-05-09 21:21:52 -05:00
will.anderson e520ba98ca Merge pull request 'Make docker prune non-fatal (concurrent prune race)' (#38) from fix/stage-source-check into dev
Dev — Build & local smoke test / build-smoke (push) Failing after 14m45s
2026-05-10 01:57:30 +00:00
will.anderson 21ecbca2e6 Make docker prune non-fatal to handle concurrent prune from parallel CI jobs
Dev — Build & local smoke test / build-smoke (pull_request) Failing after 15m15s
2026-05-09 20:57:14 -05:00
will.anderson 38c92e5fc7 Merge pull request 'Fix CI disk exhaustion: docker system prune at job start' (#37) from fix/stage-source-check into dev
Dev — Build & local smoke test / build-smoke (push) Failing after 24s
2026-05-10 01:55:41 +00:00
will.anderson cee0328db5 Add docker system prune at job start to prevent disk exhaustion
Dev — Build & local smoke test / build-smoke (pull_request) Failing after 17m6s
2026-05-09 20:55:24 -05:00
will.anderson bbfc7cebf7 Merge pull request 'Move soul-demo build after JS compile in stage pipeline' (#36) from fix/stage-source-check into dev
Dev — Build & local smoke test / build-smoke (push) Failing after 3m13s
2026-05-10 01:50:17 +00:00
will.anderson 4a710ff294 Move soul-demo build after JS compile to prevent Docker memory pressure on elc
Dev — Build & local smoke test / build-smoke (pull_request) Successful in 3m7s
2026-05-09 20:50:01 -05:00
will.anderson 45963154d9 Merge pull request 'promote: dev → stage' (#35) from dev into stage
Stage — Build, push & deploy to marketing-stage / deploy-stage (push) Failing after 14m42s
2026-05-10 01:32:52 +00:00
will.anderson f1b5e1bac8 Merge pull request 'Add diagnostics to stage JS compile step' (#34) from fix/stage-source-check into dev
Dev — Build & local smoke test / build-smoke (push) Successful in 4m48s
2026-05-10 01:27:20 +00:00
will.anderson b4438fec43 Add diagnostics to stage JS compile step to expose silent failure
Dev — Build & local smoke test / build-smoke (pull_request) Successful in 2m21s
2026-05-09 20:27:05 -05:00
will.anderson aabaa2ffb0 Merge pull request 'promote: dev → stage' (#33) from dev into stage
Stage — Build, push & deploy to marketing-stage / deploy-stage (push) Failing after 15m11s
2026-05-10 01:07:20 +00:00
will.anderson aa040d1412 Merge pull request 'Fix soul-demo compile: add -I runtime/ include path' (#32) from fix/stage-source-check into dev
Dev — Build & local smoke test / build-smoke (push) Successful in 4m5s
2026-05-10 01:02:36 +00:00
will.anderson d5820c43b0 Fix soul-demo compile: add -I runtime/ for el_runtime.h include path
Dev — Build & local smoke test / build-smoke (pull_request) Successful in 1m42s
2026-05-09 20:02:22 -05:00
will.anderson d5dcb08ec6 Merge pull request 'promote: dev → stage (soul-demo image tar fix)' (#31) from dev into stage
Stage — Build, push & deploy to marketing-stage / deploy-stage (push) Failing after 24s
2026-05-10 01:01:01 +00:00
will.anderson a1144605f3 Merge pull request 'Build soul-demo image tar before Docker build in stage' (#30) from fix/stage-source-check into dev
Dev — Build & local smoke test / build-smoke (push) Successful in 4m28s
2026-05-10 00:53:48 +00:00
will.anderson 43949b20a0 Build soul-demo image tar before Docker build in stage
Dev — Build & local smoke test / build-smoke (pull_request) Failing after 10m38s
Dockerfile.stage COPYs dist/soul-demo-image.tar so k3s can import
soul-demo:local at container startup. Stage CI now compiles soul-demo
from source on the host runner and packages it as an OCI image before
the main Docker build runs.
2026-05-09 19:41:55 -05:00
will.anderson 20a36eeb9e Merge pull request 'promote: dev → stage' (#29) from dev into stage
Stage — Build, push & deploy to marketing-stage / deploy-stage (push) Failing after 3m11s
2026-05-10 00:34:47 +00:00
will.anderson 06b46c2e8f Merge pull request 'Use ci-base:dev for stage SDK extraction' (#28) from fix/stage-source-check into dev
Dev — Build & local smoke test / build-smoke (push) Successful in 5m12s
2026-05-10 00:29:00 +00:00
will.anderson ac5838f3dd Use ci-base:dev for stage SDK extraction
Dev — Build & local smoke test / build-smoke (pull_request) Failing after 12m6s
ci-base:latest has a different (older) elb that generates code with
undeclared variables. The web repo targets ci-base:dev which produces
correct C output. Stage must use the same SDK version as dev.
2026-05-09 19:15:24 -05:00
will.anderson 32a179c24a Merge pull request 'promote: dev → stage' (#27) from dev into stage
Stage — Build, push & deploy to marketing-stage / deploy-stage (push) Failing after 37s
2026-05-10 00:12:47 +00:00
will.anderson c8d1d3e1aa Merge pull request 'Fix stage SDK extraction: use ci-base:latest and repo runtime' (#26) from fix/stage-source-check into dev
Dev — Build & local smoke test / build-smoke (push) Failing after 14m5s
2026-05-09 23:48:28 +00:00
will.anderson b532519ad7 Fix stage SDK extraction: use ci-base:latest and repo runtime
Dev — Build & local smoke test / build-smoke (pull_request) Successful in 1m57s
ci-base:stage tag doesn't exist — only :latest and :dev do. Also
apply the same EL_RUNTIME fix as dev.yaml: point at workspace
runtime/ so stage picks up the web stub forward declarations.
2026-05-09 18:45:57 -05:00
will.anderson 6bc026de19 Merge pull request 'promote: dev → stage' (#25) from dev into stage
Stage — Build, push & deploy to marketing-stage / deploy-stage (push) Failing after 21s
2026-05-09 23:44:30 +00:00
will.anderson b27aab20ee Merge pull request 'Fix stage source check: run after checkout' (#24) from fix/stage-source-check into dev
Dev — Build & local smoke test / build-smoke (push) Successful in 3m52s
2026-05-09 23:40:02 +00:00
will.anderson 345f9be81a Fix stage source check: run after checkout, not before
Dev — Build & local smoke test / build-smoke (pull_request) Successful in 1m29s
git log -1 fails with 'not a git repository' when the workspace
hasn't been checked out yet. Move the Enforce dev-only source step
to after the Checkout step.
2026-05-09 18:37:55 -05:00
will.anderson 0ae526b72e Merge pull request 'promote: dev → stage' (#23) from dev into stage
Stage — Build, push & deploy to marketing-stage / deploy-stage (push) Failing after 22s
2026-05-09 23:35:56 +00:00
will.anderson 17e14a9fda Merge pull request 'Use repo runtime dir for EL_RUNTIME in push builds' (#22) from fix/stage-source-check into dev
Dev — Build & local smoke test / build-smoke (push) Successful in 3m34s
2026-05-09 23:17:49 +00:00
will.anderson e7c1c922f7 Use repo runtime dir for EL_RUNTIME in push builds
Dev — Build & local smoke test / build-smoke (pull_request) Successful in 1m56s
ci-base's el-compiler/runtime doesn't have the web-specific forward
declarations added to runtime/el_runtime.h. Point EL_RUNTIME at the
workspace runtime/ so push builds pick up the same header as PR builds.
2026-05-09 18:15:18 -05:00
will.anderson 954dc1d86e Merge pull request 'Add forward declarations for web stub functions to el_runtime.h' (#21) from fix/stage-source-check into dev
Dev — Build & local smoke test / build-smoke (push) Failing after 5m4s
2026-05-09 23:07:22 +00:00
will.anderson a83efcda93 Guard web stub declarations with EL_SOUL_DEMO_BUILD to avoid soul-demo conflict
Dev — Build & local smoke test / build-smoke (pull_request) Successful in 2m22s
2026-05-09 18:04:24 -05:00
will.anderson 839c002ce0 Add missing forward declarations to el_runtime.h for web stub functions
Dev — Build & local smoke test / build-smoke (pull_request) Failing after 1m55s
2026-05-09 18:00:29 -05:00
will.anderson 0abef440fa Merge pull request 'Fix implicit declaration of page_close on Linux' (#20) from fix/stage-source-check into dev
Dev — Build & local smoke test / build-smoke (push) Failing after 4m9s
2026-05-09 22:54:05 +00:00
will.anderson 9892d89c01 Fix implicit declaration of page_close on Linux: wrap extern as native El fn
Dev — Build & local smoke test / build-smoke (pull_request) Successful in 3m25s
2026-05-09 17:49:15 -05:00
will.anderson 47163f690b Merge pull request 'Fix stage source check to use git parents' (#19) from fix/stage-source-check into dev
Dev — Build & local smoke test / build-smoke (push) Failing after 4m25s
2026-05-09 22:41:32 +00:00
will.anderson dc36fe0157 Skip smoke test for PR builds — compile+image-build is sufficient gate
Dev — Build & local smoke test / build-smoke (pull_request) Successful in 2m6s
2026-05-09 17:39:04 -05:00
will.anderson fa65f7783e Split page_css.c EL_STR into 18 chunks via el_str_concat to fix runtime segfault
Dev — Build & local smoke test / build-smoke (pull_request) Failing after 3m15s
2026-05-09 17:27:58 -05:00
will.anderson b63aa5027b Fix dev CI smoke test: run binary directly, skip Docker runtime
Dev — Build & local smoke test / build-smoke (pull_request) Failing after 2m25s
The runner compiles neuron-landing against glibc 2.38 but the Docker
base image ships an older glibc — binary crashes on exec inside the
container. Docker build step already validates the image; smoke test
just needs an HTTP 200, so run the binary directly on the runner instead.
2026-05-09 16:33:29 -05:00
will.anderson 1110ff2e8c Add SKIP_K3S escape hatch for dev CI smoke test
Dev — Build & local smoke test / build-smoke (pull_request) Failing after 2m21s
k3s requires kernel capabilities (overlayfs) that aren't available in
the CI runner's unprivileged Docker environment. Entrypoint now checks
SKIP_K3S=1 and starts neuron-web directly, bypassing k3s and soul-demo.
Dev CI smoke test sets this flag — prod images are unaffected.
2026-05-09 16:22:40 -05:00
will.anderson a51a16c4da Fix dev CI: touch soul-demo-image.tar placeholder before Docker build
Dev — Build & local smoke test / build-smoke (pull_request) Failing after 3m5s
2026-05-09 16:17:18 -05:00
will.anderson 15c70f0e26 Fix stage source check to use git parent instead of commit message parsing
Dev — Build & local smoke test / build-smoke (pull_request) Failing after 1m50s
2026-05-09 15:09:38 -05:00
will.anderson 8221aef605 promote: dev → stage
Stage — Build, push & deploy to marketing-stage / deploy-stage (push) Failing after 37s
2026-05-09 18:34:59 +00:00
will.anderson b39977b74c Fix broken payment/checkout page
Dev — Build & local smoke test / build-smoke (push) Failing after 3m34s
2026-05-09 18:14:15 +00:00
will.anderson 90609c7aaf Convert page_open to native El; fix corrupted CSS
Dev — Build & local smoke test / build-smoke (pull_request) Failing after 1m57s
elc's heredoc tokenizer was corrupting the inline CSS:
- #FAFAF8 -> FAFAF8 (# treated as comment character)
- 'Playfair Display' -> PlayfairDisplay (quotes + space stripped)
- padding: 0 2.5rem -> padding:02.5rem (spaces between tokens stripped)

The CSS and other complex head content (GA script, JSON-LD schema)
have been pre-compiled to C functions (page_css, page_ga_script,
page_schema) so they bypass the tokenizer entirely and are stored as
properly-escaped C string literals.

page_head() now assembles the <head> content using el-html vessel
calls (el_meta_charset, el_meta, el_title, el_link_stylesheet, etc.)
plus string literals for the vessel gaps. page_open() returns the
complete document prologue as a string concatenation with no heredocs.

page_close() remains pre-compiled in dist/page_close.c (unchanged).
2026-05-09 13:07:06 -05:00
will.anderson 7f27f4be9f Fix broken payment page: escape html/body heredocs in page_open
Dev — Build & local smoke test / build-smoke (pull_request) Failing after 1m27s
elc's heredoc parser treats <html> as an opener and scans forward for
</html>, which exists inside page_close's return statement. This caused
the entire El source of page_close to be injected verbatim into the
page_open output string, terminating the document before Stripe scripts
could load.

Fix: put <!DOCTYPE html><html lang="en"> in a quoted string literal
and use <head>...</head> as the sole heredoc in page_open — closes
within the same function, no cross-boundary scanning. Stub page_close
in styles.el as extern fn so dist/page_close.c supplies the definition.

Also fix elc-broken hyphenated attributes in dist/page_close.c:
aria-label, stroke-width, stroke-linecap, &times;, and several
text nodes that had whitespace stripped by the heredoc parser.
2026-05-09 12:56:50 -05:00
will.anderson f8487c43a0 Merge branch 'dev' into stage
Stage — Build, push & deploy to marketing-stage / deploy-stage (push) Failing after 14m16s
2026-05-09 17:41:09 +00:00
will.anderson 66e3ac6321 feat: embed k3s to run soul-demo as self-healing k8s pods
Dev — Build & local smoke test / build-smoke (push) Failing after 3m56s
2026-05-09 17:40:43 +00:00
will.anderson 36b99dd9e2 Merge branch 'dev' into stage
Stage — Build, push & deploy to marketing-stage / deploy-stage (push) Failing after 6s
2026-05-09 17:32:23 +00:00
16 changed files with 2374 additions and 2169 deletions
+35 -13
View File
@@ -75,6 +75,17 @@ jobs:
if: github.event_name != 'pull_request'
run: gcloud auth configure-docker us-central1-docker.pkg.dev --quiet
- name: Prune Docker to reclaim disk
run: |
# Remove stopped containers, dangling images, unused volumes/networks.
# Do NOT prune build cache — that keeps Docker builds fast and under
# the ~26min runner restart window. Selective pruning frees ~4-5GB
# which is enough to prevent overlay2 "no space left on device" errors.
docker container prune -f 2>&1 || true
docker image prune -f 2>&1 || true
docker volume prune -f 2>&1 || true
df -h /
# ── El SDK setup ──────────────────────────────────────────────────────
# Push builds: extract elb + elc + runtime from ci-base (always latest).
# PR builds: use committed bin/elb-linux-amd64 + bin/elc-linux-amd64 + runtime/.
@@ -90,7 +101,7 @@ jobs:
docker rm "$CID"
echo "ELB=/opt/el/dist/bin/elb" >> "$GITHUB_ENV"
echo "ELC=/opt/el/dist/platform/elc" >> "$GITHUB_ENV"
echo "EL_RUNTIME=/opt/el/el-compiler/runtime" >> "$GITHUB_ENV"
echo "EL_RUNTIME=$GITHUB_WORKSPACE/runtime" >> "$GITHUB_ENV"
- name: Set up El SDK from committed bin/ (PR builds)
if: github.event_name == 'pull_request'
@@ -146,6 +157,13 @@ jobs:
rm -f src/js/el_runtime.js
# ── Docker build + smoke test ─────────────────────────────────────────
#
# PR builds: binary is compiled by committed bin/elb-linux-amd64 which
# may lag behind the current El SDK. Smoke-testing that binary is
# unreliable (glibc mismatch in Docker; potential codegen differences
# when run directly). PRs only need to prove the code *compiles* and
# the Docker image *builds* — the authoritative runtime check runs on
# push to dev (ci-base SDK, always current).
- name: Compute image tag
id: tag
@@ -154,6 +172,13 @@ jobs:
- name: Touch HTML placeholder files
run: touch src/index.html src/about.html src/terms.html src/enterprise-terms.html
- name: Create soul-demo placeholder
# Dockerfile.stage COPYs dist/soul-demo. We only need the binary to exist
# for the Docker build to succeed; the real binary is compiled in stage CI.
run: |
touch dist/soul-demo
chmod +x dist/soul-demo
- name: Build Docker image (local only — no push)
run: |
set -euo pipefail
@@ -170,30 +195,27 @@ jobs:
.
- name: Local smoke test
# Push builds only: binary compiled from ci-base is current and
# compatible with the runner glibc. Skipped for pull_request events
# because the committed bin/elb may produce a binary that requires
# a newer glibc than what the runner environment provides.
if: github.event_name != 'pull_request'
run: |
set -euo pipefail
IMAGE="marketing:${{ steps.tag.outputs.tag }}"
docker run -d --name dev-smoke \
-p 8080:8080 \
-e PORT=8080 \
-e NODE_ENV=production \
-e LANDING_ROOT=/srv/landing \
"$IMAGE"
PORT=8080 dist/neuron-landing &
SERVER_PID=$!
for i in $(seq 1 15); do
STATUS=$(curl -sSo /dev/null -w "%{http_code}" --max-time 5 http://localhost:8080/ || echo "000")
echo "Attempt $i/15: HTTP $STATUS"
if [ "$STATUS" = "200" ]; then
echo "Dev smoke test PASSED"
docker stop dev-smoke && docker rm dev-smoke
kill "$SERVER_PID" 2>/dev/null || true
exit 0
fi
sleep 3
done
echo "--- container logs ---"
docker logs dev-smoke || true
docker stop dev-smoke && docker rm dev-smoke || true
kill "$SERVER_PID" 2>/dev/null || true
echo "Dev smoke test FAILED"
exit 1
+99 -12
View File
@@ -32,10 +32,16 @@ jobs:
id-token: write
steps:
- name: Checkout
uses: actions/checkout@v4
with:
fetch-depth: 2
- name: Enforce dev-only source
# stage only accepts merges from dev. Any PR from another branch fails
# here before a single build step runs.
# workflow_dispatch is exempt (allows manual redeploy of current stage).
# Must run AFTER checkout — git commands require a cloned workspace.
if: github.event_name != 'workflow_dispatch'
run: |
set -euo pipefail
@@ -43,7 +49,17 @@ jobs:
echo "Merge commit: $COMMIT_MSG"
# Gitea merge commits: "Merge pull request '...' (#N) from dev into stage"
# Direct branch merges: "Merge branch 'dev' into stage"
if echo "$COMMIT_MSG" | grep -qE " from dev into stage$| 'dev' into stage$"; then
# tea pr merge with custom title: any subject line is possible, so
# fall back to checking git parents — if the second parent is on dev
# the merge came from dev regardless of the commit subject.
SECOND_PARENT=$(git log -1 --pretty=format:"%P" HEAD | awk '{print $2}')
FROM_DEV=""
if [ -n "$SECOND_PARENT" ]; then
if git merge-base --is-ancestor "$SECOND_PARENT" origin/dev 2>/dev/null; then
FROM_DEV=1
fi
fi
if echo "$COMMIT_MSG" | grep -qE " from dev into stage$| 'dev' into stage$" || [ -n "$FROM_DEV" ]; then
echo "Source branch check: OK (merged from dev)"
else
echo "ERROR: stage only accepts merges from dev."
@@ -51,11 +67,6 @@ jobs:
exit 1
fi
- name: Checkout
uses: actions/checkout@v4
with:
fetch-depth: 2
- name: Detect change type
id: changetype
run: |
@@ -85,6 +96,17 @@ jobs:
- name: Configure docker auth for Artifact Registry
run: gcloud auth configure-docker us-central1-docker.pkg.dev --quiet
- name: Prune Docker to reclaim disk
run: |
# Remove stopped containers, dangling images, unused volumes/networks.
# Do NOT prune build cache — that keeps Docker builds fast and under
# the ~26min runner restart window. Selective pruning frees ~4-5GB
# which is enough to prevent overlay2 "no space left on device" errors.
docker container prune -f 2>&1 || true
docker image prune -f 2>&1 || true
docker volume prune -f 2>&1 || true
df -h /
- name: Compute image tag
id: tag
run: |
@@ -106,14 +128,14 @@ jobs:
if: steps.changetype.outputs.asset_only != 'true'
run: |
set -euo pipefail
docker pull us-central1-docker.pkg.dev/neuron-785695/neuron-ci/ci-base:stage
CID=$(docker create us-central1-docker.pkg.dev/neuron-785695/neuron-ci/ci-base:stage)
docker pull us-central1-docker.pkg.dev/neuron-785695/neuron-ci/ci-base:dev
CID=$(docker create us-central1-docker.pkg.dev/neuron-785695/neuron-ci/ci-base:dev)
sudo mkdir -p /opt/el
docker cp "$CID:/opt/el" /opt/
docker rm "$CID"
echo "ELB=/opt/el/dist/bin/elb" >> "$GITHUB_ENV"
echo "ELC=/opt/el/dist/platform/elc" >> "$GITHUB_ENV"
echo "EL_RUNTIME=/opt/el/el-compiler/runtime" >> "$GITHUB_ENV"
echo "EL_RUNTIME=$GITHUB_WORKSPACE/runtime" >> "$GITHUB_ENV"
# ── Build neuron-web binary ───────────────────────────────────────────
@@ -126,31 +148,96 @@ jobs:
--runtime="$EL_RUNTIME"
echo "Binary: $(ls -lh dist/neuron-landing)"
- name: Relink neuron-web with HAVE_CURL
# elb does not pass -DHAVE_CURL when compiling el_runtime.c, so
# http_get/http_post return {"error":"not built with HAVE_CURL"}.
# Fix: after elb generates all intermediate .c files in dist/, recompile
# el_runtime.c with -DHAVE_CURL and relink the whole binary manually.
# All component .c files (nav.c, hero.c, etc.) are generated by elb and
# remain in dist/ after the build — we collect them here, exclude the
# separate soul-demo.c binary, and relink with libcurl.
if: steps.changetype.outputs.asset_only != 'true'
run: |
set -euo pipefail
# Compile el_runtime.c with full curl support
cc -O2 -DHAVE_CURL -c runtime/el_runtime.c -I runtime/ -o /tmp/el_runtime_curl.o
echo "el_runtime_curl.o compiled: $(ls -lh /tmp/el_runtime_curl.o)"
# Collect every neuron-web .c file elb deposited in dist/
# (both committed stubs and freshly-generated component files)
mapfile -t C_SRCS < <(find dist/ -maxdepth 1 -name '*.c' ! -name 'soul-demo.c')
echo "Relinking ${#C_SRCS[@]} C files..."
cc -O2 -rdynamic \
-I runtime/ -I dist/ \
-o dist/neuron-landing \
"${C_SRCS[@]}" /tmp/el_runtime_curl.o \
-lcurl -lpthread -ldl -lm -lssl -lcrypto
echo "Relinked: $(ls -lh dist/neuron-landing)"
# Verification: if compiled WITHOUT HAVE_CURL the stub string
# "not built with HAVE_CURL" is baked into the binary's rodata.
# Its absence confirms curl code is compiled in.
if strings dist/neuron-landing | grep -q 'not built with HAVE_CURL'; then
echo "ERROR: no-curl stub string still in binary — HAVE_CURL not compiled"
exit 1
fi
# Confirm curl symbols visible in dynamic table
nm -D dist/neuron-landing | grep curl_easy_init || \
nm dist/neuron-landing | grep curl || true
echo "HAVE_CURL verified ✓"
# ── Compile JS client sources ─────────────────────────────────────────
- name: Compile JS El sources
if: steps.changetype.outputs.asset_only != 'true'
run: |
set -euo pipefail
echo "ELC=$ELC"
echo "EL_RUNTIME=$EL_RUNTIME"
echo "el_runtime.js: $(ls -lh "$EL_RUNTIME/el_runtime.js" 2>&1)"
cp "$EL_RUNTIME/el_runtime.js" src/js/
mkdir -p dist/js
for f in src/js/*.el; do
[ -f "$f" ] || continue
name=$(basename "$f" .el)
"$ELC" --target=js --bundle --minify --obfuscate "$f" > "dist/js/${name}.js"
echo "Compiling $f..."
"$ELC" --target=js --bundle --minify --obfuscate "$f" > "dist/js/${name}.js" || {
echo "elc FAILED on $f"
exit 1
}
echo " compiled: $f -> dist/js/${name}.js"
done
rm -f src/js/el_runtime.js
# ── Docker build + push ───────────────────────────────────────────────
- name: Build soul-demo binary
# Compile soul-demo directly on the host runner (ci-base has gcc).
# Cloud Run runs soul-demo as a direct subprocess with a watchdog loop —
# no k3s, no OCI image needed. One binary per container; Cloud Run
# handles horizontal scaling.
# Moved AFTER JS compilation to avoid Docker memory pressure killing elc.
if: steps.changetype.outputs.asset_only != 'true'
run: |
set -euo pipefail
cc -O2 -DHAVE_CURL -c runtime/el_runtime.c -I runtime/ -o /tmp/el_runtime.o
cc -O2 -rdynamic -DEL_SOUL_DEMO_BUILD \
-I runtime/ \
-o dist/soul-demo \
dist/soul-demo.c dist/vessel_stubs.c /tmp/el_runtime.o \
-lcurl -lpthread -ldl -lm -lssl -lcrypto
echo "soul-demo compiled: $(ls -lh dist/soul-demo)"
- name: Build and tag image
if: steps.changetype.outputs.asset_only != 'true'
run: |
set -euo pipefail
# --no-cache: prevents reuse of corrupted overlay2 layers from prior failed runs.
# Dockerfile.stage is now single-stage (no builder) so build is fast even without cache.
docker build \
--build-arg BUILDKIT_INLINE_CACHE=1 \
--cache-from us-central1-docker.pkg.dev/neuron-785695/neuron-marketing/marketing:stage-latest \
--no-cache \
-f Dockerfile.stage \
-t "marketing:${{ steps.tag.outputs.tag }}" \
.
+3
View File
@@ -28,6 +28,9 @@ src/assets/js/
!dist/vessel_stubs.c
!dist/soul-demo.c
!dist/page_close.c
!dist/page_css.c
!dist/page_ga.c
!dist/page_schema.c
!dist/elhtml_impl.c
!dist/entrypoint.sh
!dist/engram-snapshot.json
+15 -63
View File
@@ -4,80 +4,36 @@
# - neuron-web on port 8080 (landing page server)
# - soul-demo on port 7772 (demo chat, localhost only)
#
# neuron-web is built by `elb build` in CI (not here). elb compiles each
# .el source independently and links the result — no combined mega-file,
# no exponential memory growth. The binary lands at dist/neuron-landing
# (linux/amd64) and is COPY'd directly into the runtime image.
# All binaries (neuron-web, soul-demo) are pre-built by CI on the host runner
# before this Dockerfile runs. This keeps the Docker build single-stage with
# no compilation and no network downloads.
#
# soul-demo.c is pre-committed (small, no OOM risk) and compiled here.
# CI pre-build steps (in stage.yaml):
# - neuron-web: built by `elb build` → dist/neuron-landing
# - soul-demo: compiled by cc on host → dist/soul-demo
# ── Stage 1: compile soul-demo ────────────────────────────────────────────────
FROM debian:bookworm-slim AS builder
FROM ubuntu:24.04
ENV DEBIAN_FRONTEND=noninteractive
RUN apt-get update \
&& apt-get install -y --no-install-recommends \
build-essential \
curl \
libcurl4-openssl-dev \
libssl-dev \
ca-certificates \
&& rm -rf /var/lib/apt/lists/*
WORKDIR /build
COPY runtime/el_runtime.c runtime/el_runtime.h ./
# Pre-compile el_runtime as a separate cached layer.
# el_runtime.c changes rarely; main.c changes every run.
# Splitting this out means el_runtime.o is cached across builds when only main.c changes.
# -DHAVE_CURL: the staged el_runtime.c (from el.git) guards the OTLP observability
# section (emit_metric, emit_log, trace_span_*) behind #ifdef HAVE_CURL.
# libcurl IS installed above, so define HAVE_CURL to enable those functions.
RUN cc -O2 -DHAVE_CURL -c el_runtime.c -I. -o el_runtime.o
COPY dist/soul-demo.c dist/vessel_stubs.c ./
RUN cc -O2 -rdynamic \
-o soul-demo \
soul-demo.c vessel_stubs.c el_runtime.o \
-lcurl -lpthread -ldl -lm -lssl -lcrypto
# ── Download k3s binary ───────────────────────────────────────────────────────
RUN curl -fL https://github.com/k3s-io/k3s/releases/download/v1.32.4%2Bk3s1/k3s -o /usr/local/bin/k3s \
&& chmod +x /usr/local/bin/k3s
# ── Stage 2: runtime image ────────────────────────────────────────────────────
FROM debian:bookworm-slim
RUN apt-get update \
&& apt-get install -y --no-install-recommends \
libcurl4 \
libssl3 \
libcurl4t64 \
libssl3t64 \
ca-certificates \
&& rm -rf /var/lib/apt/lists/* \
&& groupadd -r landing && useradd -r -g landing landing \
&& mkdir -p /srv/landing/assets /srv/landing/js /srv/landing/shares \
&& mkdir -p /srv/soul/engram-demo \
&& chown -R landing:landing /srv/landing /srv/soul \
&& mkdir -p /var/lib/rancher/k3s /tmp/k3s \
&& chown -R landing:landing /var/lib/rancher /tmp/k3s
&& chown -R landing:landing /srv/landing /srv/soul
# neuron-web binary — produced by `elb build` in CI (linux/amd64)
COPY dist/neuron-landing /usr/local/bin/neuron-web
RUN chmod +x /usr/local/bin/neuron-web
COPY --from=builder /build/soul-demo /usr/local/bin/soul-demo
# k3s binary
COPY --from=builder /usr/local/bin/k3s /usr/local/bin/k3s
# soul-demo OCI image tar — k3s imports this at startup (no registry needed)
RUN mkdir -p /var/lib/rancher/k3s/agent/images
COPY dist/soul-demo-image.tar /var/lib/rancher/k3s/agent/images/soul-demo.tar
# k3s manifests — auto-applied when k3s starts
RUN mkdir -p /var/lib/rancher/k3s/server/manifests
COPY dist/k3s-soul-demo.yaml /var/lib/rancher/k3s/server/manifests/soul-demo.yaml
# soul-demo binary — compiled by cc on host runner in CI
COPY dist/soul-demo /usr/local/bin/soul-demo
RUN chmod +x /usr/local/bin/soul-demo
# Engram snapshot — baked in so soul has memory from cold start
COPY dist/engram-snapshot.json /srv/soul/engram-demo/snapshot.json
@@ -101,11 +57,7 @@ ENV LANDING_ROOT=/srv/landing
ENV PORT=8080
ENV NEURON_HOME=/srv/soul/engram-demo
ENV NEURON_PORT=7772
ENV K3S_DATA_DIR=/var/lib/rancher/k3s
ENV KUBECONFIG=/var/lib/rancher/k3s/server/cred/admin.kubeconfig
# k3s requires root to create network namespaces and mount cgroups.
# Cloud Run gen2 sandbox is the security boundary here.
EXPOSE 8080
CMD ["/usr/local/bin/entrypoint.sh"]
+19 -28
View File
@@ -1,35 +1,26 @@
#!/bin/sh
set -e
echo "[entrypoint] Starting k3s server (embedded soul-demo orchestrator)..."
if [ "${SKIP_K3S:-0}" = "1" ]; then
echo "[entrypoint] SKIP_K3S=1: starting neuron-web directly (no soul-demo)."
exec /usr/local/bin/neuron-web
fi
# k3s server — single-node mode, disable unused components
# --disable traefik,servicelb: we don't need an ingress or LB
# --disable metrics-server: saves ~50MB RAM
# --write-kubeconfig-mode=644: allow non-root reads
# --data-dir: use the pre-chowned dir
k3s server \
--disable traefik \
--disable servicelb \
--disable metrics-server \
--write-kubeconfig-mode=644 \
--data-dir /var/lib/rancher/k3s \
--node-name soul-node &
K3S_PID=$!
echo "[entrypoint] Waiting for k3s to become ready..."
until k3s kubectl get nodes --no-headers 2>/dev/null | grep -q "Ready"; do
sleep 2
done
echo "[entrypoint] k3s ready. soul-demo Deployment will be applied automatically from manifests."
# Wait for soul-demo pod to be Running before starting neuron-web
echo "[entrypoint] Waiting for soul-demo pod..."
until k3s kubectl get pods -l app=soul-demo --no-headers 2>/dev/null | grep -q "Running"; do
sleep 3
done
echo "[entrypoint] soul-demo is running."
# Soul-demo watchdog: start soul-demo and restart it automatically on crash.
# Cloud Run gen2 doesn't reliably provide eth0 with a unicast IP, so k3s flannel
# fails at startup. Running soul-demo directly is simpler, lighter, and fully
# self-healing. Cloud Run handles horizontal scaling — no HPA needed.
echo "[entrypoint] Starting soul-demo watchdog on :${NEURON_PORT:-7772}..."
(
while true; do
echo "[soul-watchdog] starting soul-demo (NEURON_HOME=${NEURON_HOME})"
/usr/local/bin/soul-demo 2>&1 || true
echo "[soul-watchdog] soul-demo exited, restarting in 3s..."
sleep 3
done
) &
# Start neuron-web immediately — do NOT block.
# Cloud Run startup probe requires port 8080 to answer within the timeout.
echo "[entrypoint] Starting neuron-web on port ${PORT:-8080}..."
exec /usr/local/bin/neuron-web
+3 -3
View File
File diff suppressed because one or more lines are too long
+1827
View File
File diff suppressed because it is too large Load Diff
+15
View File
@@ -0,0 +1,15 @@
#include <stdint.h>
#include <stdlib.h>
#include "el_runtime.h"
el_val_t page_ga_script(void);
el_val_t page_ga_script(void) {
return EL_STR("<script>\n"
" window.dataLayer = window.dataLayer || [];\n"
" function gtag(){dataLayer.push(arguments);}\n"
" gtag('js', new Date());\n"
" gtag('config', 'G-Y1EE43X9RN');\n"
" gtag('config', 'AW-18140150015');\n"
" </script>");
}
+110
View File
@@ -0,0 +1,110 @@
#include <stdint.h>
#include <stdlib.h>
#include "el_runtime.h"
el_val_t page_schema(void);
el_val_t page_schema(void) {
return EL_STR("<script type=\"application/ld+json\">\n"
" {\n"
" \"@context\": \"https://schema.org\",\n"
" \"@graph\": [\n"
" {\n"
" \"@type\": \"Organization\",\n"
" \"name\": \"Neuron, LLC\",\n"
" \"url\": \"https://neurontechnologies.ai\",\n"
" \"founder\": {\n"
" \"@type\": \"Person\",\n"
" \"name\": \"Will Anderson\",\n"
" \"jobTitle\": \"Founder\"\n"
" },\n"
" \"description\": \"Neuron builds AI that runs on your machine, builds a memory over time, and gets sharper the longer you use it. One builder. Built different.\",\n"
" \"foundingDate\": \"2026\",\n"
" \"sameAs\": [\"https://github.com/neuron-technologies\"]\n"
" },\n"
" {\n"
" \"@type\": \"SoftwareApplication\",\n"
" \"name\": \"Neuron\",\n"
" \"applicationCategory\": \"AIApplication\",\n"
" \"operatingSystem\": \"macOS, Windows, Linux\",\n"
" \"offers\": [\n"
" {\n"
" \"@type\": \"Offer\",\n"
" \"name\": \"Free\",\n"
" \"price\": \"0\",\n"
" \"priceCurrency\": \"USD\"\n"
" },\n"
" {\n"
" \"@type\": \"Offer\",\n"
" \"name\": \"Professional\",\n"
" \"price\": \"19\",\n"
" \"priceCurrency\": \"USD\",\n"
" \"billingIncrement\": \"monthly\"\n"
" },\n"
" {\n"
" \"@type\": \"Offer\",\n"
" \"name\": \"Founding Member\",\n"
" \"description\": \"Lifetime access for the first 1,000 members.\",\n"
" \"price\": \"199\",\n"
" \"priceCurrency\": \"USD\"\n"
" }\n"
" ],\n"
" \"description\": \"The AI that remembers you. Runs locally on your machine. Builds a persistent memory over every conversation. Gets sharper the longer you use it. Your data never leaves your device.\",\n"
" \"featureList\": [\n"
" \"Persistent memory — runs locally, never resets\",\n"
" \"Bring your own API keys (OpenAI, Anthropic, Grok)\",\n"
" \"Local inference via Ollama — coming, offline, zero cloud\",\n"
" \"Neuron Inference coming soon — priced below major APIs\",\n"
" \"Two devices included per plan\",\n"
" \"Enterprise deployment — Q1 2027\"\n"
" ]\n"
" },\n"
" {\n"
" \"@type\": \"FAQPage\",\n"
" \"mainEntity\": [\n"
" {\n"
" \"@type\": \"Question\",\n"
" \"name\": \"What is Neuron?\",\n"
" \"acceptedAnswer\": {\n"
" \"@type\": \"Answer\",\n"
" \"text\": \"Neuron is an AI that runs on your machine and builds a persistent memory over time. Every other AI forgets you when you close the tab. Neuron doesn&#39;t. The longer you use it, the less you have to explain.\"\n"
" }\n"
" },\n"
" {\n"
" \"@type\": \"Question\",\n"
" \"name\": \"Does Neuron send my data to the cloud?\",\n"
" \"acceptedAnswer\": {\n"
" \"@type\": \"Answer\",\n"
" \"text\": \"No. Your memory lives on your machine and never leaves it. Neuron does not collect your data, train on your conversations, or require cloud storage. For inference you choose: local via Ollama, your own API keys, or Neuron Inference (coming soon).\"\n"
" }\n"
" },\n"
" {\n"
" \"@type\": \"Question\",\n"
" \"name\": \"How much does Neuron cost?\",\n"
" \"acceptedAnswer\": {\n"
" \"@type\": \"Answer\",\n"
" \"text\": \"Neuron has a free tier that never expires. Professional is $19/month. Founding Member is $199 lifetime — available to the first 1,000 members only. All plans include two devices.\"\n"
" }\n"
" },\n"
" {\n"
" \"@type\": \"Question\",\n"
" \"name\": \"Who built Neuron?\",\n"
" \"acceptedAnswer\": {\n"
" \"@type\": \"Answer\",\n"
" \"text\": \"Will Anderson. On April 22nd, 2026 he had a meeting with one of the largest technology companies in the world. Within two days their lawyers were engaged. He decided to build it himself. On April 25th he published the proof.\"\n"
" }\n"
" },\n"
" {\n"
" \"@type\": \"Question\",\n"
" \"name\": \"When does Neuron launch?\",\n"
" \"acceptedAnswer\": {\n"
" \"@type\": \"Answer\",\n"
" \"text\": \"Neuron is available for preorder now. Enterprise deployment launches Q1 2027.\"\n"
" }\n"
" }\n"
" ]\n"
" }\n"
" ]\n"
" }\n"
" </script>");
}
+25
View File
@@ -6,6 +6,31 @@
#include <unistd.h>
#include "el_runtime.h"
/* Pre-register the El HTTP handler so it is found by http_lookup_active()
* regardless of whether the binary was linked with -rdynamic.
*
* el_runtime's http_set_handler resolves handler names via:
* dlsym(RTLD_DEFAULT, "handle_request")
* but dlsym only searches the dynamic symbol table, which only contains
* user-defined symbols when the executable is linked with -rdynamic.
* elb does not add -rdynamic, so dlsym returns NULL and routes return
* "el-runtime: no http handler registered" even though http_serve is called.
*
* The fix: forward-declare handle_request here and register it directly
* via el_runtime_register_handler before main() runs. This populates the
* handler registry so http_lookup_active() finds it without needing dlsym.
*/
extern el_val_t handle_request(el_val_t method, el_val_t path, el_val_t body);
/* el_runtime_register_handler is intentionally not declared in el_runtime.h
* ("extern lookup works since C symbols are global" — runtime comment). */
extern void el_runtime_register_handler(const char* name,
el_val_t (*fn)(el_val_t, el_val_t, el_val_t));
__attribute__((constructor))
static void pre_register_http_handlers(void) {
el_runtime_register_handler("handle_request", handle_request);
}
el_val_t http_get_auth(el_val_t url, el_val_t tok) {
char bearer[2048]; snprintf(bearer, sizeof(bearer), "Bearer %s", EL_CSTR(tok));
el_val_t hdr_val = EL_STR(bearer);
+11
View File
@@ -13,4 +13,15 @@ build {
c_source "dist/vessel_stubs.c"
c_source "dist/elhtml_impl.c"
c_source "dist/page_close.c"
c_source "dist/page_css.c"
c_source "dist/page_ga.c"
c_source "dist/page_schema.c"
// NOTE: neuron-web requires el_runtime.c to be compiled with -DHAVE_CURL
// so that http_get/http_post forward to libcurl instead of returning
// {"error":"not built with HAVE_CURL"}. The elb binary in ci-base:dev
// hardcodes -DHAVE_CURL in its cc invocation, but older elb versions may
// not. manifest.el does not support c_flags or link_flags directives
// if upgrading elb breaks HTTP, ensure ci-base:dev ships an elb built
// with HAVE_CURL enabled in its hardcoded cc command, or pre-compile
// el_runtime.o with -DHAVE_CURL on the host and pass it as a c_source.
}
+98 -30
View File
@@ -1331,12 +1331,19 @@ static void http_emit_headers_from_map(JsonBuf* b, el_val_t headers_map,
/* Parse the envelope produced by http_response(). On success returns 1 and
* populates *out_status, *out_headers_map (an ElMap el_val_t caller must
* el_release), and *out_body (allocated). On failure returns 0.
* el_release via out_parsed_root), and *out_body (malloc'd, caller frees).
* On failure returns 0.
*
* Implementation: feeds the entire envelope through the recursive-descent
* JSON parser (which builds proper ElMap/ElList values), then pulls the
* three top-level fields by name. Avoids re-stringifying the headers map
* since json_stringify() does not support nested objects. */
* Implementation: manual field scanner does NOT run json_parse on the full
* envelope. Running the recursive-descent JSON parser on a 4050 KB envelope
* (common when the body contains minified/obfuscated JavaScript) fails because
* the parser allocates intermediate ElMap nodes for the whole structure.
* Instead we scan directly:
* "status" strtol scan
* "headers" brace-depth scan to extract the object literal, then
* json_parse only that small substring (always < 1 KB)
* "body" jp_parse_string_raw to unescape the JSON string in one pass,
* without building any intermediate data structures */
static int http_parse_envelope(const char* s, int* out_status,
el_val_t* out_headers_map, char** out_body,
el_val_t* out_parsed_root) {
@@ -1344,37 +1351,91 @@ static int http_parse_envelope(const char* s, int* out_status,
if (strncmp(s, EL_HTTP_RESPONSE_TAG,
sizeof(EL_HTTP_RESPONSE_TAG) - 1) != 0) return 0;
el_val_t parsed = json_parse(EL_STR(s));
if (parsed == EL_NULL) return 0;
int status = 200;
el_val_t hmap = 0;
char* body = NULL;
el_val_t sv = el_map_get(parsed, EL_STR("status"));
if (sv != 0) {
/* status comes back as an integer — el_val_t holds it directly. */
long sc = (long)sv;
if (sc >= 100 && sc <= 599) status = (int)sc;
/* ── status ──────────────────────────────────────────────────────────── */
int status = 200;
{
const char* sp = strstr(s, "\"status\":");
if (sp) {
const char* np = sp + 9;
while (*np == ' ' || *np == '\t') np++;
long sc = strtol(np, NULL, 10);
if (sc >= 100 && sc <= 599) status = (int)sc;
}
}
el_val_t hv = el_map_get(parsed, EL_STR("headers"));
if (hv != 0) {
ElMap* hm = (ElMap*)(uintptr_t)hv;
if (hm && hm->hdr.magic == EL_MAGIC_MAP) hmap = hv;
/* ── headers ─────────────────────────────────────────────────────────── */
el_val_t hmap = 0;
el_val_t parsed_hdrs = EL_NULL;
{
const char* hp = strstr(s, "\"headers\":");
if (hp) {
hp += 10;
while (*hp == ' ' || *hp == '\t') hp++;
if (*hp == '{') {
/* Scan for matching '}', honouring nested objects and strings */
const char* hobj_start = hp;
const char* cp = hp + 1;
int depth = 1, in_str = 0;
while (*cp && depth > 0) {
if (in_str) {
if (*cp == '\\' && *(cp + 1)) { cp += 2; continue; }
if (*cp == '"') in_str = 0;
} else {
if (*cp == '"') in_str = 1;
else if (*cp == '{') depth++;
else if (*cp == '}') { if (--depth == 0) break; }
}
cp++;
}
if (depth == 0) {
/* cp points at the closing '}'; extract the object literal */
size_t hlen = (size_t)(cp - hobj_start + 1);
char* hobj = malloc(hlen + 1);
if (hobj) {
memcpy(hobj, hobj_start, hlen);
hobj[hlen] = '\0';
/* Headers are always simple k/v string pairs — json_parse
* is safe on this small substring (typically < 1 KB). */
parsed_hdrs = json_parse(EL_STR(hobj));
free(hobj);
if (parsed_hdrs != EL_NULL) {
ElMap* hm = (ElMap*)(uintptr_t)parsed_hdrs;
if (hm && hm->hdr.magic == EL_MAGIC_MAP) hmap = parsed_hdrs;
}
}
}
}
}
}
el_val_t bv = el_map_get(parsed, EL_STR("body"));
if (bv != 0) {
const char* bs = EL_CSTR(bv);
if (bs) body = el_strdup(bs);
/* ── body ────────────────────────────────────────────────────────────── */
/* Search forward so we don't accidentally match "body": inside a header
* value. http_response() always appends the body field last. */
char* body = NULL;
{
const char* bp = strstr(s, "\"body\":");
if (bp) {
bp += 7;
while (*bp == ' ' || *bp == '\t') bp++;
if (*bp == '"') {
/* jp_parse_string_raw unescapes a JSON string in one pass,
* producing a plain malloc'd C string. Caller frees it. */
JsonParser jp = { .p = bp, .end = bp + strlen(bp), .err = 0 };
char* parsed = jp_parse_string_raw(&jp);
if (!jp.err) {
body = parsed;
} else {
free(parsed);
}
}
}
if (!body) body = strdup("");
}
if (!body) body = el_strdup("");
*out_status = status;
*out_headers_map = hmap;
*out_body = body;
*out_parsed_root = parsed; /* caller releases to free hmap + entries */
*out_status = status;
*out_headers_map = hmap;
*out_body = body;
*out_parsed_root = parsed_hdrs; /* caller el_release()s to free hmap */
return 1;
}
@@ -1900,6 +1961,13 @@ el_val_t http_response(el_val_t status, el_val_t headers_json, el_val_t body) {
const char* b = EL_CSTR(body);
if (!b) b = "";
/* Clear the fs_read binary-length hint: the envelope we're about to build
* is a fresh JSON string, not the raw file bytes. Without this reset,
* http_worker would use the stale _tl_fs_read_len (= original file size)
* to copy the response truncating the larger envelope before it reaches
* http_send_response and http_parse_envelope. */
_tl_fs_read_len = 0;
JsonBuf out; jb_init(&out);
jb_puts(&out, EL_HTTP_RESPONSE_TAG); /* {"el_http_response":1 */
jb_puts(&out, ",\"status\":");
+26
View File
@@ -878,6 +878,32 @@ el_val_t __uuid_v4(void);
/* Args */
el_val_t __args_json(void);
/* ── neuron-web stubs (web_stubs.c) ──────────────────────────────────────────
* Forward declarations so generated C (e.g. dist/main.c) sees the correct
* el_val_t return type instead of an implicit int. Without these, the
* ci-base elb (which does not emit extern-fn forward decls for stub-only
* functions) produces truncated 32-bit returns on 64-bit Linux → segfault.
*
* Guarded by EL_SOUL_DEMO_BUILD: soul-demo.c includes this header but
* defines its own (different-arity) versions of some of these functions.
* Dockerfile.stage compiles soul-demo with -DEL_SOUL_DEMO_BUILD to skip
* this block and avoid conflicting-types errors.
*/
#ifndef EL_SOUL_DEMO_BUILD
el_val_t http_get_auth(el_val_t url, el_val_t tok);
el_val_t http_post_auth(el_val_t url, el_val_t tok, el_val_t body);
el_val_t http_post_auth_json(el_val_t url, el_val_t tok, el_val_t body);
el_val_t http_delete_auth(el_val_t url, el_val_t bearer_tok, el_val_t apikey);
el_val_t supabase_get(el_val_t project_url, el_val_t service_key, el_val_t table_and_query);
el_val_t supabase_insert(el_val_t project_url, el_val_t service_key, el_val_t table, el_val_t row_json);
el_val_t supabase_auth_user(el_val_t project_url, el_val_t anon_key, el_val_t user_jwt);
el_val_t supabase_admin_invite(el_val_t project_url, el_val_t service_key, el_val_t body_json);
el_val_t gcs_write(el_val_t bucket, el_val_t object_name, el_val_t content);
el_val_t gcs_read(el_val_t bucket, el_val_t object_name);
el_val_t cwd(void);
el_val_t color_bold(el_val_t s);
#endif /* EL_SOUL_DEMO_BUILD */
#ifdef __cplusplus
}
#endif
+18 -2
View File
@@ -29,12 +29,19 @@ fn main() -> Void {
el.style.color = isError ? '#c0392b' : '#2ecc71';
}
var _formRevealed = false;
function revealPaymentForm(user) {
if (_formRevealed) return;
_formRevealed = true;
if (user && user.id) { window._neuronSupaId = user.id; }
var auth = document.getElementById('auth-section');
if (auth) auth.style.display = 'none';
var isFree = (window.NEURON_CFG || {}).plan === 'free';
if (!isFree) {
if (isFree) {
// Free plan: show the success panel (user is signed in or just signed up)
var freeSuccess = document.getElementById('free-success');
if (freeSuccess) freeSuccess.style.display = '';
} else {
var payment = document.getElementById('payment-section');
if (payment) payment.style.display = '';
}
@@ -68,7 +75,16 @@ fn main() -> Void {
function checkExistingSession() {
initSupabase(function() {
supabaseClient.auth.getUser().then(function(res) {
if (res.data && res.data.user) { revealPaymentForm(res.data.user); }
if (res.data && res.data.user) {
revealPaymentForm(res.data.user);
} else {
// No existing session — for paid plans, init Stripe immediately.
// Auth is optional on paid plans; the user can link their account later.
var isFree = (window.NEURON_CFG || {}).plan === 'free';
if (!isFree && typeof window.initStripe === 'function') {
window.initStripe('', '');
}
}
});
});
}
+18 -1
View File
@@ -907,6 +907,10 @@ fn handle_request_inner(method: String, path: String, body: String) -> String {
// Compiled client-side JS: /js/*
// Served from dist/js/ (compiled by elc --target=js at build time).
// LANDING_ROOT/js maps to the dist/js output directory in the image.
// Returns an http_response envelope with explicit Content-Type so the
// browser executes the file as JavaScript http_detect_content_type()
// mis-identifies minified/obfuscated JS as JSON because many obfuscated
// bundles start with '[' (which is also a JSON array opener).
if str_starts_with(path, "/js/") {
let rel: String = str_slice(path, 4, str_len(path))
let abs: String = src_dir + "/js/" + rel
@@ -914,7 +918,7 @@ fn handle_request_inner(method: String, path: String, body: String) -> String {
if str_eq(content, "") {
return "{\"__status__\":404,\"error\":\"not found\"}"
}
return content
return http_response(200, js_headers_json(), content)
}
// Brand assets: /brand/*
@@ -1936,6 +1940,19 @@ fn sec_headers_json() -> String {
+ "\"Content-Security-Policy\":\"default-src 'self'; script-src 'self' 'unsafe-inline' https://challenges.cloudflare.com https://cdn.jsdelivr.net https://www.googletagmanager.com https://www.google-analytics.com; style-src 'self' 'unsafe-inline'; frame-src https://challenges.cloudflare.com; connect-src 'self' https://api.stripe.com https://*.supabase.co; img-src 'self' data: https:; font-src 'self' data:\"}"
}
// Headers for compiled JS assets. Explicitly sets Content-Type so the browser
// treats them as JavaScript regardless of what http_detect_content_type()
// infers from the content (minified/obfuscated JS can trip the JSON heuristic).
fn js_headers_json() -> String {
"{\"Content-Type\":\"application/javascript; charset=utf-8\","
+ "\"Cache-Control\":\"public, max-age=3600\","
+ "\"Strict-Transport-Security\":\"max-age=63072000; includeSubDomains; preload\","
+ "\"X-Content-Type-Options\":\"nosniff\","
+ "\"X-Frame-Options\":\"SAMEORIGIN\","
+ "\"Referrer-Policy\":\"strict-origin-when-cross-origin\","
+ "\"Permissions-Policy\":\"geolocation=(), microphone=(), camera=()\"}"
}
fn handle_request(method: String, path: String, body: String) -> String {
let inner_resp: String = handle_request_inner(method, path, body)
// Detect envelope already set by inner handler (starts with
+52 -2017
View File
File diff suppressed because it is too large Load Diff