Compare commits
40 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 3020b4e902 | |||
| 00f05f813e | |||
| 93f9ea2be2 | |||
| e82425a829 | |||
| e480aba2f1 | |||
| feee40c34b | |||
| c4cdb31529 | |||
| e6e89a1f4d | |||
| 8b8cb2f580 | |||
| a1c0cc090d | |||
| 4d359ff021 | |||
| cd1c6737e8 | |||
| 7df96a2273 | |||
| f27fc2622c | |||
| 0433fe8c0f | |||
| d3b890b739 | |||
| 9da4d50883 | |||
| c99ca82302 | |||
| 3f069eeb79 | |||
| e292453905 | |||
| 0263e51407 | |||
| 8676751ed6 | |||
| b4935ed880 | |||
| a4f5312069 | |||
| ee0147869e | |||
| c76e5a19eb | |||
| 25f6631049 | |||
| 58b7b32cdd | |||
| 0fdabcce86 | |||
| 79de47de2c | |||
| 45963154d9 | |||
| aabaa2ffb0 | |||
| d5dcb08ec6 | |||
| 20a36eeb9e | |||
| 32a179c24a | |||
| 6bc026de19 | |||
| 0ae526b72e | |||
| 8221aef605 | |||
| f8487c43a0 | |||
| 36b99dd9e2 |
@@ -172,11 +172,12 @@ jobs:
|
|||||||
- name: Touch HTML placeholder files
|
- name: Touch HTML placeholder files
|
||||||
run: touch src/index.html src/about.html src/terms.html src/enterprise-terms.html
|
run: touch src/index.html src/about.html src/terms.html src/enterprise-terms.html
|
||||||
|
|
||||||
- name: Create soul-demo-image.tar placeholder
|
- name: Create soul-demo placeholder
|
||||||
# Dockerfile.stage COPYs this file (used by k3s at runtime).
|
# Dockerfile.stage COPYs dist/soul-demo. We only need the binary to exist
|
||||||
# We only need the COPY to succeed here; real tar is built by
|
# for the Docker build to succeed; the real binary is compiled in stage CI.
|
||||||
# build-stage.sh in the deploy pipeline.
|
run: |
|
||||||
run: touch dist/soul-demo-image.tar
|
touch dist/soul-demo
|
||||||
|
chmod +x dist/soul-demo
|
||||||
|
|
||||||
- name: Build Docker image (local only — no push)
|
- name: Build Docker image (local only — no push)
|
||||||
run: |
|
run: |
|
||||||
|
|||||||
+101
-22
@@ -12,6 +12,7 @@ on:
|
|||||||
- 'dist/**'
|
- 'dist/**'
|
||||||
- 'runtime/**'
|
- 'runtime/**'
|
||||||
- 'Dockerfile.stage'
|
- 'Dockerfile.stage'
|
||||||
|
- 'Dockerfile.soul-demo'
|
||||||
- 'build-stage.sh'
|
- 'build-stage.sh'
|
||||||
- '.gitea/workflows/stage.yaml'
|
- '.gitea/workflows/stage.yaml'
|
||||||
|
|
||||||
@@ -148,6 +149,46 @@ jobs:
|
|||||||
--runtime="$EL_RUNTIME"
|
--runtime="$EL_RUNTIME"
|
||||||
echo "Binary: $(ls -lh dist/neuron-landing)"
|
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 ─────────────────────────────────────────
|
# ── Compile JS client sources ─────────────────────────────────────────
|
||||||
|
|
||||||
- name: Compile JS El sources
|
- name: Compile JS El sources
|
||||||
@@ -173,15 +214,15 @@ jobs:
|
|||||||
|
|
||||||
# ── Docker build + push ───────────────────────────────────────────────
|
# ── Docker build + push ───────────────────────────────────────────────
|
||||||
|
|
||||||
- name: Build soul-demo image tar
|
- name: Build soul-demo binary
|
||||||
# Dockerfile.stage COPYs dist/soul-demo-image.tar so k3s can import
|
# Compile soul-demo directly on the host runner (ci-base has gcc).
|
||||||
# soul-demo:local at runtime. We compile soul-demo from source on the
|
# Cloud Run runs soul-demo as a direct subprocess with a watchdog loop —
|
||||||
# host runner (ci-base has gcc), build a minimal OCI image, and save it.
|
# 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.
|
# Moved AFTER JS compilation to avoid Docker memory pressure killing elc.
|
||||||
if: steps.changetype.outputs.asset_only != 'true'
|
if: steps.changetype.outputs.asset_only != 'true'
|
||||||
run: |
|
run: |
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
# Compile el_runtime.o and soul-demo on the host runner
|
|
||||||
cc -O2 -DHAVE_CURL -c runtime/el_runtime.c -I runtime/ -o /tmp/el_runtime.o
|
cc -O2 -DHAVE_CURL -c runtime/el_runtime.c -I runtime/ -o /tmp/el_runtime.o
|
||||||
cc -O2 -rdynamic -DEL_SOUL_DEMO_BUILD \
|
cc -O2 -rdynamic -DEL_SOUL_DEMO_BUILD \
|
||||||
-I runtime/ \
|
-I runtime/ \
|
||||||
@@ -189,26 +230,49 @@ jobs:
|
|||||||
dist/soul-demo.c dist/vessel_stubs.c /tmp/el_runtime.o \
|
dist/soul-demo.c dist/vessel_stubs.c /tmp/el_runtime.o \
|
||||||
-lcurl -lpthread -ldl -lm -lssl -lcrypto
|
-lcurl -lpthread -ldl -lm -lssl -lcrypto
|
||||||
echo "soul-demo compiled: $(ls -lh dist/soul-demo)"
|
echo "soul-demo compiled: $(ls -lh dist/soul-demo)"
|
||||||
# Package as minimal OCI image for k3s import
|
|
||||||
# --no-cache: prevents reuse of corrupted overlay2 layers from prior failed runs
|
|
||||||
docker build --no-cache -f dist/Dockerfile.soul-demo -t soul-demo:local dist/
|
|
||||||
docker save soul-demo:local -o dist/soul-demo-image.tar
|
|
||||||
echo "soul-demo-image.tar: $(du -sh dist/soul-demo-image.tar | cut -f1)"
|
|
||||||
docker rmi soul-demo:local 2>/dev/null || true
|
|
||||||
|
|
||||||
- name: Download k3s binary
|
- name: Build and push soul-demo image
|
||||||
# Pre-download k3s on the host runner so Dockerfile.stage can COPY it
|
|
||||||
# directly. Previously k3s was downloaded inside the Docker builder stage,
|
|
||||||
# which combined with build-essential and C compilation caused RWLayer nil
|
|
||||||
# corruption on the runner's overlay2 driver. Host-runner download is safe.
|
|
||||||
if: steps.changetype.outputs.asset_only != 'true'
|
if: steps.changetype.outputs.asset_only != 'true'
|
||||||
|
id: soul-image
|
||||||
run: |
|
run: |
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
curl -fL --retry 3 --retry-delay 10 \
|
SOUL_IMAGE="us-central1-docker.pkg.dev/neuron-785695/neuron-marketing/soul-demo:${{ steps.tag.outputs.tag }}"
|
||||||
https://github.com/k3s-io/k3s/releases/download/v1.32.4%2Bk3s1/k3s \
|
docker build --no-cache \
|
||||||
-o dist/k3s
|
-f Dockerfile.soul-demo \
|
||||||
chmod +x dist/k3s
|
-t "soul-demo:${{ steps.tag.outputs.tag }}" \
|
||||||
echo "k3s: $(ls -lh dist/k3s)"
|
.
|
||||||
|
docker tag "soul-demo:${{ steps.tag.outputs.tag }}" "$SOUL_IMAGE"
|
||||||
|
docker tag "soul-demo:${{ steps.tag.outputs.tag }}" \
|
||||||
|
"us-central1-docker.pkg.dev/neuron-785695/neuron-marketing/soul-demo:stage-latest"
|
||||||
|
docker push "$SOUL_IMAGE"
|
||||||
|
docker push "us-central1-docker.pkg.dev/neuron-785695/neuron-marketing/soul-demo:stage-latest"
|
||||||
|
echo "soul_image=${SOUL_IMAGE}" >> "$GITHUB_OUTPUT"
|
||||||
|
echo "Soul-demo image: ${SOUL_IMAGE}"
|
||||||
|
|
||||||
|
- name: Deploy soul-demo-stage
|
||||||
|
if: steps.changetype.outputs.asset_only != 'true'
|
||||||
|
id: deploy-soul
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
gcloud run deploy soul-demo-stage \
|
||||||
|
--image "${{ steps.soul-image.outputs.soul_image }}" \
|
||||||
|
--region us-central1 \
|
||||||
|
--project neuron-785695 \
|
||||||
|
--service-account neuron-marketing-sa@neuron-785695.iam.gserviceaccount.com \
|
||||||
|
--update-env-vars "NEURON_LLM_0_FORMAT=anthropic,NEURON_LLM_0_MODEL=claude-sonnet-4-5,NEURON_LLM_0_URL=https://api.anthropic.com/v1/messages" \
|
||||||
|
--update-secrets "NEURON_LLM_0_KEY=anthropic-api-key:latest,ANTHROPIC_API_KEY=anthropic-api-key:latest" \
|
||||||
|
--min-instances 1 \
|
||||||
|
--max-instances 10 \
|
||||||
|
--concurrency 20 \
|
||||||
|
--port 8080 \
|
||||||
|
--allow-unauthenticated \
|
||||||
|
--quiet
|
||||||
|
|
||||||
|
SOUL_URL=$(gcloud run services describe soul-demo-stage \
|
||||||
|
--region us-central1 --project neuron-785695 \
|
||||||
|
--format 'value(status.url)')
|
||||||
|
echo "soul_url=${SOUL_URL}" >> "$GITHUB_OUTPUT"
|
||||||
|
echo "Soul-demo URL: ${SOUL_URL}"
|
||||||
|
|
||||||
- name: Build and tag image
|
- name: Build and tag image
|
||||||
if: steps.changetype.outputs.asset_only != 'true'
|
if: steps.changetype.outputs.asset_only != 'true'
|
||||||
@@ -255,6 +319,21 @@ jobs:
|
|||||||
docker push "${LATEST%:*}:stage-latest"
|
docker push "${LATEST%:*}:stage-latest"
|
||||||
echo "Fast asset build complete"
|
echo "Fast asset build complete"
|
||||||
|
|
||||||
|
- name: Resolve soul-demo URL
|
||||||
|
id: soul-url
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
# For full builds: soul_url comes from deploy-soul step output.
|
||||||
|
# For asset-only builds (soul-demo not redeployed): describe existing service.
|
||||||
|
SOUL_URL="${{ steps.deploy-soul.outputs.soul_url }}"
|
||||||
|
if [ -z "$SOUL_URL" ]; then
|
||||||
|
SOUL_URL=$(gcloud run services describe soul-demo-stage \
|
||||||
|
--region us-central1 --project neuron-785695 \
|
||||||
|
--format 'value(status.url)' 2>/dev/null || echo "")
|
||||||
|
fi
|
||||||
|
echo "soul_url=${SOUL_URL}" >> "$GITHUB_OUTPUT"
|
||||||
|
echo "Resolved SOUL_URL: ${SOUL_URL}"
|
||||||
|
|
||||||
- name: Deploy to marketing-stage
|
- name: Deploy to marketing-stage
|
||||||
id: deploy-stage
|
id: deploy-stage
|
||||||
env:
|
env:
|
||||||
@@ -267,7 +346,7 @@ jobs:
|
|||||||
--region us-central1 \
|
--region us-central1 \
|
||||||
--project neuron-785695 \
|
--project neuron-785695 \
|
||||||
--service-account neuron-marketing-sa@neuron-785695.iam.gserviceaccount.com \
|
--service-account neuron-marketing-sa@neuron-785695.iam.gserviceaccount.com \
|
||||||
--update-env-vars "NODE_ENV=production,STRIPE_PUBLISHABLE_KEY=pk_test_51TPoHnJg9Fv1D3AUp1FEMcy4MGlKRZqs4scW66kjQFQjWofmNc2rottzXzDaXekHvuw1OQpyp2WCIsc7O5fXIG0G00HQQrkdGX,GCS_SHARE_BUCKET=neuron-shares-prod,SUPABASE_ANON_KEY=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6Im9jb2pzZ2hhb25sdHVuaWRrenB3Iiwicm9sZSI6ImFub24iLCJpYXQiOjE3Nzc2NDIxNjgsImV4cCI6MjA5MzIxODE2OH0.e0FVFw1aahnrBVvnkR5R8a-RxCx095U8o_gsk7Quq3E,NEURON_LLM_0_FORMAT=anthropic,NEURON_LLM_0_MODEL=claude-sonnet-4-5,NEURON_LLM_0_URL=https://api.anthropic.com/v1/messages" \
|
--update-env-vars "NODE_ENV=production,STRIPE_PUBLISHABLE_KEY=pk_test_51TPoHnJg9Fv1D3AUp1FEMcy4MGlKRZqs4scW66kjQFQjWofmNc2rottzXzDaXekHvuw1OQpyp2WCIsc7O5fXIG0G00HQQrkdGX,GCS_SHARE_BUCKET=neuron-shares-prod,SUPABASE_ANON_KEY=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6Im9jb2pzZ2hhb25sdHVuaWRrenB3Iiwicm9sZSI6ImFub24iLCJpYXQiOjE3Nzc2NDIxNjgsImV4cCI6MjA5MzIxODE2OH0.e0FVFw1aahnrBVvnkR5R8a-RxCx095U8o_gsk7Quq3E,NEURON_LLM_0_FORMAT=anthropic,NEURON_LLM_0_MODEL=claude-sonnet-4-5,NEURON_LLM_0_URL=https://api.anthropic.com/v1/messages,SOUL_URL=${{ steps.soul-url.outputs.soul_url }}" \
|
||||||
--update-secrets "SUPABASE_SERVICE_KEY=supabase-service-key:latest,NEURON_LLM_0_KEY=anthropic-api-key:latest,ANTHROPIC_API_KEY=anthropic-api-key:latest,STRIPE_SECRET_KEY=stripe-secret-key-stage:latest,STRIPE_WEBHOOK_SECRET=stripe-webhook-secret-stage:latest,STRIPE_PRICE_PROFESSIONAL=stripe-price-professional-stage:latest,STRIPE_PRICE_FOUNDING=stripe-price-founding-stage:latest,STRIPE_PRICE_FAMILY_CHILD=stripe-price-family-child:latest,RESEND_API_KEY=resend-api-key:latest,DOCUSEAL_WEBHOOK_TOKEN=docuseal-webhook-token:latest" \
|
--update-secrets "SUPABASE_SERVICE_KEY=supabase-service-key:latest,NEURON_LLM_0_KEY=anthropic-api-key:latest,ANTHROPIC_API_KEY=anthropic-api-key:latest,STRIPE_SECRET_KEY=stripe-secret-key-stage:latest,STRIPE_WEBHOOK_SECRET=stripe-webhook-secret-stage:latest,STRIPE_PRICE_PROFESSIONAL=stripe-price-professional-stage:latest,STRIPE_PRICE_FOUNDING=stripe-price-founding-stage:latest,STRIPE_PRICE_FAMILY_CHILD=stripe-price-family-child:latest,RESEND_API_KEY=resend-api-key:latest,DOCUSEAL_WEBHOOK_TOKEN=docuseal-webhook-token:latest" \
|
||||||
--allow-unauthenticated \
|
--allow-unauthenticated \
|
||||||
--quiet
|
--quiet
|
||||||
|
|||||||
@@ -0,0 +1,32 @@
|
|||||||
|
# Dockerfile.soul-demo — Soul-demo as a standalone Cloud Run service.
|
||||||
|
# Decoupled from neuron-web so it can scale independently.
|
||||||
|
# Built from repo root. soul-demo binary compiled by CI before this runs.
|
||||||
|
|
||||||
|
FROM ubuntu:24.04
|
||||||
|
|
||||||
|
ENV DEBIAN_FRONTEND=noninteractive
|
||||||
|
|
||||||
|
RUN apt-get update \
|
||||||
|
&& apt-get install -y --no-install-recommends \
|
||||||
|
libcurl4t64 \
|
||||||
|
libssl3t64 \
|
||||||
|
ca-certificates \
|
||||||
|
&& rm -rf /var/lib/apt/lists/* \
|
||||||
|
&& groupadd -r soul && useradd -r -g soul soul \
|
||||||
|
&& mkdir -p /srv/soul/engram-demo \
|
||||||
|
&& chown -R soul:soul /srv/soul
|
||||||
|
|
||||||
|
COPY dist/soul-demo /usr/local/bin/soul-demo
|
||||||
|
RUN chmod +x /usr/local/bin/soul-demo
|
||||||
|
|
||||||
|
COPY dist/engram-snapshot.json /srv/soul/engram-demo/snapshot.json
|
||||||
|
RUN chown soul:soul /srv/soul/engram-demo/snapshot.json
|
||||||
|
|
||||||
|
USER soul
|
||||||
|
|
||||||
|
ENV NEURON_HOME=/srv/soul/engram-demo
|
||||||
|
ENV NEURON_PORT=8080
|
||||||
|
|
||||||
|
EXPOSE 8080
|
||||||
|
|
||||||
|
CMD ["/usr/local/bin/soul-demo"]
|
||||||
+7
-39
@@ -1,18 +1,14 @@
|
|||||||
# Dockerfile.stage — Stage build: landing server + soul-demo in one image.
|
# Dockerfile.stage — Stage build: landing server only.
|
||||||
#
|
#
|
||||||
# Both processes run in the same container:
|
# neuron-web runs on port 8080 (landing page server).
|
||||||
# - neuron-web on port 8080 (landing page server)
|
# soul-demo is now a separate Cloud Run service (soul-demo-stage).
|
||||||
# - soul-demo on port 7772 (demo chat, localhost only)
|
|
||||||
#
|
#
|
||||||
# All binaries (neuron-web, soul-demo, k3s) are pre-built by CI on the host
|
# neuron-web binary is pre-built by CI on the host runner before this
|
||||||
# runner before this Dockerfile runs. This keeps the Docker build single-stage
|
# Dockerfile runs. This keeps the Docker build single-stage with no
|
||||||
# with no compilation and no network downloads, eliminating the multi-stage
|
# compilation and no network downloads.
|
||||||
# complexity that caused RWLayer corruption on the runner's overlay2 driver.
|
|
||||||
#
|
#
|
||||||
# CI pre-build steps (in stage.yaml):
|
# CI pre-build steps (in stage.yaml):
|
||||||
# - neuron-web: built by `elb build` → dist/neuron-landing
|
# - neuron-web: built by `elb build` → dist/neuron-landing
|
||||||
# - soul-demo: compiled by cc on host → dist/soul-demo
|
|
||||||
# - k3s: downloaded by curl on host → dist/k3s
|
|
||||||
|
|
||||||
FROM ubuntu:24.04
|
FROM ubuntu:24.04
|
||||||
|
|
||||||
@@ -26,34 +22,12 @@ RUN apt-get update \
|
|||||||
&& rm -rf /var/lib/apt/lists/* \
|
&& rm -rf /var/lib/apt/lists/* \
|
||||||
&& groupadd -r landing && useradd -r -g landing landing \
|
&& groupadd -r landing && useradd -r -g landing landing \
|
||||||
&& mkdir -p /srv/landing/assets /srv/landing/js /srv/landing/shares \
|
&& mkdir -p /srv/landing/assets /srv/landing/js /srv/landing/shares \
|
||||||
&& mkdir -p /srv/soul/engram-demo \
|
&& chown -R landing:landing /srv/landing
|
||||||
&& 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
|
|
||||||
|
|
||||||
# neuron-web binary — produced by `elb build` in CI (linux/amd64)
|
# neuron-web binary — produced by `elb build` in CI (linux/amd64)
|
||||||
COPY dist/neuron-landing /usr/local/bin/neuron-web
|
COPY dist/neuron-landing /usr/local/bin/neuron-web
|
||||||
RUN chmod +x /usr/local/bin/neuron-web
|
RUN chmod +x /usr/local/bin/neuron-web
|
||||||
|
|
||||||
# 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
|
|
||||||
|
|
||||||
# k3s binary — downloaded from GitHub releases by CI
|
|
||||||
COPY dist/k3s /usr/local/bin/k3s
|
|
||||||
RUN chmod +x /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
|
|
||||||
|
|
||||||
# Engram snapshot — baked in so soul has memory from cold start
|
|
||||||
COPY dist/engram-snapshot.json /srv/soul/engram-demo/snapshot.json
|
|
||||||
|
|
||||||
COPY src/assets /srv/landing/assets
|
COPY src/assets /srv/landing/assets
|
||||||
COPY dist/js /srv/landing/js
|
COPY dist/js /srv/landing/js
|
||||||
COPY src/llms.txt /srv/landing/llms.txt
|
COPY src/llms.txt /srv/landing/llms.txt
|
||||||
@@ -71,13 +45,7 @@ RUN chmod +x /usr/local/bin/entrypoint.sh
|
|||||||
|
|
||||||
ENV LANDING_ROOT=/srv/landing
|
ENV LANDING_ROOT=/srv/landing
|
||||||
ENV PORT=8080
|
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
|
EXPOSE 8080
|
||||||
|
|
||||||
CMD ["/usr/local/bin/entrypoint.sh"]
|
CMD ["/usr/local/bin/entrypoint.sh"]
|
||||||
|
|||||||
Vendored
+1
-38
@@ -1,41 +1,4 @@
|
|||||||
#!/bin/sh
|
#!/bin/sh
|
||||||
set -e
|
set -e
|
||||||
|
echo "[entrypoint] Starting neuron-web on port ${PORT:-8080}..."
|
||||||
# SKIP_K3S=1 — bypass k3s/soul-demo startup and go straight to neuron-web.
|
|
||||||
# Used by the dev CI smoke test where the container runtime doesn't support
|
|
||||||
# the kernel capabilities k3s requires (overlayfs / privileged mode).
|
|
||||||
if [ "${SKIP_K3S:-0}" = "1" ]; then
|
|
||||||
echo "[entrypoint] SKIP_K3S=1: starting neuron-web directly (no k3s/soul-demo)."
|
|
||||||
exec /usr/local/bin/neuron-web
|
|
||||||
fi
|
|
||||||
|
|
||||||
echo "[entrypoint] Starting k3s server (embedded soul-demo orchestrator)..."
|
|
||||||
|
|
||||||
# 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
|
|
||||||
# --flannel-iface=eth0: explicitly set the network interface.
|
|
||||||
# Cloud Run gen2 provides eth0 but k3s default IP detection walks the routing
|
|
||||||
# table looking for a default route, which fails in Cloud Run's network sandbox.
|
|
||||||
# Pinning to eth0 bypasses that detection and lets k3s bind correctly.
|
|
||||||
k3s server \
|
|
||||||
--disable traefik \
|
|
||||||
--disable servicelb \
|
|
||||||
--disable metrics-server \
|
|
||||||
--write-kubeconfig-mode=644 \
|
|
||||||
--data-dir /var/lib/rancher/k3s \
|
|
||||||
--node-name soul-node \
|
|
||||||
--flannel-iface=eth0 &
|
|
||||||
|
|
||||||
K3S_PID=$!
|
|
||||||
|
|
||||||
# Start neuron-web immediately — do NOT block on k3s becoming ready.
|
|
||||||
# Cloud Run's startup probe requires port 8080 to be listening within the
|
|
||||||
# startup timeout. k3s may take 30-60s to initialise; blocking here causes
|
|
||||||
# probe failures and container termination before neuron-web ever starts.
|
|
||||||
# soul-demo becomes available asynchronously once k3s is ready. neuron-web
|
|
||||||
# handles soul-demo being temporarily unavailable gracefully.
|
|
||||||
echo "[entrypoint] Starting neuron-web on port ${PORT:-8080} (k3s initialising in background)..."
|
|
||||||
exec /usr/local/bin/neuron-web
|
exec /usr/local/bin/neuron-web
|
||||||
|
|||||||
@@ -16,4 +16,12 @@ build {
|
|||||||
c_source "dist/page_css.c"
|
c_source "dist/page_css.c"
|
||||||
c_source "dist/page_ga.c"
|
c_source "dist/page_ga.c"
|
||||||
c_source "dist/page_schema.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
@@ -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
|
/* 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
|
* 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
|
* Implementation: manual field scanner — does NOT run json_parse on the full
|
||||||
* JSON parser (which builds proper ElMap/ElList values), then pulls the
|
* envelope. Running the recursive-descent JSON parser on a 40–50 KB envelope
|
||||||
* three top-level fields by name. Avoids re-stringifying the headers map
|
* (common when the body contains minified/obfuscated JavaScript) fails because
|
||||||
* since json_stringify() does not support nested objects. */
|
* 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,
|
static int http_parse_envelope(const char* s, int* out_status,
|
||||||
el_val_t* out_headers_map, char** out_body,
|
el_val_t* out_headers_map, char** out_body,
|
||||||
el_val_t* out_parsed_root) {
|
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,
|
if (strncmp(s, EL_HTTP_RESPONSE_TAG,
|
||||||
sizeof(EL_HTTP_RESPONSE_TAG) - 1) != 0) return 0;
|
sizeof(EL_HTTP_RESPONSE_TAG) - 1) != 0) return 0;
|
||||||
|
|
||||||
el_val_t parsed = json_parse(EL_STR(s));
|
/* ── status ──────────────────────────────────────────────────────────── */
|
||||||
if (parsed == EL_NULL) return 0;
|
int status = 200;
|
||||||
|
{
|
||||||
int status = 200;
|
const char* sp = strstr(s, "\"status\":");
|
||||||
el_val_t hmap = 0;
|
if (sp) {
|
||||||
char* body = NULL;
|
const char* np = sp + 9;
|
||||||
|
while (*np == ' ' || *np == '\t') np++;
|
||||||
el_val_t sv = el_map_get(parsed, EL_STR("status"));
|
long sc = strtol(np, NULL, 10);
|
||||||
if (sv != 0) {
|
if (sc >= 100 && sc <= 599) status = (int)sc;
|
||||||
/* status comes back as an integer — el_val_t holds it directly. */
|
}
|
||||||
long sc = (long)sv;
|
|
||||||
if (sc >= 100 && sc <= 599) status = (int)sc;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
el_val_t hv = el_map_get(parsed, EL_STR("headers"));
|
/* ── headers ─────────────────────────────────────────────────────────── */
|
||||||
if (hv != 0) {
|
el_val_t hmap = 0;
|
||||||
ElMap* hm = (ElMap*)(uintptr_t)hv;
|
el_val_t parsed_hdrs = EL_NULL;
|
||||||
if (hm && hm->hdr.magic == EL_MAGIC_MAP) hmap = hv;
|
{
|
||||||
|
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"));
|
/* ── body ────────────────────────────────────────────────────────────── */
|
||||||
if (bv != 0) {
|
/* Search forward so we don't accidentally match "body": inside a header
|
||||||
const char* bs = EL_CSTR(bv);
|
* value. http_response() always appends the body field last. */
|
||||||
if (bs) body = el_strdup(bs);
|
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_status = status;
|
||||||
*out_headers_map = hmap;
|
*out_headers_map = hmap;
|
||||||
*out_body = body;
|
*out_body = body;
|
||||||
*out_parsed_root = parsed; /* caller releases to free hmap + entries */
|
*out_parsed_root = parsed_hdrs; /* caller el_release()s to free hmap */
|
||||||
return 1;
|
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);
|
const char* b = EL_CSTR(body);
|
||||||
if (!b) b = "";
|
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);
|
JsonBuf out; jb_init(&out);
|
||||||
jb_puts(&out, EL_HTTP_RESPONSE_TAG); /* {"el_http_response":1 */
|
jb_puts(&out, EL_HTTP_RESPONSE_TAG); /* {"el_http_response":1 */
|
||||||
jb_puts(&out, ",\"status\":");
|
jb_puts(&out, ",\"status\":");
|
||||||
|
|||||||
+18
-2
@@ -29,12 +29,19 @@ fn main() -> Void {
|
|||||||
el.style.color = isError ? '#c0392b' : '#2ecc71';
|
el.style.color = isError ? '#c0392b' : '#2ecc71';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var _formRevealed = false;
|
||||||
function revealPaymentForm(user) {
|
function revealPaymentForm(user) {
|
||||||
|
if (_formRevealed) return;
|
||||||
|
_formRevealed = true;
|
||||||
if (user && user.id) { window._neuronSupaId = user.id; }
|
if (user && user.id) { window._neuronSupaId = user.id; }
|
||||||
var auth = document.getElementById('auth-section');
|
var auth = document.getElementById('auth-section');
|
||||||
if (auth) auth.style.display = 'none';
|
if (auth) auth.style.display = 'none';
|
||||||
var isFree = (window.NEURON_CFG || {}).plan === 'free';
|
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');
|
var payment = document.getElementById('payment-section');
|
||||||
if (payment) payment.style.display = '';
|
if (payment) payment.style.display = '';
|
||||||
}
|
}
|
||||||
@@ -68,7 +75,16 @@ fn main() -> Void {
|
|||||||
function checkExistingSession() {
|
function checkExistingSession() {
|
||||||
initSupabase(function() {
|
initSupabase(function() {
|
||||||
supabaseClient.auth.getUser().then(function(res) {
|
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
@@ -907,6 +907,10 @@ fn handle_request_inner(method: String, path: String, body: String) -> String {
|
|||||||
// ── Compiled client-side JS: /js/* ───────────────────────────────────────
|
// ── Compiled client-side JS: /js/* ───────────────────────────────────────
|
||||||
// Served from dist/js/ (compiled by elc --target=js at build time).
|
// 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.
|
// 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/") {
|
if str_starts_with(path, "/js/") {
|
||||||
let rel: String = str_slice(path, 4, str_len(path))
|
let rel: String = str_slice(path, 4, str_len(path))
|
||||||
let abs: String = src_dir + "/js/" + rel
|
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, "") {
|
if str_eq(content, "") {
|
||||||
return "{\"__status__\":404,\"error\":\"not found\"}"
|
return "{\"__status__\":404,\"error\":\"not found\"}"
|
||||||
}
|
}
|
||||||
return content
|
return http_response(200, js_headers_json(), content)
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Brand assets: /brand/* ────────────────────────────────────────────────
|
// ── 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:\"}"
|
+ "\"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 {
|
fn handle_request(method: String, path: String, body: String) -> String {
|
||||||
let inner_resp: String = handle_request_inner(method, path, body)
|
let inner_resp: String = handle_request_inner(method, path, body)
|
||||||
// Detect envelope already set by inner handler (starts with
|
// Detect envelope already set by inner handler (starts with
|
||||||
|
|||||||
Reference in New Issue
Block a user