spark 1.37 . by forgewright-ai

Installing spark

Start at section 1 without a machine, at section 2 with one. The rest is the runbook, in the order you will need it.

1. A machine from zero

About ten minutes, most of it one 4.7 GB download; sudo once.

Debian:

  1. Get Debian 13 (trixie), the small installer image, from https://www.debian.org/CD/netinst/ (amd64 for a PC, arm64 for an ARM board). Write it to a USB stick and boot it.
  2. In the installer, leave the root password empty: that puts your user in the sudo group. At *Software selection* keep SSH server and standard system utilities; a desktop is optional.
  3. Log in and install what spark needs:
    sudo apt-get update && sudo apt-get install -y git curl python3
  4. Continue at section 2.

Arch:

  1. Get the ISO from https://archlinux.org/download/, write it to a USB stick and boot it.
  2. Run archinstall. The answers that matter: the *Minimal* profile; a user account marked superuser (that is the sudo); *systemd-boot* as the bootloader (GRUB works too); git curl python openssh under additional packages; "copy the ISO's" network configuration to keep the Wi-Fi you joined with iwctl; one HTTPS mirror as a custom server (https://geo.mirror.pkgbuild.com/$repo/os/$arch), because a router that inspects HTTP turns a mirror into invalid or corrupted database (PGP signature). A box that will not join the Wi-Fi: docs/TROUBLESHOOTING.md (one Wi-Fi daemon per card, then the logs).
  3. Reboot, log in, sudo pacman -Syu once, then section 2.

macOS: any Mac Apple still updates. xcode-select --install brings git, curl and Apple's python3 (3.9 is enough). Continue at section 2.

Windows: spark runs in WSL 2, where Ubuntu is Linux to it. In PowerShell, wsl --install -d Ubuntu-24.04, reboot when asked, open Ubuntu, then Debian's step 3 and section 2.

2. Install spark

  1. Check the four things spark needs: sudo once for the package manager, git, curl, python3 3.9 or newer.
    for c in sudo git curl python3; do
        command -v "$c" >/dev/null || echo "missing: $c"
    done
    python3 -c 'import sys; sys.exit(sys.version_info < (3, 9))' \
        || echo "python3 is older than 3.9"
    Silence means you are ready. Otherwise:
    • Debian 13 / Ubuntu 24.04 or newer: `sudo apt-get install -y git
    curl python3. No sudo at all: su -c 'apt-get install -y sudo && usermod -aG sudo YOURNAME'`, then log out and in.
    • Arch Linux, or what says ID_LIKE=arch: `sudo pacman -S --needed
    git curl python. Never pacman -Sy alone: sudo pacman -Syu` first when a package cannot be found.
    • macOS: xcode-select --install.
    • Your login shell must be bash 4+ or zsh: the prompt widget lives in
    one of them (macOS ships zsh; its bash is 3.2).
  2. One line:
    curl -fsSL https://github.com/forgewright-ai/spark/releases/latest/download/get | sh
    It clones spark to ~/.spark, lands on the newest release and runs spark setup. To read it first: curl -fsSLO https://github.com/forgewright-ai/spark/releases/latest/download/get; sh get. With wget: wget -qO- URL | sh. By hand, the same two steps:
    git clone https://github.com/forgewright-ai/spark.git ~/.spark
    ~/.spark/bin/spark setup
  3. When it finishes:
    exec $SHELL      # the prompt widget goes live
    spark check      # every row green

What setup does. It asks three things and never more: this machine's name (the short hostname by default), yours (your login), and the model (the row this machine earns is marked *; section 4 says how). Then:

  1. writes ~/.config/spark/site.env (0600);
  2. on Linux, sudo -v once when a package is missing (libgomp1 on Debian, plus the Mesa Vulkan packages with a GPU; Arch has the library in base, so a bare Arch asks only with a GPU); nothing on macOS;
  3. runs bootstrap.sh: the engine (one pinned llama.cpp tarball for this OS, sha256-verified), the model with curl's progress bar, the token, the prompt widget, one rc line, the units (the server, the page, a 5-minute check timer);
  4. brings the server up and waits for it;
  5. asks ? how big is this dir for you and prints the tok/s it measured;
  6. prints the three things to try.

It paints nothing: the machine looks as it did. It is re-runnable. Flags: --yes takes every default (implied when stdin is not a terminal); --model NAME|auto|none, --name, --user, --theme, --no-serve pre-answer, as do SITE_NAME, SITE_USER, SITE_AI_MODEL and SITE_THEME in the environment. get itself checks the ground first (the command line tools on macOS, apt-get or pacman, git, python3 >= 3.9) and refuses with the install line when one is missing; it never runs sudo. SPARK_HOME moves the clone, SPARK_URL points it elsewhere, SPARK_REF=main follows development; sh get --clone-only stops after the clone.

The rc line. bootstrap appends exactly one line to your login shell's rc file (~/.bashrc for bash, ~/.zshrc for zsh), only when it is not there yet:

[ -r ~/.config/spark/hook.bash ] && . ~/.config/spark/hook.bash   # spark: the AI at the prompt
[[ -r ~/.config/spark/hook.zsh ]] && source ~/.config/spark/hook.zsh   # spark: the AI at the prompt

The hook puts ~/.local/bin first on PATH, sources the widget, keeps a blank row above the prompt for the hint, and sources TAB completion (the verbs, then each verb's words, offline). It goes last, after fzf, because the widget wraps Enter. If the rc row says todo, the login shell cannot host the widget (another shell, or macOS's bash 3.2): chsh -s /bin/zsh, then spark setup again. A bare zsh needs your own autoload -Uz compinit && compinit in ~/.zshrc for completion.

3. Use it

  1. At the prompt: ? words or words?, Enter, and the command lands in your line with a hint above it; Enter again runs it. A command that deletes comes back marked ! -- a recursive rm also says how many files and bytes it clears. ?? words follows up on the last answer; Esc s asks about the line you are on; cmd 2>&1 | explain says what went wrong. spark off gives Enter back and quiets the failure line; spark on restores both. TAB completes the verbs and their names, offline.
  2. When a command fails, one line appears above the next prompt: * failed (1) -- press Esc s to ask why. Esc s on the empty line puts the command back, already piped to explain, and nothing runs until you press Enter. A second Esc s after that proposes the corrected command in your line. When a command is not found (exit 127), Esc s offers the line that installs it -- a tool spark itself installs is named with no model call, the rest through the model. A command that deletes or destroys (rm, dd, mkfs...) is never offered a re-run -- the line says so, and ? words still answers about it. After the fix works, Esc s offers to keep what happened as a spark memory add fact -- edit the line, then Enter. Only a command typed on one line is offered; Ctrl-C and a no-match from grep or diff stay quiet. The offer lives in the one pane it happened in and is gone with it.
  3. Esc r is intent search: type what a command did in your own words, press Esc r, and the line that ran lands in your prompt -- Esc r again cycles through the matches. Your shell hands its own history to spark; nothing reads a history file, and every candidate is one that is actually in it. Ctrl-R is left to the shell (and to fzf). On bash, Esc r (M-r) replaces the default revert-line.
  4. spark chat is a conversation at a chat> prompt. /help lists its verbs: /new a fresh thread, /resume [N] an older one, /clear the screen, /last the last turn with its tok/s, /model which one answers, /q (or Ctrl-D) ends. Ctrl-C cancels a reply and keeps the chat. spark chat --thread N [words] continues an older thread from the spark history list (1 = newest).
  5. spark <words> streams one answer; spark @FILE words sends a text file's first 4 kB and last 12 kB with the question. Quote words the shell would glob (a trailing ?, parentheses).
  6. spark do <words> proposes one command at a time: Enter runs it, e edits it first, s skips, q quits; a step that can destroy data (a sudo step counts) runs only when you type yes. After a step, its proof -- one read-only check that it worked -- is offered the same way, and only its exit code goes back to the model, never what it printed. Each step's output (last 4 kB) goes back to the model until it says done, or after 8 steps; every step is recorded as it ran.
  7. spark ask reads a plan, a draft or a decision on stdin and answers with questions about it -- at most three, one per line, and nothing else. Every line of the output is a question: a line that is not one, a question quoting words the text does not contain, and a question that could be asked of any plan are dropped before you see them, so the reply cannot state a fact the text does not hold. When nothing survives, you get one line saying so and nothing more -- a reader with nothing to ask says nothing.
    spark ask < plan.md
    spark ask what am I deciding here < plan.md
    Answer one of them and it stops coming back: spark ask --answered --name plan.md with the question on stdin; spark ask --ledger --name plan.md lists what you have answered, --ledger clear drops it. spark ask -h says the rest.
  8. spark read answers a question from a source on stdin -- a page, a message, a document -- and from nowhere else. Every line of the answer quotes the source and the quote is checked: a line whose quotes are not in the source, or that quotes nothing, is dropped before you see it. When the source does not answer, the reply is one line showing the source's own opening words, never a guess. A source past 16000 characters is read one part at a time (--part N; the answer's first line names the part).
    w3m -dump https://example.com | spark read "what is this page for"
    spark read when does it open < page.txt
    --name page.txt records the question asked (never sent anywhere); spark read --ledger --name page.txt lists what has been asked, --ledger clear drops it. spark read -h says the rest.
  9. spark drill turns a source on stdin into practice: it asks a question, you try it, then it shows the source's own words so you can say whether you had it. Both the question and the answer are spans of the source -- an answer the model invents is dropped before it is ever asked -- and a source too thin to drill is one line, never padded from the model's own knowledge.
    spark drill < notes.md
    w3m -dump https://example.com | spark drill --name page
    --name NAME keeps a schedule: a missed item comes back on a widening interval (1, 3, 7, 21, 60 days) until you have had it right twice in a row; spark drill --ledger --name NAME shows the schedule, --ledger clear drops it. Without --name, a session is practice kept nowhere. spark drill -h says the rest.
  10. spark watch reads a live stream on stdin -- a log tail, a build, a long migration -- and says nothing until a line matches what you asked for, then one line quoting it. The quote is checked against the stream, so it cannot report what is not there, and silence is the normal, healthy state. The stream never leaves this machine.
    tail -f app.log | spark watch "a 500 appears"
    journalctl -f  | spark watch "anything about the disk"
    spark watch -h says the rest.
  11. spark soul edit writes the paragraph that tells the model who it is (~/.config/spark/soul, at most 4000 characters; spark soul shows which is in use, spark soul reset goes back to the default). The default:
    You are spark, the AI on this machine. You run here, on hardware the user
    owns; nothing you are told leaves it. You are here to answer, to explain,
    to write, and to hand the user a command when one is what they need.
    Speak plainly, in the user's language. Say when you do not know. Never
    invent a flag, a path, or a command.
  12. spark memory add <words> adds a fact it keeps (spark memory forget N drops one, spark memory lists them, spark memory off stops sending them; 40 facts of 200 characters). Soul and facts ride on every conversation, so a fact costs tokens every time: keep the ones that change answers. The model never writes them. spark history clear empties the turns and threads and never touches a fact.

