Last updated: 2026-05-03
Spark should be easy to test in an environment that is not the operator's laptop. This lane is for realtime sandbox agents: a disposable or persistent container that runs Spark Live, Telegram long polling, Builder, memory, character, and Spawner together.
This is separate from the locked-down Docker workbench in docs/OPTIONAL_DOCKER_WORKBENCH.md. The live lane has network on by design because Telegram and the selected LLM provider need it.
The live container starts:
- as
rootonly long enough to prepare the mounted state volume, then drops to the non-rootsparkuser; spark setup telegram-starterin/data/spark;spark update --skip-dirtyso persistent volumes move to the image's current registry pins on redeploy;spark live start;spawner-ui, bound to0.0.0.0:$SPARK_SPAWNER_PORT;spark-telegram-bot, using Telegram long polling;- Builder, memory, researcher, and character as installed modules/configured roots.
Secrets are read from environment variables only. They are not baked into the image.
Use Docker locally for smoke tests, then Railway for a hosted sandbox.
Railway is a good first target because it gives:
- a public HTTPS app URL for Spawner/Canvas/Kanban;
- app secrets as environment variables;
- optional persistent volumes for
/data/spark; - simple redeploys when Spark CLI is pinned to a new release.
Any VPS with Docker also works.
OpenClaw-style hosted deployments show why browser-first onboarding matters:
users understand a single /setup or control-panel surface much faster than
SSH plus many terminal commands. Railway templates also proved that a persistent
volume, setup wizard, model/channel picker, and one public HTTPS URL are a good
shape for nontechnical users.
The same pattern creates risk if it is not locked down. Spark Live should keep these standards:
- one public surface, protected by a user-chosen access key;
- API routes require
SPARK_BRIDGE_API_KEY, not just a browser cookie; - setup is browser-friendly, but repeat setup is not an unauthenticated open URL;
- public hosts must set
SPARK_ALLOWED_HOSTSto exact domains; - the runtime drops to a non-root user before starting Spark services;
- no Docker socket, host root, cloud credentials, SSH keys, browser profiles, or
real local
~/.sparkmounts; - headless hosts use API-key providers, not interactive OAuth CLIs;
- every hosted release must pass
spark live verify --quick; release-candidate pins must also passspark live verifybefore they are promoted publicly.
Hermes-style approval systems are also relevant for future full-access hosted agents: dangerous shell/file operations should ask in-chat or in-UI, with allowlists that users can inspect and remove. Spark Live is not ready for a public "full operating system access" VPS mode until those approvals and sandbox boundaries are first-class.
For Railway and other headless hosts, use API-first provider auth. Hosted Spark should be boring: bot, Spawner, Mission Control, previews, and private relays run in the container; model work goes to an API provider or to a model server the container can reach.
Safe hosted defaults:
zai,openai,openrouter,kimi,huggingface, andminimaxuse provider API keys.anthropicusesANTHROPIC_API_KEYin hosted containers. Claude Code OAuth stays a local desktop route.codexmay use the Codex CLI in hosted containers only with a dedicatedOPENAI_API_KEY. The live image writes that key intoCODEX_HOMEat startup.lmstudioandollamashould point at a reachable model server. Do not expect Railway to run large local models inside the Spark container.
Do not copy personal ~/.codex, Claude Code, browser profile, or desktop OAuth
state into Railway. Use revocable service keys for hosted sandboxes.
Choose one Telegram polling owner before setting secrets:
- Monolith Spark Live: this container owns Telegram long polling, so set
TELEGRAM_BOT_TOKENandTELEGRAM_ADMIN_IDShere. - Split Railway/VPS: a standalone
spark-telegram-botservice owns Telegram long polling, so do not setTELEGRAM_BOT_TOKENon Spark Live. Put the token only on the bot service asBOT_TOKEN, and setSPARK_LIVE_TELEGRAM_MODE=externalon Spark Live.
If both Spark Live and spark-telegram-bot receive the same bot token, Telegram
will terminate one poller with 409 Conflict: terminated by other getUpdates request.
Minimum for monolith Spark Live:
TELEGRAM_BOT_TOKEN=...
TELEGRAM_ADMIN_IDS=123456789
SPARK_LLM_PROVIDER=zai
ZAI_API_KEY=...
Minimum for split Railway/VPS Spark Live:
SPARK_LIVE_TELEGRAM_MODE=external
SPARK_LLM_PROVIDER=zai
ZAI_API_KEY=...
Minimum for the separate split spark-telegram-bot service:
BOT_TOKEN=...
ADMIN_TELEGRAM_IDS=123456789
TELEGRAM_GATEWAY_MODE=polling
TELEGRAM_RELAY_SECRET=<same-random-relay-secret-as-Spark-Live>
TELEGRAM_RELAY_URL=http://spark-telegram-bot.railway.internal:8788/spawner-events
Supported SPARK_LLM_PROVIDER values for headless hosts:
| Provider | Required env | Notes |
|---|---|---|
zai |
ZAI_API_KEY |
Good default for API-key VPS sandboxes. |
codex |
OPENAI_API_KEY |
Codex CLI in the container, authenticated from a dedicated OpenAI key. |
openai |
OPENAI_API_KEY |
Use this for OpenAI API keys. |
openrouter |
OPENROUTER_API_KEY |
Broad model gateway. |
kimi |
KIMI_API_KEY |
Moonshot/Kimi OpenAI-compatible route. |
huggingface |
HF_TOKEN |
Hugging Face router. |
minimax |
MINIMAX_API_KEY |
MiniMax OpenAI-compatible route. |
anthropic |
ANTHROPIC_API_KEY |
API-key mode only in containers. |
lmstudio |
LMSTUDIO_BASE_URL, optional LMSTUDIO_MODEL |
Only if the container can reach your LM Studio server. |
ollama |
OLLAMA_URL, optional OLLAMA_MODEL |
Only if the container can reach your Ollama server. |
Codex OAuth is an interactive local CLI sign-in path. It belongs on a user's
machine, not in a hosted container. For hosted Codex, use SPARK_LLM_PROVIDER=codex
with OPENAI_API_KEY, or use SPARK_LLM_PROVIDER=openai if you want the direct
OpenAI API route instead of the Codex CLI route.
Optional:
SPARK_MODEL=provider-specific-model-id
SPARK_SPAWNER_PORT=5173
SPARK_SPAWNER_HOST=0.0.0.0
On Railway, map SPARK_SPAWNER_PORT to $PORT if you expose the web process.
From the spark-cli repo:
docker build -f docker/live/Dockerfile -t spark-live:local .Run with a disposable Spark home:
docker run --rm -it \
-p 5173:5173 \
-e TELEGRAM_BOT_TOKEN="$TELEGRAM_BOT_TOKEN" \
-e TELEGRAM_ADMIN_IDS="$TELEGRAM_ADMIN_IDS" \
-e SPARK_LLM_PROVIDER=zai \
-e ZAI_API_KEY="$ZAI_API_KEY" \
spark-live:localFor persistence:
docker volume create spark-live-data
docker run --rm -it \
-p 5173:5173 \
-v spark-live-data:/data/spark \
-e TELEGRAM_BOT_TOKEN="$TELEGRAM_BOT_TOKEN" \
-e TELEGRAM_ADMIN_IDS="$TELEGRAM_ADMIN_IDS" \
-e SPARK_LLM_PROVIDER=zai \
-e ZAI_API_KEY="$ZAI_API_KEY" \
spark-live:localThen check:
docker exec -it <container> spark live status
docker exec -it <container> spark live verify --quickRecommended Railway settings:
Build: Dockerfile
Dockerfile path: docker/live/Dockerfile
Start command: leave empty; the image entrypoint starts Spark Live
Volume mount: /data/spark
Set Spark Live secrets in Railway Variables, never in source control:
RAILWAY_DOCKERFILE_PATH=docker/live/Dockerfile
RAILWAY_RUN_UID=0
SPARK_ALLOWED_HOSTS=<your-railway-domain>.up.railway.app
SPARK_UI_API_KEY=<different-random-secret-at-least-24-chars>
SPARK_BRIDGE_API_KEY=<different-random-secret-at-least-24-chars>
SPARK_LLM_PROVIDER
ZAI_API_KEY / OPENAI_API_KEY / etc.
CODEX_HOME=/data/spark/codex
SPARK_SPAWNER_PORT=${PORT}
Only add TELEGRAM_BOT_TOKEN and TELEGRAM_ADMIN_IDS to Spark Live for a
monolith deployment where Spark Live owns Telegram polling.
For split Railway deploys, omit TELEGRAM_BOT_TOKEN and TELEGRAM_ADMIN_IDS
from this Spark Live service. The separate spark-telegram-bot service should
hold BOT_TOKEN, ADMIN_TELEGRAM_IDS, TELEGRAM_GATEWAY_MODE=polling, relay
settings, and the shared bridge secret. Set SPARK_LIVE_TELEGRAM_MODE=external
on Spark Live so the Docker entrypoint configures the runtime without requiring
or starting a local Telegram poller.
RAILWAY_RUN_UID=0 lets the entrypoint repair Railway volume ownership. Spark then
drops to the non-root spark user before setup and runtime work starts.
SPARK_ALLOWED_HOSTS lets Spawner's Vite server accept the generated Railway
domain without disabling host-header protection for every possible host. Use
hostnames only: no https://, no paths, no wildcards, no loopback values.
SPARK_UI_API_KEY protects the hosted Spawner UI. Open the Railway URL and Spark
will redirect browser users to /spark-live/login; paste the UI key there to set
an httpOnly browser cookie. The ?uiKey=<SPARK_UI_API_KEY> route is a legacy
automation/bootstrap escape hatch only. Do not share it with humans or paste it
into chat; query-string secrets can leak through browser history and request
logs.
SPARK_BRIDGE_API_KEY protects mission-start/control APIs.
For VPS hosts where you control Docker runtime flags, start from
docker/live/docker-compose.vps.yml. It uses the Docker hardening controls that
the hosted audit checks for:
security_opt: no-new-privileges:truecap_drop: [ALL]read_only: true- tmpfs
/tmp - one writable Spark state mount at
/data/spark
Before the first start, create the state directory and give it to the spark
image UID. This avoids needing root capabilities inside the running container:
cd docker/live
mkdir -p spark-data
sudo chown -R 1001:1001 spark-data
cp spark-live.env.example spark-live.envThen put the same Railway-style variables in spark-live.env and start:
docker compose -f docker-compose.vps.yml up -d --build
docker compose -f docker-compose.vps.yml exec spark-live spark security audit --hostedIf you need the entrypoint to repair an existing root-owned volume, do that as a
one-time maintenance step without cap_drop: [ALL], then restore the hardened
Compose file before running the public service.
After deploy:
-
Open the Railway logs and confirm
Spark Live is running. -
Run
spark live statusinside the container or overrailway ssh. -
Run
spark live verify --quickfor the fast hosted security gate. -
Run
spark live verifyfor a release-candidate gate. It may start a protected mission smoke and spend real LLM credits. -
Open the Railway URL and sign in at
/spark-live/login. -
Open
/spark-live/setupand confirm the checklist shows browser auth, bridge auth, allowed hosts, Telegram, agent LLM, mission LLM, and full-access safety as ready. -
Send
/diagnoseto the sandbox Telegram bot. -
Send
/remember Railway sandbox works, then/recall Railway sandbox. -
Send this constrained static build smoke:
/run Build a tiny static HTML page called Spark Production Smoke. It should have one file, index.html, with a dark Mission Control panel, a green "Spark Live OK" status, and the text "Telegram to Spawner relay worked on May 8, 2026". Do not add package files, do not install dependencies, and keep it simple enough to finish fast.
Pass criteria:
/diagnoseshows the bot relay and Spawner UI as reachable.- If plain chat times out but Spawner mission ping passes, treat that as a chat provider timeout to tune, not a failed build path.
- The mission plan has the constrained static steps:
Create Exact Static Index HTMLandVerify One-File Static Contract. - The mission completes with
2completed tasks and0failed tasks. - The generated workspace contains
index.htmlwithSpark Live OKandTelegram to Spawner relay worked on May 8, 2026. - The generated workspace does not need
package.json,node_modules, or dependency installation.
For a repeatable operator-side check across the split Railway services, run:
.\scripts\railway-production-smoke.ps1 `
-SparkLiveCwd C:\path\to\spark-cli-prod-worktree `
-TelegramBotCwd C:\path\to\spark-telegram-bot `
-PublicUrl https://spark-live-production.up.railway.appThe script checks public health, spark live status, the installed Spawner
registry pin, Telegram bot runtime health, and the bot service's authenticated
health/providers/mission-board calls into Spawner. It does not start a new LLM
mission, so use the Telegram /run smoke above for the full paid-provider path.
The hosted deep smoke is intentionally not part of the fast default check. It uses the configured mission provider and may spend real LLM credits.
Do not point agent.sparkswarm.ai or a public release manifest at a new Spark
Live CLI/image until this gate passes:
python -m pytest tests/test_cli.py -q
python -m spark_cli.cli verify --registry-pins
python -m spark_cli.cli verify --provenance
python -m spark_cli.cli verify --sandboxes
railway up --detach --service spark-live
railway ssh --service spark-live spark live status
railway ssh --service spark-live spark live verify --quickThen check Railway logs for:
- the entrypoint dropping from
rootto the non-rootsparkuser; - setup completing without raw secret values in output;
spark update --skip-dirtyresolving every pinned module;spark live startstarting bothspawner-uiandspark-telegram-bot;- no
upload-pack: not our referrors from a mistyped or unpushed registry pin.
If Docker Desktop is available locally, also run the local Docker smoke first. If it is not running, record that as an environment limitation rather than calling the release gate complete.
- Use a fresh Telegram bot for every hosted sandbox.
- Keep sandbox API keys scoped and revocable.
- Prefer separate hosted sandbox provider keys from the operator's main personal keys.
- Require
SPARK_UI_API_KEYandSPARK_BRIDGE_API_KEYbefore exposing Spawner on a public host. - Make
SPARK_UI_API_KEYandSPARK_BRIDGE_API_KEYdifferent random values, at least 24 characters each. Do not use placeholders such aschangeme,password,secret, orspark. - Keep hosted secret files private.
spark live verify --quickfails if a POSIXsecrets.local.jsonis group/world-readable. - Do not mount or copy a real local
~/.sparkinto hosted containers. - Never mount
/var/run/docker.sock,/,/root, cloud credential directories, SSH keys, or browser profiles. - Prefer Docker hardening flags on VPS:
--cap-drop=ALL,--security-opt no-new-privileges, resource limits, and only one writable Spark state volume. - Use a persistent volume only when you intentionally want memory/state to survive redeploys.
- Rotate tokens after demos if screenshots/logs might have exposed them.
- Do not expose shell/terminal access in the browser until it is behind a separate admin auth layer and a dangerous-command approval system.
- Keep public health probes shallow. Deep health checks that reveal providers, mission ids, logs, or memory state must require an admin key.
For Spark to feel easier than current agent VPS setups, the target flow should be:
- Click "Deploy Spark Live" from the site.
- Choose a host template such as Railway for the easiest first path.
- Enter one setup password/UI key, Telegram bot token, Telegram admin id, and one LLM provider.
- Spark generates bridge secrets, stores them as platform variables, and starts.
- Browser opens
/spark-live/login, then Mission Control shows a guided checklist. - Telegram
/diagnoseandspark live verifyboth pass.
For VPS users who want full control, offer a Docker Compose path with the same checks, but do not make them discover firewall, host header, volume ownership, and secret rules by accident.
- OpenClaw Docker docs: non-root image, persistence paths, health checks, gateway bind modes, sandbox behavior, and Docker socket caveats.
- Railway OpenClaw templates: browser setup, model/channel picker, admin
dashboard, persistent
/data, and single-port deployment pattern. - Hermes Agent security docs: dangerous-command approval, gateway allowlists, pairing, and Docker terminal backend resource/security settings.
This lane proves Spark can:
- install the whole ecosystem on a clean Linux host;
- run Telegram long polling without webhooks;
- keep Spawner, Telegram, memory, and LLM routing alive under one foreground process;
- support VPS/Railway users who do not want to keep a laptop terminal open.
It does not replace the local installer. Local users should still use the hosted installer from agent.sparkswarm.ai.