4. Models

Two files:

filewhat
models.envevery model spark can serve: 26 models, each with its license; line marks a row proven on the prompt line
~/.config/spark/models.envyour own rows (spark model add URL --license), 0600; marked u

spark model list shows every row: file size, the RAM it needs against this machine's budget (SITE_AI_BUDGET, default 60 percent of RAM plus GPU memory), the license, the proof column (line when tested on the line; a kept/run score once the grounding audition measured how faithfully it quotes a source -- those rows win an auto tie, and the earlier row of the list wins among equals), downloaded or serving, and its speed here (~N tok/s is an estimate until spark bench or a real turn measures it; too big when it does not fit). The tested rows:

namefileRAM
qwen3-1-7b1.0 GB3 GB
qwen3-4b2.3 GB5 GB
qwen3-8b4.7 GB7 GB
qwen3-14b8.4 GB11 GB
qwen3-30b-a3b17.4 GB21 GB
granite-4-2-8b5.0 GB7 GB

The untested rows (Qwen3 4B-Thinking and Coder-30B-A3B, Qwen2.5 7B / 14B / Coder-7B, Mistral 7B and Nemo 12B, Phi-4 mini and 14B, DeepSeek-R1 distills 7B / 14B, SmolLM2 1.7B, gpt-oss-20b, all Apache-2.0 or MIT; Llama 3.2 1B / 3B, Llama 3.1 8B and Gemma 3 1B / 4B / 12B / 27B under their own terms) are yours by name. A row under a license that is not Apache-2.0 or MIT prints its license and asks download it? yes/NO: first. The page lists them all: https://spark.forgewright.ai/models/

How auto picks: every tested open-license row whose RAM fits the budget, then the largest of those whose file is under this build's speed cap (3 GB on cpu, 6 GB on vulkan, 20 GB on metal: the sizes that keep about 8 tok/s). When the cap held a bigger row back the table's header says so, and spark model NAME takes that row anyway. Nothing under the cap fits: the smallest row that fits.

  1. spark model NAME chooses a model: downloads and verifies it (size and sha256 from its row) and restarts the server. spark model auto goes back to the rule above; spark model rm NAME deletes a file not in use.
  2. spark model budget N (10-95) sets the percent and prints the table.
  3. A .gguf of your own in ~/.local/share/spark/models is served with SPARK_MODEL=<file> in spark.env.
  4. spark ember NAME adds a second, bigger model for conversations: the prompt line stays with the small one (context 4096, reasoning off, so a thinking model answers fast) and every conversation (spark <words>, chat, do, the page, any /v1 client naming no model) goes to the second. One server, one port, one token; the request's model field picks. spark ember auto pairs the smallest tested row with the largest that fits beside it; spark ember none (the default) runs one model in both roles.
  5. spark model add URL adds your own row: a huggingface.co .../resolve/<rev>/<file> URL is verified from its redirect headers, any other URL needs --sha256 HEX; --license "NAME URL" is required. The row lands in ~/.config/spark/models.env, then it is downloaded and served like any row.
  6. spark model verify re-hashes every downloaded file, prints ok or bad -- spark model rm NAME; spark model NAME per file, and exits 1 on a mismatch; nothing is deleted for you. spark check's models row is the daily, cached version of the same check.

Speed: spark bench measures with llama-bench (pp512 / tg128) and keeps the result as the file's baseline; the throughput check row warns when real turns fall below 70 percent of it. spark bench tune tries GPU layers, flash attention, KV cache types and thread counts; spark bench tune apply writes the winner to spark.env and restarts. spark stats [--week] sums up what real turns measured. The server keeps no prompt cache in RAM (--cache-ram 0: llama-server would otherwise keep up to 8 GiB of replaced prompts in host memory); SPARK_EXTRA_ARGS=--cache-ram N in spark.env sets a budget in MiB.

5. Other machines and your phone

spark serves the same AI, with its soul, memory and threads, on one LAN address (http://<host>:8081). The admin token stays on this machine; everyone else is a named user with a token of their own.

Another machine of yours:

  1. Here: spark user add NAME mints an account; its token is shown once and never stored.
  2. There, with spark installed: spark client URL (the URL from spark forge --print-client here), then spark user login NAME with that token.
  3. spark brain there says which server answers.

A client runs nothing of its own: no engine, no model, no units, and no account of its own -- the login is the token minted here, and until spark user login NAME there, answers are given but no thread is kept. spark check there reads na on those rows and the peer row says whether this machine answers and accepts that login. spark model there prints this machine's table; choosing a model there is refused. spark client off gives it a model of its own again.

Another OS user on this same machine (a shared workstation): the owner runs spark share on once (a spark group, one engine for everyone -- Linux). Then add the user to the group (sudo gpasswd -a NAME spark; they log in again), and they set up in userspace -- no sudo, no download:

curl -fsSL https://github.com/forgewright-ai/spark/releases/latest/download/get | sh   # clones spark's code only
spark setup                                          # detects the shared engine, offers to join

spark setup sees the shared engine and joins it instead of the first-run: no model to download, no root, their own soul and memory in their own home, answering from the one engine. spark share off ends it.

Every non-human caller -- a script, an app, a CI job -- gets its own user with spark user add NAME and its own token; the admin token is this machine's and is never shared. Any program, with the OpenAI shape (a request naming no model gets the conversation model with the identity; model: spark the bare prompt model):

curl -sN http://<host>:8081/v1/chat/completions \
  -H "Authorization: Bearer $YOUR_SPARK_USER_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"messages":[{"role":"user","content":"what is this machine for?"}],"stream":true}'

The page, in any browser on the LAN:

  1. spark forge --print-url prints http://<host>:8081/login and, at a terminal, the admin token and a QR code. Scan the QR with a phone's camera and the page signs in by itself -- the token rides the link after #, which stays in the browser and never reaches the server. The QR is the token drawn as squares: show it (or a screenshot of it) only to the person it is for.
  2. Or type a token once; the browser keeps a cookie (90 days; logging out or a server restart asks again, the admin's too). Everyone lands in the chat. A user's token is a chat app: their own threads and memory, plus their account behind the one menu button. The admin token also opens the whole machine (activity, do, the settings, the log). Give each of the household their own spark user add NAME -- it prints their token and their own QR to scan, once.
  3. On a phone, add it to the home screen (iOS: share > add to home screen, and it becomes an app; Android keeps a shortcut that opens in a browser tab). The page needs this machine reachable when it opens: there is no offline copy.

spark forge alone is the status (URL, health, model, unit, users); spark forge on|off enables or disables it. spark forge token --new rotates the admin token; spark user token --new rotates a user's; that principal logs in again. spark forge audit lists the newest admin actions -- a command run from the page (its sha256 prefix and exit code, never its text), a verb run, a user added, removed or rotated -- sealed in this machine's own store.

Sealed stores: each user's threads, memory and chat history are encrypted (ChaCha20-Poly1305, written from RFC 8439) under a key wrapped by that user's token. The machine keeps a sha256 verifier and the wrap, never the token: nobody, the admin included, holds a key to another user's messages, and a lost token is lost history. There is no TLS on the LAN: the trust model is your LAN.

Headless. On the machine that stays on, spark headless on:

Then point every laptop (spark client URL) and every phone at it: one address, one identity, the same answers everywhere. The headless check row names any piece that is missing.

6. Per-OS notes

macOS:

Linux:

Arch:

Windows (Ubuntu 24.04 on WSL 2):

7. Keep it

Update:

spark update

A clone get made moves to the newest release tag; a developer clone on a branch pulls --ff-only. Either way it converges (bootstrap.sh runs, spark check re-reads); --dry-run says what it would do. By hand: git -C ~/.spark pull --ff-only && ~/.spark/bootstrap.sh.

A release tag is signed, and spark update moves only to a tag signed by a key in the tree's allowed-signers; any other tag is refused in one line -- spark update -- v1.36 is not signed by a known key: refused -- and nothing moves (get keeps the same rule, and the signed row of spark check names the key that signed the tag you are on).

Uninstall:

spark uninstall
  1. It prints the plan, one row per thing, then asks for the word yes.
  2. Everything spark made goes: the units, the rc line, the console palette and font (VGA again), the shell layer's files back from their .bak (docs/SHELL.md), ~/.local/bin/spark, the engine and every model, ~/.config/spark, ~/.local/state/spark, and the clone at ~/.spark when it is the one get made and clean. Headless and the quiet login and boot are undone first (sudo).
  3. What stays, on purpose: your soul, your memory, the sealed users' stores with their keys, your models.env, your themes and privacy-terms; --purge takes those too. The shell layer's packages are a question (--packages / --keep-packages answer up front). --dry-run shows the plan; --yes skips the question for a script.
  4. Named at the end with the line that puts it back: a hostname it set, macOS's pmset values, a console font set before v1.12. A root step whose sudo refuses becomes a todo row, never a failure.

The keys. Everything in ~/.config/spark/site.env beyond the three setup asks is optional and has a verb; editing the file and running ./bootstrap.sh does the same.

keyvaluesdefault
SITE_NAMEthis machine's display nameshort hostname
SITE_USERyour display nameyour login
SITE_SET_HOSTNAMEyes: the OS hostname follows SITE_NAME (sudo)no
SITE_AI_MODELauto, none, or a name -- spark model NAME; none beside a peer URL is a clientauto
SITE_EMBER_MODELnone, auto, or a name: the second model for conversations -- spark ember NAMEnone
SITE_AI_BUDGET10..95: percent of RAM+GPU memory auto may use -- spark model budget N60
SITE_AI_BUILDauto, cpu or vulkan: the Linux engine build (macOS ignores it; WSL 2 lands on cpu)auto
SITE_PEER_AI_URLanother machine's URL (spark forge --print-client there) -- spark client URLunset
SITE_HEADLESSyes: up from boot, never asleep -- `spark headless on\off`no
SITE_THEMEnone, or a palette from themes/ or ~/.config/spark/themes/ -- spark theme NAME; painted only when you asknone
SITE_FONT_FACE / SITE_FONT_SIZELinux console: a face and size from spark font list (Terminus 16x32); macOS: Terminal.app's font and points -- spark font FACE SIZE. Refused on WSL 2 and Arch (no console-setup)unset / 16x32 (Linux), the Nerd Font / 13 (macOS)
SITE_QUIET_LOGINLinux: yes bares the login (motd, /etc/issue; originals kept) -- spark quiet login onno
SITE_QUIET_BOOTLinux: yes makes the boot silent (one GRUB drop-in) -- spark quiet boot on; refused on WSL 2 and Archno
SITE_QUIET_STARTyes: no banner, one-line serve, forge and bare spark -- spark quiet start onno
SITE_QUIET_AUDIOyes: no sound from spark -- spark quiet audio onno

Runtime knobs live in ~/.config/spark/spark.env (spark.env.example lists them all); the ones with a verb:

keyvaluesdefault
SPARK_MEMORYon/off: send the remembered facts -- `spark memory on\off`on
SPARK_FORGEauto/on/off: serve the page and the API -- `spark forge on\off`auto
SPARK_FORGE_HOST / SPARK_FORGE_PORTthe address and port (never 0.0.0.0)the LAN address / 8081
SPARK_HISTORYdays of turns and threads kept; off keeps none30
SPARK_NGL SPARK_FLASH_ATTN SPARK_KV SPARK_THREADSthe engine's tuning -- spark bench tune applyauto
SPARK_API_KEY_FILEa token file you already have~/.local/state/spark/api-token

What needs root. bootstrap.sh --dry-run lists exactly which of these it would do and never calls sudo:

spark uninstall uses sudo for the mirror image. Passwordless sudo is yours to decide (echo 'you ALL=(ALL) NOPASSWD:ALL' | sudo tee /etc/sudoers.d/you, fine for a test bench).

When something stops working:

  1. spark check names the row and the remedy (long output pages through $PAGER, plain when piped).
  2. ./bootstrap.sh --dry-run: what a rebuild would change.
  3. spark: which server answers, which shells have the widget.
  4. A stale server after a DHCP move shows as moved on the serve row: spark serve off; spark serve on. The forge row likewise: spark forge off; spark forge on.
  5. spark forge: is the page up, at which address; one line per request in ~/.local/state/spark/forge.log, never a body.
  6. The ember row: the pair over budget, the file not downloaded (./bootstrap.sh), or not warm (spark serve warms it).
  7. A GPU new servers cannot see (the gpu row warns): on Linux the serving user must be in the render group; log out of every session and in again.
  8. SPARK_DEBUG=1 spark ... and ~/.local/state/spark/debug.log.
  9. the ledger does not open -- spark user login again (or the memory): the key this machine holds is not the one that sealed the file -- a login by another token, or a byte that changed on disk. Nothing is written over it; spark user login NAME with your token puts the right key back.
  10. For an issue: spark check --report prints a block safe to paste -- version, OS, backend, model stems and every row's status, never a value, a path or a name (it runs the privacy word lists over its own output besides).

8. What an attacker can and cannot do

The trust boundary is your LAN: spark serves plain HTTP to the addresses you gave it and nothing else.

What leaves is counted, never read: every request's size and destination ride its turn record (a number and a host), spark stats --sends prints them by destination and day for the last week, and the sends row of spark check warns the day any bytes went to a host other than the server you chose.

Appendix: how it fits together

your shell                    this machine                        the LAN
----------                    ------------                        -------
? words ---- widget -------> spark line ---+
an app's key - spark-<app> -> spark edit --+
spark chat | do | explain -> spark <verb> -+-> spark's server :8081 --> another
                                           |   soul, memory, threads;    machine's
                                           |   /v1 and /api; the page    spark, a
                                           |                             browser,
                                           +-> llama-server :8080 <---- a program
                                               one model, or two:
                                               the pinned engine and a
                                               GGUF from models.env

get -> spark setup -> bootstrap.sh (apply) -> install.sh (links, renders)
                      the engine, the model, the token, the units, one rc
                      line; spark-shell adds spark's own shell; a spark app
                      is its own repository (spark-<app>)

spark check   39 rows: every promise the machine makes, fixture-tested
spark update  the newest signed tag, or main on a developer clone; converge

what leaves the machine: pinned downloads in, your questions to the
server you chose, nothing else -- no telemetry, no account, one LAN address.

source: INSTALL.md on GitHub -- this page is that file at spark v1.37