跳到正文
GitHub·· 6 天前AI 评分62

nl-veil 发布 v1.1.16:开源智能体编码桌面应用,新增图式长对话压缩与工具树

gary23w/nl-veil

AI 导读

开源智能体编码桌面应用 nl-veil 发布 v1.1.16,用依赖图而非摘要来延续长对话,并新增工具树让模型一次走七个分支,小模型也能使用数百个工具。该应用以单个 Zig 二进制打包桌面端、服务端、推理引擎与记忆引擎,可离线运行内置 the-veil-12b 模型,也可接入 Ollama 或 OpenAI 兼容端点。v1.1.17 已被撤回,作者建议已安装者改用 v1.1.16 安装包。

正文

the-veil

the veil

An AI coding team that remembers your project.

the Veil (nl-veil) is an open-source desktop app for building software with AI. Give it a task, let specialist agents share the work, and carry project memory into your next session. Use the built-in model, Ollama, or an OpenAI-compatible endpoint.

Download the desktop app · Screenshot walkthrough: setup & build with Cloudflare AI · Installation and requirements · Try your first task

Windows · macOS · Linux · MIT app code and AGPL security runtime. Local inference needs no provider account or API key; hosted providers have their own data handling and costs. The built-in model requires a separate download, and coding tools require Python on PATH (some checks also use Node).

Stable release: v1.1.16

v1.1.17 has been withdrawn. If you installed it, download and run the v1.1.16 bundle for your platform. Automatic updates do not downgrade an installed app.

New in v1.1.16

Gary's 4tope carries a long chat forward by graph instead of by summary. When a turn has to compact, the engine reads its working log as a dependency graph, keeps the chain of results the current step rests on, and writes the continuation from the turn's own text: the step's last words, the files on disk, the calls that failed, and what the work depends on. No model call, no paraphrase. The tool tree (NL_TOOL_MAP=1) turns the belt into a map the model walks seven branches at a time, so a small model can use a belt of hundreds of tools. Long chats with Agent Garrett compact far less: its belt is 60% smaller and the spend ceiling now scales with it. Release results · how Gary's 4tope works · the tool tree.

New in v1.1.12

Agent Garrett's full 166-tool security catalogue is available to chats, swarms and tater-tots through typed MCP tools. Settings deploys the edge Worker, private Worker, Cloudflare Container and R2 storage into your account, then shows progress while the runtime builds. Enable each feature separately. Desktop margins, field spacing and past-run card borders are repaired, and Settings keeps scrolling with its scrollbar hidden.

Live cloud command execution, shell-session listing and indicator extraction were verified through NL-Veil. Release results · Security setup and personal defense guide.

New in v1.1.10

Tater-tot deployment now reconnects a missing or unreachable cached runtime before creating a tot, checks its public route, and reports Cloudflare's error when setup fails. Past-run cards have clearer spacing and status lines, the activity controls have their own header, and Remove run clears an entry from history while keeping its files. Release notes.

New in v1.1.9

Each tater-tot deployment has its own console and run history. Redeploying the same name starts a fresh console, while deleted and failed deployments remain under Past runs with their events and failure details. Deploy again opens the form with the previous request. Release notes.

New in v1.1.8

Tater-tots can read, edit and deploy their shared Worker runtime, including their prompts, tools and stop rules. Browser tasks can work through CAPTCHA challenges and continue. Runtime uploads are applied by your running, Cloudflare-connected veil; cloud work continues on the last deployed version while it is offline.

Admin credentials are checked against durable storage before the listener or Tunnel starts. Legacy default passwords are replaced, an explicitly configured admin password is applied to existing accounts, and startup stops if a required change cannot be verified. Release notes.

New in v1.1.7

Tater-tots: the veil's goal loop running in your own Cloudflare account, with nobody in it and your computer off. Deploy one from the desktop's Tater-tots tab or veil --tater deploy "<goal>"; the first is always named Gary. How tater-tots work · release notes.

New in v1.1.4

A calmer desktop for longer tasks: visible preparation and planning updates, steadier bottom-follow while replies stream, and settings grouped into General, Models, Connection, Data, and Updates. New chats offer editable starter prompts. Code blocks wrap long lines and keep copying faithful to the original text.

This release also fixes an HTTP completion race that could report a false timeout. Read the release notes and desktop guide.

Try your first task

After installing, select a model and use a small project you can safely edit. Start with one concrete change:

Find one missing input-validation test in this project. Explain the expected behavior, add the test, make the smallest fix if needed, and run the relevant tests. Report the files changed and the actual test result. Remember the project's test command for our next session.

Review the diff and test output. Then start a new conversation on the same project and ask: "What test command did we use, and what did we change last time?" This gives you a specific way to evaluate both the coding workflow and memory on your own code.

Trying it for the first time? Tell us what worked or where you got stuck. Include your OS, app version, model/provider, and expected versus actual behavior. Remove private code and credentials from anything you share.

NEURON-LOOPS · NL-VEIL — an agentic coding desktop app that runs a whole team of AI agents on your machine, and remembers. One binary, many minds, one shared memory. Browse the annotated source of every module at the docs site.

build release zig built-in model memory cortex memory license stars

What this actually is

Desktop release updates: official bundles check GitHub at startup. Use Settings → Updates → App updates → Update & restart to install a verified release without Git. See updates, recovery and Cloudflare connectivity for first-install requirements and Windows/macOS network troubleshooting.

A desktop app for agentic coding, called the Veil. You download one file, run it, and a native window opens: a three-pane workspace where you give an AI a goal and watch it do the work. Your conversations down the left, the work itself in the middle — the chat, or the files it is writing, or live token and latency metrics — and on the right what the agents are doing and what the thing remembers. Drag the dividers to whatever widths suit you (they persist), collapse a side you don't need, branch a side question into its own tab without losing the build. Seven tabs across the top: Dashboard, Chat, Tasks, Swarm, Tater-tots, Hub, Settings. It is the place you work, not a sidebar bolted onto something else.

The difference is what's behind the window. Ask most tools for a feature and one assistant writes it. Ask the Veil and it can split the job across a swarm — a handful of specialist agents, each owning real files, building and critiquing in parallel against one shared workdir with its own version control, reconciled back into a single answer. You talk to one voice; a team is doing the work behind it.

And it remembers. Not chat history — a memory engine that accumulates what it learns about your code and your preferences, keeps a playbook of fixes it has verified by running commands on this machine, and carries all of it into the next session. The same problem stops getting re-solved.

Local inference, with optional cloud connections. One Zig binary carries the whole thing: the desktop app, the server behind it, a web UI, the inference engine, the memory engine, and the veil command line. Nothing to install first — no Docker, no cloud account, no database service, no API key — it will download a 12B model and run it itself, offline, on your GPU. One honest dependency: the agent's tool belt shells out to Python (list_dir, run_tests, web_search, deep_crawl, and its own executor run_python) and to Node for a JavaScript syntax check, so without python on PATH the agent can chat but cannot do much work — it tells you exactly which tool wanted what. Point it at Ollama or any OpenAI-compatible endpoint instead whenever you'd rather. Your local conversations, keys and memory are stored in a folder next to the binary. Hosted model calls, web tools, and optional Cloudflare backups send the data needed for those features to the services you connect.

⭐ If the veil is useful to you, star it on GitHub. It's a solo project — a star genuinely helps it reach the next person who'd get something out of it.

What's in it now

Log in with Cloudflare one click: your account's live Workers AI models, an automatic R2 backup of your chats, and an agent that can deploy to your own account
The built-in model the-veil-12b served in-process from a downloaded GGUF — no external runtime, no key, works offline
The chat brain in the server one agentic turn loop, server-side; the web app, the desktop and veil chat are thin clients of it
A chat without end every summary fold banks its facts in an append-only ledger, and memory is settled around every round of a turn as an advisory overlay that never enters the transcript
The model trio every LLM call is labelled and routed — coding / thinking / prompting — so a cheap model can carry the bulk
Your own browser drive the Chrome/Edge you're already signed into, through a tiny extension — or a private throwaway profile
The tool belt ~58 tools: files, shell, tests, web, deep crawl, memory, images/OCR, MCP servers, tool authoring
Memory that compounds neuron-db behind every recall, with --lineage so re-casts get better
Built-in RAG mirror the nl-rag corpus to disk and research with the network off
It learns a playbook of fixes verified by real exit codes, a smoke gate, and a judge that reads the trace
Themes & plugins Lua, hot-reloaded, across web + desktop + CLI (PLUGINS.md)
Scheduled tasks · the desktop · the fleet hub cron-ish runs that are real conversations, a native dashboard, one console over every swarm

Browse the annotated source of every module at the docs site (docs/) — an interactive architecture map and ~100 generated source documents, in the same design system the desktop app compiles in.

Log in with Cloudflare

Frontier-scale reasoning models, through your own Cloudflare account. Cloudflare describes Kimi K2.6 as a frontier-scale model with reasoning, multi-turn tool calling and a 262K-token context window. The Veil's real build walkthrough used this model to build and refine a customer-support workspace.

The Workers Paid plan starts at $5 USD/month and includes 10,000 Neurons per day. Usage beyond that allowance is billed at the model's rates: the $5 plan is not unlimited AI usage. The Veil is MIT-licensed with no Veil subscription required; you pay your chosen provider directly. Local models remain an option.

One button, and the veil gains a cloud. Click Log in with Cloudflare — in the web app's Settings, under the chat composer, on the Dashboard, or in the desktop's Settings — approve the consent screen in your browser, and you are connected. No token to paste, nothing to configure.

It is your account, not ours. The veil is open source and every copy ships the same public client id, the way wrangler and gh do; the token that comes back is minted for your Cloudflare account and lands in your local server's sealed vault. It never reaches the maintainer, and there is no nl-veil server in the middle — Cloudflare redirects the browser to localhost, which is the machine already in front of you.

What you get

Workers AI, live-synced Your account's real model catalogue is fetched from Cloudflare and replaces the Workers AI group in every model picker. The repo ships exactly one model id as a bootstrap default — the list you pick from is always your account's, never a stale copy in a file.
Chat on the free tier Workers AI has a free daily allowance (10,000 neurons/day as of 2026) — no paid plan and no card for the base tier. Logging in points chat at it automatically; one click in Settings puts you back on local or BYOK.
R2 chat backup The first login provisions an nl-veil bucket in your account and mirrors your conversations and durable memories into it, incrementally, in the background. Your data stays local too — this is a copy, not a move. R2 is 10 GB free with no egress fees, but must be activated once on your Cloudflare dashboard; until then the card says so in Cloudflare's own words.
An agent that can deploy With the build scopes granted, the assistant can write a Worker and ship it to a live workers.dev URL, read and write R2, query D1, and reach the rest of the API — from the web app, the desktop, veil chat and scheduled tasks, on the admin account; see the cf_ tool belt.
Everywhere a turn runs Chat, swarm casts and scheduled tasks all resolve the same login, so a task that fires at 3am uses it too.

What it asks for, and what you can refuse

The consent screen asks for four required permissions — the minimum to know which account you are and to run inference:

Workers AI Read + Write      run models, list your catalogue
Account Settings Read        resolve your account id
User Details Read            your name / email, to show "signed in as ..."

Everything else is optional and declinable right on the consent screen — Workers Scripts, R2, D1, KV, Pages, Queues, Vectorize, Routes, Tail, Observability. Decline them and you still log in and chat; a tool that later needs one simply refuses with Cloudflare's own explanation. Grant them and the agent can build.

DNS, zone and Access scopes are requested — optional and declinable like the rest. The tunnel needs them to put this veil on a hostname you own: argotunnel, dns.read/dns.write, zone.read, and zone-access.read/zone-access.write with access-org.read. Decline them and everything except a custom domain still works — the default tunnel is a random trycloudflare.com address that needs none of them (tick use my domain without them and the switch says so and asks you to log in again). Grant them and you are granting real DNS write on your zones, which is what putting a name on a tunnel requires; the consent screen names each one.

Never requested at all: billing and payment, WAF and security-rule configuration, or the ability to add members to your account.

Disconnect any time — Settings → Disconnect. That forgets the credential locally; the R2 bucket and everything in it stay in your Cloudflare account, because they were always yours.

The cf_ tool belt

When — and only when — a turn holds Cloudflare credentials, six tools join the assistant's belt. A user who never connected is never even shown them. The belt belongs to the admin account: a normal account's turns run sandboxed, and deploying code or writing to a cloud account is outside that sandbox, so a non-admin who connects Cloudflare still chats on its Workers AI but is not shown the belt.

They work from every surface: the web app, scheduled tasks, the desktop app and veil chat. A cf_ call always runs in the server, the one process that holds your token. The desktop and veil chat run their other tools on your machine, so the one file a call touches crosses over instead: the file a deploy or an upload sends is fetched from your workdir first, and a download is written back into it and read back to confirm it arrived. If your file cannot be fetched, nothing is sent to Cloudflare; the server's older copy is never sent in its place. When the desktop shares the server's data folder (one machine, the usual install), nothing is copied at all. A client on another disk (veil chat in its own folder, or a desktop on another machine) can carry text files up to 512 KB that way. A larger or binary file is refused before an upload, and after a download the result says it did not reach your machine.

tool what it does
cf_deploy_worker Ship an ES-module Worker from the workspace and get back a live https://<name>.<you>.workers.dev URL
cf_r2_list List your buckets, or the objects inside one
cf_r2_put / cf_r2_get Move files between the workspace and R2
cf_d1_query Run SQL against a D1 (serverless SQLite) database
cf_api Any other v4 endpoint — Pages, KV, Queues, Vectorize, routes, logs. Write {account_id} in the path and it is filled in

Try it in one sentence: "write a Worker that returns the current time as JSON and deploy it."

Credentials are resolved once per turn and handed to the tools; blank means the family is neither advertised nor callable, so swarm minds and the client-side tool executor structurally cannot reach your cloud, and the token never rides a tool call out to the desktop or veil chat. File arguments are jailed to the workspace, and cf_api refuses any path that would send your token anywhere other than api.cloudflare.com.

Public URL - the tunnel switch

Once you are logged in with Cloudflare, Settings gains a Public URL (Cloudflare Tunnel) row with a switch and a box. Flip the switch and the veil is reachable at a Cloudflare URL. By default that address is confidential: a random, unlisted trycloudflare.com hostname that changes on every start and puts nothing on any domain you own - the veil is a personal harness, and a URL nobody can guess or look up is the right default for one. The switch finds or fetches Cloudflare's official cloudflared connector and runs it against this server; the URL lands in the box (Copy / Open), the desk shows the same row, and the switch position survives a restart. On a headless box, NL_TUNNEL=1 (or veil --tunnel) turns it on at boot and the URL is printed in the log once it is published.

The URL appears once Cloudflare's own resolver has published it, usually within ten seconds of the connector registering, or after a 90-second budget flagged as not yet published. That short wait is deliberate: a lookup made before the name exists leaves it unresolvable on that network for half an hour (the domain's negative-cache TTL), so nothing on your machine, not even the status poll, gets to ask first.

Tick use my domain and it becomes a named tunnel through your own account instead: created on the account, pointed at this server, given the hostname you typed on one of your domains (or veil.<your first domain>), and put behind a Cloudflare Access policy that admits only your own email when your account has a Zero Trust organization. That address is permanent and public in the DNS sense, which is exactly why it is a choice and not the default. Type a different hostname later and the tunnel moves with it: the old name's DNS record and Access app come off your account, and the new name gets its own.

What keeps that safe to do:

  • owner only - the switch is admin-only, and it exposes the whole server, so no ordinary user can open one.
  • registration stays closed - the tunnel refuses to start while open registration is on, and the server refuses registration on any request that arrived through a tunnel or proxy, whatever the flag says.
  • no "localhost trust" for tunneled traffic - the connector delivers the internet to this server from 127.0.0.1, so every decision that used to mean "this caller is on the machine" also requires the request not to carry the headers a tunnel adds, and the login guard rate-limits on the visitor's real address.
  • the tunnel token stays sealed - it lives in the vault and reaches cloudflared through its environment, never on a command line or in a file.
  • Access when possible - with a Zero Trust organization on the account, Cloudflare's own login page stands in front of the veil's; without one, the veil's rate-limited login is the gate, and the status line says which of the two you have. A confidential trycloudflare.com address never has Access, and never claims it.
  • off means off - the connector is stopped and reaped, and closing the desktop app stops it too (the switch stays where you left it, so the tunnel comes back at the next start); {"on":false,"delete":true} on the API also removes the tunnel, its DNS record and its Access app from your account.

The login asks for the tunnel, DNS, zone and Access scopes as optional permissions. A login that predates them simply lacks them; the switch then says so and asks you to log in with Cloudflare again.

Tater-tots - goal loops that run without you, in your own account

A tater-tot (a tot, short for Tiny Overview Technician) is the veil's goal loop with no human in it and no machine of yours under it. Once you are logged in with Cloudflare, the desk's Tater-tots tab (or veil --tater deploy "<goal>") uploads one small Worker, veil-tots, into your account and creates a tater-tot in it. From then on the tater-tot works on its own: it wakes on a timer, picks the single best next improvement toward its goal, does it with its tools, has a judge measure whether it helped from the tool results alone (against the iterations before it: an attempt that failed and left things as they were is same, and only something that got worse is regressed), records the iteration, and sets its next wake-up. Your computer can be off. Its model calls go through the account's own AI binding, so they never cross the public internet and need no API key.

  • 24 per account by default, more if your setup carries them. The first is always named Gary; you name the others. Raise the limit with the -/+ beside the count in the tab, or veil --tater limit N (up to 1000). How many an account really carries is its Cloudflare plan's to say: each tater-tot is a Durable Object that wakes every few seconds and calls the account's AI, so a free plan's daily allowance runs out long before a paid one's.

  • It never waits for anyone. A message you send lands in its inbox and the next iteration reads it as a directive. /goal <text>, /goal stop, /queue <goal>, /charter <text>, /pause and /resume work in the tab's command line and through veil --tater tell <name> "...".

  • A goal that ends is not the end. Achieved, out of budget, or three iterations with no improvement: the tater-tot takes the next queued goal, or proposes one itself from its charter, or rests and looks again later. /pause is what holds it still.

  • It improves itself. An iteration that did not move the goal becomes a lesson - one rule the tater-tot writes for its future self - and its lessons ride every later prompt; the least useful one is dropped when the list is full. After two flat iterations it grows by one mind, and after an easy win it shrinks, inside the size you allow (the tab's Grow / Shrink buttons move that ceiling).

  • Tater-tots work together. They share one scratchpad, which you can read and write too, and can message each other. A tater-tot can also cast its own small swarm: several minds side by side, one task each.

  • Its tool belt. A tater-tot works with real tools, in the cloud:

    • files - its own workspace (write_file, read_file, edit_file, append_file, list_files, delete_file), kept across iterations and mirrored to your machine;
    • the web - web_search (a keyless chain: public SearXNG instances, DuckDuckGo, Bing, reference lookups, and - when those refuse a datacenter address - the same search through the real browser), web_fetch, and http_request for any method, headers and body; private and internal addresses are refused, on redirects too;
    • a real browser - Cloudflare's browser, driven over the DevTools protocol. browser_open and browser_read give the page as its text plus a numbered list of what can be clicked or typed into, and browser_click, browser_type and browser_select act on an element by its number with real mouse and key input (so forms and sign-up pages built on modern frameworks respond); browser_key, browser_scroll, browser_back, browser_wait and browser_eval do the rest. The page stays open between iterations. When a page presents a CAPTCHA or other bot check, the tater-tot can use its browser tools to solve it and continue. Search keeps a challenge page open and returns its controls so the tot can work through it;
    • Python - run_python runs a script beside the tater-tot's files and keeps what it writes. A Worker's Python has no sockets, no processes and no pip, so the runner supplies what a script expects: import requests and urllib work on the Worker's own fetch, a missing import is installed from PyPI by itself (pure-Python packages; pip install and pip_install do the same) and stays installed, and anything that needs a process or native code is refused in words the tater-tot can act on. Packages with native code cannot be fetched by a running script, so the upload asks Cloudflare for a set of them by name (numpy, regex, pandas, matplotlib, pillow); if Cloudflare refuses the set, or the Python does not start with it, a smaller set goes up, down to the plain Python. Every iteration the tater-tot is told what its Python has and what it cannot have, so it plans around the gaps instead of finding them one failed step at a time; save_skill keeps a script as a tool of the tater-tot's own and run_skill runs it again, so a tater-tot builds its own tools;
    • a mind - the tater-tot's facts live in neuron-db, the veil's own memory engine compiled to WebAssembly and uploaded with the runtime: remember keeps a fact and recall finds it by meaning. The same engine keeps stances - feel records how the tater-tot has come to regard a topic, a tool or a site from working on it - and a mood the engine sets from how its measured iterations have been going. Both are shown to the tater-tot every iteration and steer what it tries next. This is an affect model that shapes behaviour, not a claim that anything is felt. plan_set / plan_done keep a plan it sees every iteration;
    • each other and you - the scratchpad, tell, an inner swarm, goal_queue, say.

    Tots also control their actual shared Worker source through runtime_read, runtime_edit and runtime_deploy. They can rewrite prompts, tools, algorithms and stop rules without a proposal or approval gate. The owner's running veil uploads the requested revision through the existing Cloudflare login on its next minute's sync; this does not require the local-machine execution checkbox. runtime_read reports the deployment result and any Cloudflare compiler error. Self-edits are kept across server restarts and ordinary deployments; an edit affects every tot in the account. When the owner's veil is offline, deployment waits for it to return.

    Python runs in a second small Worker (veil-tots-py), the browser is Cloudflare's Browser Rendering and neuron-db is two more modules of the upload; each is added when your account takes it, and the tab says which a tater-tot has and why not otherwise.

  • Search that answers. Keyless search engines often refuse a cloud address, and their pages show a bot check to a cloud browser. Give the tater-tots a search API key and web_search asks it first: veil --tater key brave <key> (or google + google_cx). The key is stored as a secret on your Worker.

  • It stands guard, with or without a model. Name what a tater-tot watches - a page and the words it must show, a status it must answer, its content pinned; a DNS name's answers - and it looks every heartbeat before the model is asked, with no model at all, so the watch goes on when the model is dead, rate-limited or withdrawn. A change is a tripwire: an event, a scratchpad entry every tater-tot reads, a directive in its inbox, and a post to your webhook (veil --tater key alert <Discord or Slack webhook URL>). veil --tater guard Gary add https://example.com/health --text "ok" --every 60, or /guard ... in the tab.

  • Every event is chained. Each event's hash covers the one before it, so the events.jsonl on your machine is tamper-evident on its own: veil --tater verify <run> recomputes the chain and names the first row that was altered or dropped. Who knew what, when, held outside anyone's lab.

  • A DEFEND posture and a leash. /posture defend freezes the runtime, reports bot checks instead of working through them, names the tater-tot in every request it makes and holds it to read-only verification; /leash 3600 holds the goal loop whenever your veil has not been heard from for an hour (the guard goes on) and releases it the moment it is back. A tater-tot never runs on with nobody's veil alive.

  • Agent Garrett, the full security toolkit. The agent contributes 166 tools: 94 existing security tools and 72 Gary tools. Settings → Models → Deploy security tools provisions its edge MCP Worker, private execution Worker, dedicated Cloudflare Container and R2 storage in your Cloudflare account. The first cloud build needs no local Docker or separate Linux host; the button shows an animated loading bar during preparation. Existing Cloudflare logins need to reconnect once for Containers permissions. Enable Agent Garrett: on beside the chat's auto-loop, or check Agent Garrett in the swarm or tater-tot deploy form. Each opted-in feature receives individual security_<name> tools with typed arguments; garrett_tools and garrett remain compatibility wrappers. Chat calls keep the MCP bearer on the server. The cloud filesystem is separate from your local machine, which NL-Veil supplies through its local harness when requested. Setup, personal defense and lab workflows.

  • A console you can read. Each event is one line saying what happened; a row that went wrong is red. Click a row to open its full text under it, click again to close it, and errors shows only what failed.

  • Your machine, only if you say so. The deploy form has one box, unchecked by default: let it use THIS machine. Checked, the tater-tot may queue jobs for the veil on your computer - the full local tool set, run unattended as an ordinary chat conversation named tot_<name>_... you can open afterwards - and gets the results back. Nothing listens at home for this: your veil asks the tater-tot for jobs while it is running. The box is decided once, at deployment; it cannot be granted later.

  • It has a budget, and you set it. Each tater-tot has a pace - one iteration every N seconds, from 5 seconds up - and a number of model calls a day, or no limit; with a limit, it rests when the day's calls are spent until the next UTC day. Workers AI usage is billed to your account by Cloudflare like any other, so an unlimited tater-tot at a 5-second pace spends as fast as its model answers.

  • A model that only reasons does not stall it. A reply with no visible answer is asked again with more room, and then answered by a model that does not reason, with a line in the console saying so.

  • You pick its model from your account. The deploy form's MODEL list is your login's live Workers AI catalogue. A goal or charter may be as long as that model can carry - about a tenth of its context window, between 800 and 4000 characters - and the form counts as you type.

  • Every deployment is a run of its own, like a chat. The tab lists the live tater-tots and, under them, past runs: a tater-tot you deleted (the server syncs its last events before it goes) and a deployment that failed (kept with what was asked and why it failed - the tab opens it when a deploy fails). Deploy the same name again and it is a new run with a new console; the old one stays under past runs, read from its folder, with Deploy again to start the form from what it asked. Remove run removes an entry from history while preserving its folder; remove that folder's .hidden marker to restore the entry.

  • Every run has a folder on your machine. Each deployment of a tater-tot is mirrored, once a minute, into <data>/u<id>/_tots/<name>-<deployed>/: events.log (one readable line per event - tail it), events.jsonl, status.json and notes/ (the tater-tot's own notes, one file each). The shared scratchpad is _tots/scratchpad.md. Open folder in the tab opens it; a deleted tater-tot keeps its folder.

  • The scratchpad can be cleared for the next set of tater-tots (two clicks in the tab, or veil --tater pad --clear); a copy of what it held is kept beside it as scratchpad-<when>.md.

  • Deleting the last tater-tot removes the Worker. The veil-tots Worker exists for its tater-tots: while any remain it stays, and when the last one is deleted it is removed from your account. A newer veil replaces the Worker's code in place, and the tater-tots keep their memory.

  • Hots are tater-tots now. v1.1.7 called them hots. The first time a newer veil runs while you are logged in to the same Cloudflare account, it moves them: each hot is created again as a tater-tot with the same name, goal, settings, owner's-machine grant and files; the shared scratchpad comes along; and only then is the old veil-hots Worker removed, so nothing keeps running under the old name. What a hot had learned (lessons, facts, stances) stays behind, and its first event as a tater-tot says so. Its local folders move from _hots to _tots.

Seen working

TATER-TOTS vs DOTS - the trailer

The trailer (2:20, sound on) is above; the numbers below are not from a trailer. They are three runs on a real Cloudflare account, each a tater-tot named Gary given the one-word goal test at a 5-second pace - the hard kind of goal, where nothing says what done looks like - copied from the folders the veil mirrors to the owner's machine:

run model iterations improved what it left in its folder
1 llama-3.3-70b-instruct-fp8-fast 18 1 3 files
2 llama-3.3-70b-instruct-fp8-fast 49 21 24 files - TF-IDF, Jaccard, Spearman and co-occurrence reports, an HTML comparison page; 12 lessons; mood "confident"
3 kimi-k2.7-code 13 3, the last at 6/6 a checklist of what test means, test_runner.py, test_results.json: 3 of 3 pass

Run 3 is the first on the runtime that writes down what a vague goal means before working on it. From its events.log, trimmed to the lines that tell the story:

19:48:52  r1   pick     Write a `test_goal_checklist.md` file that defines concrete, tool-verifiable items ...
19:49:40       lesson   Never stop at a checklist; always create and run the minimal test artifact, then
                        verify it passes before reporting progress.
19:51:23  r3   verdict  improved [5/6]: py_compile OK; test_results.json has "status":"pass" and
                        "tests_run":3; required files exist.
20:01:45       lesson   If os.getcwd() differs from the workspace shown by list_files, enumerate /tmp and
                        /workspace subdirectories to find where the files actually exist ...
20:14:22  r11  verdict  improved [6/6]: run_python verified py_compile OK, exit code 0, stdout "TEST OK" /
                        "PASS: 3", stderr empty, and test_results.json with status "pass" and tests_run 3 ...

The same run showed what to fix next, and both are fixed: its model sometimes answered in its own native tool-call markup instead of the JSON protocol (the runtime now reads that markup as the calls it meant, and a step written in it as plain words), and a script saw only the files it named, which is what that 20:01 lesson was working around (a script that names none now gets the whole workspace beside it, by plain names).

The desktop's console shows one line per event, a failed one in red; click a row to open its full text, and errors keeps only what failed. veil --tater lists them, veil --tater watch Gary follows one, veil --tater rm <name> deletes one, and veil --tater teardown --yes removes the Worker and everything the tater-tots stored. The upload needs the optional Workers scope the login already asks for; a login that declined it gets Cloudflare's refusal in plain words.

Running your own OAuth client (optional)

The shipped client id works out of the box. You only need your own if you are forking the veil or want a different scope set. Register one at Manage Account → OAuth clients → Create client: response type Code; grant types Authorization Code and Refresh Token (without the second, offline_access is refused and logins die at the first token expiry); token auth None (PKCE); redirect http://localhost:8787/api/v1/oauth/cloudflare/callback. Then:

NL_CF_OAUTH_CLIENT_ID=<your-public-client-id>   # overrides the compiled-in default
NL_CF_OAUTH_SCOPES="..."                        # must match what your client declares
# optional, sane defaults shown:
NL_CF_OAUTH_REDIRECT=http://localhost:8787/api/v1/oauth/cloudflare/callback
NL_CF_OAUTH_AUTH_URL=https://dash.cloudflare.com/oauth2/auth
NL_CF_OAUTH_TOKEN_URL=https://dash.cloudflare.com/oauth2/token

A warning worth the ink: scope ids are Cloudflare's internal permission-group slugs, not hyphenated dashboard labels. "Workers AI" is ai.*, "Workers" is workers-scripts.*, and "Pages" is page.* — singular. One wrong id rejects the whole authorize request, so look them up in the catalogue (GET /client/v4/oauth/scopes, or Cloudflare's mirror in cloudflare/mcp) rather than deriving them.

The HTTP surface

POST /api/v1/oauth/cloudflare/start      mint the consent URL (PKCE + CSRF state)
GET  /api/v1/oauth/cloudflare/callback   the browser lands here; the token is sealed server-side
GET  /api/v1/oauth/cloudflare/status     connected? as whom? (never returns the token)
GET  /api/v1/oauth/cloudflare/models     your account's live Workers AI catalogue
POST /api/v1/oauth/cloudflare/logout     forget the credential
GET  /api/v1/oauth/cloudflare/r2         backup status: bucket, files, bytes, last sync
POST /api/v1/oauth/cloudflare/r2/sync    run a backup pass now
POST /api/v1/oauth/cloudflare/r2/auto    turn the automatic backup on or off
GET  /api/v1/tots                        your tots: the roster and the deployment status
POST /api/v1/tots                        deploy one (uploads the runtime when the account lacks it)
POST /api/v1/tots/:name/command          a command or a message for one tot
POST /api/v1/tots/:name/config           its settings: model, pace, size, daily calls, charter, paused
GET  /api/v1/tots/:name/events           its event tail
GET  /api/v1/tots/pad   (and POST)       the scratchpad the tots share
POST /api/v1/tots/pad/clear              empty it (a local copy is kept)
POST /api/v1/tots/keys                   give the tater-tots a search key (brave, google, google_cx)
POST /api/v1/tots/limit                  how many this account may run ({"max": N}, 1 to 1000; 24 by default)
GET  /api/v1/tots/runs                    every run kept on this machine: live, ended, failed (newest first)
GET  /api/v1/tots/runs/:run/events       one run's events, read from its folder (?after=N)
DELETE /api/v1/tots/runs/:run            remove a history entry; its files are preserved
DELETE /api/v1/tots/:name                delete one (the last one takes the Worker with it);
                                         DELETE /api/v1/tots removes them all

The token is stored per user, auto-refreshed, and never returned to any client — the UIs only ever see the connection status and your profile.

One server, three faces

The mental model, because everything else follows from it: the server process is the whole system. It holds the accounts, the memory, the model keys, and it is the only thing that executes tools. The web app, the desktop window, and the CLI are all just clients of the same /api/v1 surface — none of them holds state the others can't see.

                    ┌──────────────────────────────────────────────┐
   a browser,  ───►  │  veil  (ONE process, ONE binary)              │
   any device        │                                              │
                     │   http :8787   ──►  route table (main.zig)   │
   the desk    ───►  │                       │                      │
   window            │                       ▼                      │
                     │                  /api/v1/*                   │
   `veil <verb>` ──► │       accounts · memory · sealed keys ·      │
                     │       swarms · scheduled tasks · TOOLS       │
                     │                       │                      │
                     └───────────────────────┼──────────────────────┘
                                             ▼
                                   this machine's disk,
                                   shell, network, models

(a) The web app. web/public/{index.html,app.js,styles.css,models.json} — no bundler, no build step, no framework. index.html is a single <div id="app"></div>; the entire UI renders from app.js. The four files are embedded into the binary at compile time (build.zig:76-79) and served by staticIndex / staticJs / staticCss / staticModels (src/main.zig, the route table). Tabs: Dashboard, Chat, Tasks, Swarms, Admin (admins only), Settings.

(b) The desktop window. desk/*.zig, compiled into the same binary via -Dapp (default true, build.zig:121). In app mode the GUI runs on the main thread and the HTTP server on a background thread (src/main.zig, "APP MODE") — one process, no child to spawn, no second executable in the bundle. raylib is a lazy dependency, so -Dapp=false never fetches it at all.

(c) The CLI. src/cli.zig dispatches every verb over HTTP to the running server (src/cli/{chat,hub}.zig for the two big ones). There is no second control plane and no argv secrets.

Why that shape matters to you

  • Your phone gets the same app as your desktop, and nothing is installed on the phone. It's a URL.
  • The tools run on the host machine, not in the browser. A browser can't execute a shell command, so the server does it — web/public/app.js deliberately omits the tool_client flag for exactly this reason (see the comment in sendTurn), and the turn's tools run server-side through POST /api/v1/chat/tool. When you ask the veil in a browser to write a file, the file lands on the machine running veil.
  • Accounts are isolated on disk. Each user's data lives under {data}/u{uid}/…, and non-admin accounts run a restricted tool surface (src/worker/chat/tools.zig, toolSafe / chatTool). See everyone else is sandboxed.
  • One provider key can serve everyone, or everyone can bring their own. That choice is yours and it has a real cost — see the shared provider key.

Install

Download it and run it — no toolchain, nothing to build. Grab your platform's bundle from the latest release, unzip, and run veil:

You're on Download Then run
Windows veil-…-windows-x86_64.zip veil.exe
macOS (Apple Silicon) veil-…-macos-arm64.zip ./veil
macOS (Intel) veil-…-macos-x86_64.zip ./veil
Linux veil-…-linux-x86_64.zip ./veil

That single action starts the local server and opens the desktop app.

Take the veil-… bundle for your OS — not "Source code (zip)". GitHub attaches a source archive to every release automatically: it's the raw repo, building it needs the Zig compiler, and the install.ps1 inside is a developer script. The veil-server-… assets are the headless server for remote boxes — no desktop app.

The builds are unsigned. Windows shows "Windows protected your PC" → More info → Run anyway. macOS says the developer can't be verified → right-click → Open (or xattr -dr com.apple.quarantine <folder>). Signing certificates are still on the list.

Build from source instead — contributors (needs Zig 0.16+)

These installers clone the repo and build it, so they need Zig 0.16+ on PATH. This is the contributor path, not the quick start.

macOS / Linux

curl -fsSL https://raw.githubusercontent.com/gary23w/nl-veil/main/scripts/install.sh | sh

Windows (PowerShell)

iwr -useb https://raw.githubusercontent.com/gary23w/nl-veil/main/scripts/install.ps1 | iex

or by hand:

git clone https://github.com/gary23w/nl-veil && cd nl-veil
zig build run

The veil binary (Zig) and the neuron-db memory engine (Rust) are built for you on first run — each with a prompt before it downloads anything. Point --neuron-bin / --bin at existing binaries to skip the builds.

The unreleased lineage batching code on main requires Neuron commit 5db2c039bf8f20349299e64650aea2d6a2fce911 from batch/stats, which supplies the lineage count batching command. The v1.1.16 release bundle includes its matching engine.

Start it

veil is the launcher and the control plane. Run it with no subcommand to start the app; add a verb to drive the running server over its API.

veil                 # THE ONE-CLICK DEFAULT: starts the server AND opens the desktop app
veil --server-only   # server alone, no desktop (headless boxes, services, containers)

Closing the desktop window shuts the whole thing down — server and everything it spawned — so there's no stray process left listening. On Windows a double-click opens no console window; run it from a terminal and your terminal is left alone.

It listens on every network interface by default. NL_BIND is unset → the server binds 0.0.0.0, which means anyone who can reach this machine on port 8787 can open the login page (src/main.zig:475-487). That is the point — a phone on the sofa should be able to open it — but it is worth knowing before you leave it running on a café wifi. NL_BIND=127.0.0.1 pins it to this machine only. See the walkthrough.

About models: you don't have to bring one — download the built-in the-veil-12b in the app (or veil model pull) and it serves itself, see below. To use something else, point the endpoint with NL_LLM_BASE_URL / NL_LLM_MODEL / NL_LLM_KEY (a local Ollama is the fallback default), or just pick a model in the web UI / the desktop. The server always mints an admin key at data/.desktop_key; the CLI and the desktop read it automatically, so same-machine commands never prompt for auth. It is a file readable only by your OS user — opening the port to the LAN does not make a local file more reachable. Any veil <verb> auto-starts the server if it isn't already up.

Where your stuff lives: a data/ folder next to the veil binary — conversations, memory, swarm workdirs, logs, all of it. Nothing is written to a system temp dir and nothing leaves the machine. Move the folder and you move the whole install; back it up and you've backed up everything. (From a source checkout it resolves to the repo's own data/, so dev and release never share state.)

The built-in model — nothing to install

Most local-AI setups start with a second program: install a runtime, pull a model, keep it running, point the app at it. veil skips that. The inference engine is compiled into the binary, and the server serves the-veil-12b itself from a GGUF on disk. There is no runtime to install, no port to keep alive, no key to paste.

The weights are the only thing not in the download (a model is bigger than an app), so the first run offers to fetch them:

veil model status              # weights + engine + any download in flight
veil model pull                # download the published weights — resumable, sha256-verified
veil model import [--path F]   # copy a GGUF you already have (no --path: find your local runtime's copy)
veil model check               # is a newer release published? (compares shas; `pull` installs it)
veil model cancel              # stop the running pull — the partial resumes next time
veil model rm                  # remove the serving weights

In the desktop there's a panel for the same thing: a download button, a live progress bar, and one click to make it the active model. A finished download hot-swaps into the running server — no restart. An install manifest beside the weights records what is installed and how it arrived, so check can tell "you have the current build" apart from "you have something you imported".

  • It runs on your GPU when you have one. The Vulkan backend is compiled in and the loader is opened at run time, so a machine with no GPU falls back to CPU cleanly rather than failing to start. (-Dvulkan=false cuts ~50 MB of embedded shaders from the binary; macOS builds are CPU-only until a Metal tier exists — Apple silicon's unified memory carries it.)
  • The engine endpoint is not an open local port. It is loopback-only and every request carries a bearer minted fresh at boot, so other processes on the machine can't quietly use your model the way they can with a conventional runtime.
  • It is one provider among the others. Pick builtin in Settings, or keep Ollama, or a hosted endpoint, or a different model per role — see the model trio.
  • You can leave it out. zig build -Dbuiltin=false never even fetches the inference sources; the builtin provider then simply reports itself unavailable and everything else works as before.

The engine is tuned for small local models, not just big hosted ones. A model at or below ~24B (or with a ≤15k window) gets a compact tool belt and a tighter doctrine — fewer verbs to rule out per call, no delegation concepts, honest "I can't read that" answers instead of fumbled fetches. Capability isn't removed: the full tool schema still dispatches if the model calls it. For local Gemma-4 the engine also renders the prompt itself rather than going through the runtime's template — verified byte-for-byte against the model's own chat template, which measured 27/30 on the harness drills against 18/30 through the runtime's renderer (src/worker/gemma4.zig).

The belt manifest — why a smaller belt scores higher

A tool array is JSON, and on the compact tier it is still ~3 KB of nested schema sitting far behind the conversation. Asked whether it has a browser, a 12B does not re-read that JSON — it answers from impression. Observed live (c6a751a54): the model ran list_dir hunting for its browser tools in the filesystem, read the empty directory as proof, and denied a browser it was holding.

So the compact tier appends one generated line naming the belt it was actually handed (beltManifest, src/worker/chat/engine.zig) — every tool by exact name, plus a WON'T, NOT CAN'T rule that separates declining by judgment from claiming a capability is absent. It is derived from the final belt string after grants and plugin schemas merge, so it can never drift from what the provider sees, and it is turn-stable so prompt-prefix caching still holds.

Measured on the-veil-12b over 110 held-out drills across five sets:

serving configuration score invented tool names
20-tool belt, full doctrine, no manifest 92/111 — 82.9% 6
17-tool compact belt + manifest + date stamp 97/110 — 88.2% 2

The trimmed belt and the shorter doctrine contribute alongside the manifest, so the 5.3-point gain belongs to the combination. What points at the manifest specifically is the invented-name column — naming the belt in prose removes the model's reason to guess — and the same comparison on a later checkpoint moved invented names from 11 to 1. The counter-intuitive part is worth stating plainly: the smaller belt scored higher. Fewer tools, described in words, beat more tools described only in schema.

The manifest is deliberately compact-tier only. Frontier models read a 49-tool array without help, and the line costs tokens on every turn; extending it upward would need its own measurement on a large model rather than an assumption borrowed from a 12B.

Walkthrough: run it, add people, put it on your network

Written for someone who is not a developer. If you only want it on your own machine, stop after step 5 — the rest is about letting other people in.

1. Download and unblock it

Grab the bundle for your OS from the latest release and unzip it somewhere you'll find again. Builds are unsigned, so:

  • Windows shows "Windows protected your PC" → More info → Run anyway.
  • macOS says the developer can't be verified → right-click the veil file → Open → Open. (Or xattr -dr com.apple.quarantine <folder> in Terminal.)

2. Run it

Double-click veil.exe (Windows) or run ./veil (macOS/Linux). The desktop window opens and the server comes up behind it.

One rough edge, stated plainly: on Windows, double-clicking from Explorer relaunches veil without a console window (src/main.zig, detachOwnConsole) — which is what you want for an app, but it means the startup banner is invisible, and the banner is where the network URL and the admin password notice get printed. There is no log file to read it out of afterwards, and the desktop window does not display the URL either.

So if you care about the banner, start it from a terminal instead — when a shell owns the console, veil leaves it alone and prints normally:

# Windows PowerShell, from the folder you unzipped into
.\veil.exe
# macOS / Linux
./veil

3. Find the URL

On startup the server prints one complete URL per address this machine answers on (src/main.zig:861-889, using src/config/lan.zig):

neuron-loops 1.1.16 on http://localhost:8787
    open from another machine (phone, laptop) at:
      http://192.168.1.42:8787

If you missed the banner, ask the OS for the address instead — ipconfig (Windows), ifconfig or ip addr (macOS/Linux) — and use http://<that-address>:8787.

The port is 8787 unless you set NL_PORT (src/main.zig:409-413). It is the one place the port is resolved, so the CLI and the server always agree.

4. Log in as the admin

The first run creates an admin account. When you do not choose a password, it generates one on both network and loopback binds — and writes it to a file, because a banner you never saw is not a delivery mechanism:

data/admin-password.txt

That file sits next to the binary, beside the data it protects. The password is stable across restarts while it matches the admin account; a stale file cannot restore a superseded password (src/main.zig, readAdminPassword / writeAdminPassword).

  • Default email: admin@neuron-loops.local (change it with NL_ADMIN_EMAIL).
  • To pin your own password instead of using the generated one, set NL_ADMIN_PASSWORD before starting. Do that and no file is written — the password is the one you chose.

Older installs that still use the published changeme password are rotated to a generated one at startup, before the server listens. A custom existing password remains in force when no generated file or NL_ADMIN_PASSWORD is present.

5. Pick a default model for the instance

Open the web app (or the desktop) and go to Admin → Default model. Pick from the same catalog the Settings tab uses; it applies immediately, to everyone who hasn't chosen their own, with no restart. It persists to data/server-config.json.

NL_DEFAULT_MODEL / NL_DEFAULT_BASE_URL seed this on a fresh install for unattended setups — but once an admin sets it in the UI, the stored value wins, so a stale launch script can't undo it on the next restart (src/config/server_config.zig).

Without a default model, a brand-new account cannot chat until it configures one itself.


Everything below is about other people using it. If it's just you, you're done.


6. The shared provider key

Admin → provider key (POST /api/v1/admin/keys). It is stored sealed in the same vault as everyone else's keys, under a reserved uid 0 that no real account can hold (src/worker/chat/service.zig:31, :89-100).

The trade is worth stating outright, because it is a billing decision:

  • Without it, every new account has to bring its own API key in Settings before it can chat at all.
  • With it, nobody needs a key — and every user's turns spend your credit. A user's own key always wins if they have one, so setting this never silently switches a paying account onto your bill. But everyone else is on it.

For a family or a LAN of people you know, that's exactly right. For anything wider, think about it first, and size NL_MAX_TURNS to the rate limit of the key you just handed everyone.

7. Create accounts

Admin → + New user (POST /api/v1/admin/users). Enter an email and a password of 8–200 characters, then hand the password over out of band — the server never shows it again. Account creation is audited (who was let in, by whom, when).

Self-signup is off by default. NL_OPEN_REGISTRATION=1 opens it, which is the wrong posture for a box on a LAN and the right one if you're running something more public.

8. Other people connect

They type http://<your-ip>:8787 into any browser — phone, tablet, laptop, whatever's on the same network. Nothing is installed on their device. They get the same web app you do, minus the Admin tab.

9. The firewall (this is the step that fails)

If step 8 does nothing — a spinner, a timeout, "can't reach this page" — it is almost certainly the firewall, and the app will not tell you. There is no firewall detection anywhere in the codebase, so a blocked port looks identical to a wrong IP address from inside the browser.

  • Windows. The first time veil binds a port, Windows Defender Firewall pops up "Allow this app to communicate on these networks." Tick Private networks and allow it. If you dismissed that prompt — or clicked Cancel — Windows silently remembers the block. Fix it in Windows Security → Firewall & network protection → Allow an app through firewall → Change settings: find veil in the list and tick Private. If it isn't listed at all, Allow another app… and browse to veil.exe.
  • macOS. You may get "Do you want the application to accept incoming network connections?" — say Allow. Otherwise check System Settings → Network → Firewall → Options.
  • Linux. Usually nothing blocks it, but if you run a firewall you'll need to open the port (sudo ufw allow 8787/tcp on ufw-based systems).

Two things to check before you blame the firewall: both machines are on the same network (guest wifi is often isolated from the main one), and you used the LAN address from the banner, not localhost — localhost on their phone means their phone.

10. Locking it back down

NL_BIND=127.0.0.1 veil       # this machine only; nothing on the network can reach it

localhost works too. Anything else — or leaving it unset — binds every interface.

Before you put this on a network

Two limits you should know about before you hand out passwords.

Traffic is plain http://. There is no HTTPS in the server today. Logins, passwords, chat contents, and API responses cross your LAN unencrypted, readable by anything else on that network. On a home or office network you control, that is a normal trade. On shared or public wifi it is not.

Tools run on the host machine. Non-admin accounts are sandboxed: their turns get the conversation's own workspace, research, and the full hive-memory surface — but no code execution, no host commands, no engine self-modification, no tool authoring, no browser or MCP drive, and no casting swarms or scheduling runs. What that means: a normal user cannot run commands on your machine. What it does not mean: their work is still stored on your disk, still spends whatever provider key is in play, and the admin account keeps the complete surface — so anyone who gets the admin password gets a shell on the host, in effect.

About exposing this to the open internet: it's a different risk class from a home LAN, and this document isn't going to hand you a port-forwarding recipe as if it weren't. Plain-http logins over the public internet mean credentials in the clear; unsigned self-signup plus a shared provider key means your billing is the attack surface. If you genuinely need remote access, put it behind something that terminates TLS and authenticates first (a VPN or a reverse proxy you already trust) rather than forwarding 8787 at the router.

The veil command line

Every subcommand is a thin client over the running server's /api/v1/* — no second control plane, no argv secrets, no Python. The verbs:

veil                          start the app: server + desktop (--server-only for headless)

SWARMS
  --swarm "<goal>" [flags]     cast a swarm and WATCH it: every mind's step on the right, one chat line
                               into the whole swarm on the left; runs to completion on its own, with as
                               many minds as the goal needs (--minds N to choose)
      --minutes N  --model M  --provider P  --lineage <id>  --once  --background (no terminal view)
  cast "<goal>" [flags]        deploy a swarm to work a goal
      --minutes N  --minds N  --model M  --provider P  --base-url U  --key K
      --style S  --name N  --continuous  --offline  --follow
      --lineage <id>           persist this swarm's memory under <id> so re-casts COMPOUND
  deploy "<goal>" [flags]      alias for `cast --continuous` (a sustained hive)
  list | ls | ps               list your swarms
  stop <id>                    ask a swarm to stop
  rm <id>                      stop and remove a swarm
  events <id> [--follow]       stream a swarm's event log  (aliases: logs, watch)
  lineage [ls]                 lineages: what each has learned + how many proposals await review
  lineage show <id>            the end-of-run judge's and habit miner's quarantined proposals
  lineage accept|reject <id> <n>  promote proposal n into the live memory, or reject it for good

CHAT (the server-side veil brain)
  chat [conv]                  interactive REPL; a line typed mid-turn steers the running turn
  goal "<what to achieve>"     a GOAL LOOP: pick the next best improvement, do it, measure it, repeat;
      [--budget N] [--forever] [--check "<cmd>"] [--conv id]   ends when achieved, spent, or nothing improves
                               in any chat: /goal <text>   /goal   /goal stop   /goal resume

TATER-TOTS (autonomous goal loops that run in YOUR Cloudflare account)
  --tater                         the roster (24 by default - see limit); the first is always named Gary
  --tater deploy "<goal>" [flags] --name N --charter "..." --model @cf/... --pace SECONDS --size MINDS
                                  --calls PER_DAY|unlimited --budget N --forever   (--pace from 5)
                                  --local  lets it queue jobs for the veil on THIS machine (deployment only)
  --tater tell <name> "<text>"    /goal <text>, /goal stop, /queue <goal>, /pause, /resume, or a message
  --tater watch <name>            follow its events
  --tater set <name> [flags]      --model --pace --size --calls --charter --pause --resume
  --tater pad ["<text>"|--clear]  the scratchpad the tater-tots share (--clear empties it)
  --tater key brave <key>         a search key for their web_search (google, google_cx; --remove)
  --tater limit [N]               how many this account may run: 24 by default, 1 to 1000
                                  (your Cloudflare plan decides how many it really carries)
  --tater rm <name>               delete one     --tater teardown --yes   remove them all + the runtime

BUILT-IN MODEL (the-veil-12b, served in-process — no external runtime)
  model status                 weights + engine + any download in flight
  model pull                   download the published weights (resumable, sha-verified)
  model import [--path FILE]   copy a GGUF you already have into the store
  model check                  is a newer release published? (`pull` installs it)
  model cancel                 cancel the running pull (the partial resumes later)
  model rm                     remove the serving weights

KNOWLEDGE (the local pack corpus — built-in RAG)
  rag status                   is a local knowledge mirror active, and where from
  rag sync --from <clone>      copy an nl-rag checkout into the app's data dir for offline RAG
      --tier atlas|facts|full  manifest only | +INDEX+distilled facts (default) | +every page
      --dest <dir>             sync somewhere else (e.g. vendor/nl-rag pre-build)
      --include-auto           also copy machine-grown packs (off-topic risk; off by default)
  rag ingest <file>            absorb a local book/doc/notes into the hive as recallable facts
      --scope S --name L --cap N --db <hive.sqlite>

SCHEDULED TASKS (admin-gated)
  sched list
  sched add --name N --prompt "..." [--kind once|every|daily] [--every MIN] [--at HH:MM]
  sched run <id>               run a task now
  sched rm <id>                delete a task

FLEET
  hub                          roster: fleet summary + every swarm's state
  hub all "<text>"             broadcast a directive to every swarm
  hub goal "<text>"            set a new goal on every swarm
  hub stopall                  stop every swarm

EXTENSIONS (themes + plugins — see PLUGINS.md)
  themes                       list every theme (built-in + user)
  themes <id>                  print one theme's full palette
  plugins                      list loaded plugins, their tools + state
  plugins reload               admin: rescan <data>/plugins + <data>/themes, hot-swap

MISC
  doctor [--runtime]           server + token health; --runtime folds the runtime ledgers in
                               (learned tool behavior, schedule fail-streaks, per-model usage)
                               and works with the server down
  desktop | desk               open the app window (like a bare `veil`, but detached)
  exec-tool <tool> [args]      run one hive tool directly, in this process
  local-host                   the per-machine daemon that owns stateful local resources
                               (browser sessions) — started for you on demand
  sync-manifest / sync-read    the file-sync side of a delegated turn
  version | help

Cast in one line and watch it work:

veil cast "Build a CLI todo app in Python with tests" --follow
veil deploy "Keep researching fusion power and brief me each pass" --style discourse
veil cast "add a search box to my landing page" --offline

Make it your own — themes & plugins

veil is extensible without a rebuild. Drop a Lua file into your data directory and it works across the whole product — web, desktop, and CLI:

  • Themes (<data>/themes/*.lua) re-skin everything. The shipped dark, light, and matrix themes are seeded there on first run; copy one, change the id and a few of the 16 colors, and it appears in every client's theme picker. mono_ui = true renders the whole UI in the code font (that's how matrix gets its terminal look).
  • Plugins (<data>/plugins/<name>/plugin.lua) add tools the AI can call, policy hooks that gate what runs, and prompt hooks that shape every turn — all in a locked-down Lua sandbox. You can also bridge an external MCP server so its tools become veil tools.

The complete, copy-paste authoring guide — the veil.* API, the hook reference, the sandbox model, and templates for both — is in PLUGINS.md.

veil themes            # every theme, built-in and yours
veil plugins           # loaded plugins, their tools and state
veil plugins reload    # admin: pick up changes with no restart

The chat brain runs in the server

The agentic chat loop — planning, tool-calling, streaming, steering, and memory — runs server-side in src/worker/chat/engine.zig. Clients (veil chat, the desk, the browser) are thin: they speak three REST calls per conversation.

POST   /api/v1/chat/convs/:id/messages          # send a message → runs one server-side turn
GET    /api/v1/chat/convs/:id/events?from=<n>    # stream the turn's event frames from a byte cursor
POST   /api/v1/chat/convs/:id/control            # {"op":"steer","text":...}  or  {"op":"stop"}

One turn runs per conversation. While it's working, a line you type becomes a steer the running turn folds in without restarting; stop ends it. The turn writes its progress into the conversation's own store (messages.jsonl + events.jsonl), which is exactly what the event stream serves — so every client sees the same turn, live. The desktop prefers this backend and falls back to a bundled local engine if it's disabled (VEIL_CHAT_BACKEND=0) or unreachable. veil chat opens a terminal REPL over the same three calls:

$ veil chat
veil chat — conversation cli7f3a1c
  type a message; while the veil is working, a line STEERS the running turn.
  /stop  interrupt the current turn   /new  fresh conversation   /quit  leave

> what are you working on?

What the model sees each turn is governed, not accreted: every context block (memory, tool digests, corrections, plugin hooks…) bids into a prompt workspace with a fixed render order, per-channel byte budgets, and a provenance receipt on every admitted block — and each turn's admit/drop record lands in workspace.jsonl, so "why did it say that" has a queryable answer. Recalled memory arrives scored (numbers, not vibes), a fact that contradicts a stored sibling arrives marked [CONTESTED] with both sides shown, and a sentinel-gated second model audits doubtful memory before the answering model reads it — annotations only, nothing is silently deleted.

A long conversation keeps its past

Every fold of the rolling summary also appends the concrete facts it established — decisions, names, paths, values — to an append-only digest.jsonl beside the transcript, and each turn projects that ledger back for the live question with no model call: the newest lines, then the ones closest to what is being asked. The summary, the facts block and the verbatim window are all sized to the window and capacity of the model that reads them and the model that writes the folds, so a 32k model keeps the history it had, a large model with a 128k window replays 64 KB verbatim, and a small local summarizer is never handed a fold it cannot hold (context).

A long turn carries itself forward by graph, not by summary. When a turn crosses its spend ceiling, Gary's 4tope reads the turn's working log as a dependency graph: each result hangs off the step that asked for it, a step depends on the results it reused, and a later read or write of the same file supersedes the earlier one. The longest path back from the current step is the chain the work rests on; it is never pruned. The continuation is laid out along the shortest-path tree from that step, under a byte budget, in the turn's own words: NEXT, ON DISK, RULED OUT, ESTABLISHED. No model writes it, so nothing is paraphrased away, and a failure a later success replaced is not carried as current. Tool findings reach neuron-db at every step, so the carry is a layout of what is already remembered. NL_HANDOFF=model restores the model-written state. Before any of that, old tool results are stubbed before anything is summarized, and the working span grows with the model's window.

Inside a turn, a recall overlay keeps a small working field of the conversation's memory, your durable notes and the file ledger, grows it with every finding the moment it exists, and settles it around what the model is doing right now before every round of the turn's tool loop (the auxiliary verdict, compaction and planning calls go without it). It reaches the model as one advisory block that is removed the instant the model answers — never written to the transcript, a summary or the store. A line shown three times and never used is set aside for the next four calls; a line the model does use is strengthened in the store at the end of the turn. NL_MEM_OVERLAY=0 turns it off, NL_MEM_OVERLAY_BYTES sizes the block (900 by default).

When the veil keeps or drops a durable memory about you (REMEMBER: / FORGET:), the turn records that under its reply and announces it on the event stream, so the desktop's Memory tab refreshes on the spot and the change is never mistaken for a step still to do.

A provider failure costs a wait, not the turn. In a chat turn, any failed call to a hosted endpoint is retried up to ten times in a row — 5 s growing to 60 s apart, about five minutes in all — counted per provider, with the Cloudflare token re-resolved before each try. Each wait shows as a status line, and Stop ends it at once. Local endpoints and the swarm's workers keep the short ladder for transient errors only.

The tool belt

A turn is only as useful as what it can actually touch. The engine ships ~58 tools, all executed by the server on the host machine (never in your browser), all logged as event frames you can watch live:

group tools
Files & code read_file write_file edit_file delete_file list_dir stage_file stage_delivery
Run things host_command run_tests run_python host_status host_explore stop_process poll probe
Web & research web_search web_fetch read_url fetch_json deep_crawl read_doc osint_scan
Your browser browser_navigate browser_read browser_click browser_click_at browser_type browser_type_text browser_key browser_scroll browser_eval browser_console browser_network browser_close
Screen & images pixel_capture pixel_ingest pixel_search — screenshots and attached images become searchable text + regions, so a model that can't see gets the picture anyway
Memory observe recall recall_hive absorb journal share save_skill note_stance
Coordination send_message ask_veil add_task complete_task propose_plan_change
Other AI apps mcp_discover mcp_call — find the MCP servers already configured on this machine (Claude Desktop, Cursor, VS Code) plus local AI runtimes, and call their tools as if they were veil's
Self-improvement make_tool (author a new tool at run time) set_directive (write its own playbook) propose_change / simulate_change / patch_system (governed, trial-gated)
Secrets get_credential — the broker hands over a named credential; the value never enters the transcript

Two of these deserve a note. poll replaces blind sleeping: a bounded watch loop that samples a file, URL, port, process, command, or swarm until it's done or the budget expires, so a long wait is a chain of bounded polls instead of one guess. And stop_process exists because a turn that can start a server must be able to end one — a Stop from you now reaches a blocking tool instead of waiting for it to finish.

The tool tree (NL_TOOL_MAP=1, opt-in) advertises the belt as a map instead of a list. The model walks it with one verb, open_tools: at most seven branches at a time, each with one line saying what it is for, and reaching a group opens its tools by name. The engine opens the likeliest group from the request before the first call, a tool the model already knows opens its own branch in the round it is called, and a model that wanders for four rounds is handed a search of the whole map. Agent Garrett's catalogue, plugins and other MCP servers attach as branches of their own, and the map learns which words lead to which tools. Shrinking what is advertised never shrinks what is callable (the tool tree).

Non-admin accounts run a restricted subset (workspace files, research, and the whole memory surface — but no shell, no browser, no MCP, no tool authoring, no casting). See everyone else is sandboxed.

It can use your browser

Research that hits a login wall is research that stops. So the veil can drive the browser you are already signed into — your profile, your cookies, your sessions, and you sitting right there to answer a verification prompt — through a deliberately tiny Chrome/Edge extension in extension/.

  • Load it once. chrome://extensions → Developer mode → Load unpacked → pick extension/. It finds the local server itself, pairs over loopback only, and remembers the pairing across a server restart.
  • The extension holds no automation logic. It relays debugger commands and posts back replies — that's all. Every snapshot, click heuristic and page model lives in the server, so the app can get smarter without you ever reinstalling the extension. Input arrives as real browser events, not the synthetic kind a content script is limited to.
  • Pairing is loopback-only. A remote attacker can't pair because the request has to originate on the machine; the relay's blast radius is the tab it drives. The token is never logged.
  • Or don't use your browser at all. With no extension the driver launches its own throwaway profile, logged out of everything — the private option, still there and still the default for unattended casts.
  • Reads describe what happened. A click reports its effect, and form fields are read back and confirmed, so the agent stops re-reading the page to guess whether anything landed.
  • A page read is projected, never truncated. Ordinary tool results get clipped head-and-tail, but a page state is a ref table — byte-offset elision deletes elements the model then cannot see, and nothing distinguishes a dropped ref from an absent one. Measured on a live 18,317-character read: from the clipped payload the model named a confidently wrong ref for the search box, one it would then have typed into. The projection keeps every interactive ref and drops the JSON boilerplate repeated once per element — 59% smaller and lossless in refs, so it is not a size/quality trade.
  • It is told what the page cannot tell it. Navigating invalidates every ref, and typing into a button or a activates it instead of entering text; both are now stated in the tool descriptions. Elements the extractor has no label for are marked (unlabelled), duplicates (dup N) — on one real compose page, 17 of 175 elements carried no label and eleven separate ones all read "More".

A note on the tool descriptions, because it cost a week. A browser logic bomb is an instruction that is internally consistent and locally reasonable, and from which a correct model derives that the task is impossible. browser_navigate said "a login-walled dashboard — reach for this"; browser_type said "never type credentials". Valid conclusion: I cannot get in. The model then defended it by denying it had a browser at all, reciting its own tool list back as proof of absence. Three fixes lost that argument before anyone checked the premise, and deserved to — on the belt as written, the model was right. The browser is the user's own and already signed in; that was simply never stated. Tool descriptions are not documentation. They are the model's model of reality, and a wall written into one is indistinguishable, from the inside, from a wall in the world.

Browser access is an admin capability. NL_BROWSER_DRIVER=1 extends it to sandboxed accounts — worth a deliberate decision, since the browser inherits this machine's network position and your profile's logins.

Three models, one turn — the model trio

A turn is not one kind of work. It streams a reply and calls tools; it decides what done means; it writes itself a one-line instruction for the next step, over and over. Those want different models, so every LLM call the engine makes is labelled and routed to one of three roles:

  • coding — the agentic step: the reply that streams onto your screen and every tool call inside it (chat).
  • thinking — planning and transcript housekeeping: the task breakdown and the acceptance bar (plan), the post-hoc critique (reflect), compaction and the rolling summary (compact, ctxsum), plus summary and the scheduled-run lesson.
  • prompting — the short, frequent calls that write the next instruction rather than answer anything: the auto-loop drive step (loop), web-search query rewrites (searchq), stuck recovery (stuck).

One model for everything is the default and is fully supported. A role counts as set only when it has both a model id and a base URL; anything else is blank, and a blank role falls back to coding (ModelTrio.pick in src/worker/chat/engine.zig). Leave thinking and prompting empty and the engine behaves exactly as it did before roles existed. The routing is guarded by a source audit (src/worker/chat/trio_routing_test.zig) that fails zig build test if a label ever reaches the wrong role.

Set it in Settings → Models (web), Settings (desktop), or Admin → Default model to publish a trio to everyone who hasn't chosen their own — per role, so an account that picked only a coding model still picks up the host's thinking and prompting. For veil chat it's environment: NL_LLM_* for coding, NL_LLM_THINK_* and NL_LLM_PROMPT_* for the other two.

The one thing worth knowing before you choose: thinking is not uniformly the expensive-model role. Measured over real request bodies, plan averages about 1 KB per call and carries all of the judgment, while compact and ctxsum are tens of KB per turn and are mechanical compression. Both run on the thinking model, so that single setting pays for a rare high-stakes call and a bulky low-stakes one at the same time. Prompting is the easy call — small outputs, no judgment, high volume — so it is the one to make cheap.

Full routing table, the per-call measurements behind that advice, the cost split, and the limitations are on the docs page: the model trio.

Scheduled tasks

A scheduled task runs strictly through chat: when it comes due, the server mints a fresh conversation named scheduled_* and fires one ordinary chat turn at it — so a run persists, streams its events, shows up in the conversation list, and can be continued by hand afterwards. Manage them with veil sched (admin-gated):

Because every run gets a new conversation, a run that was cut short used to leave nothing the next one could read, and a task that kept hitting the spend ceiling restarted from zero every time. Since v1.0.1-beta-6 the engine banks what a cut run established as a resume anchor against the task itself rather than against the transcript, so the next run reads it back and picks up from the step it names. A run that finishes cleanly clears its own anchor, so this is invisible until something is actually left unfinished.

veil sched add --name nightly --prompt "summarize today's repo changes" --kind daily --at 22:00
veil sched add --name watch   --prompt "check the build and report" --kind every --every 30
veil sched list
veil sched run <id>          # fire it now → prints the conversation it created
veil sched rm <id>

Kinds are once (fires at a set time), every (--every MIN), and daily (--at HH:MM, on your local wall clock). An overdue task — server was down, laptop asleep — runs once on wake and reschedules from now; it never backfills a run per missed interval.

Workers AI as your provider

Cloudflare Workers AI is a normal provider in the model catalogue, and the OAuth login above is the easy way in — see Log in with Cloudflare for the whole integration: the live model sync, the R2 chat backup, the cf_ deploy tools, and the scopes.

The manual route still works if you would rather not connect an account: pick Cloudflare Workers AI in Settings, paste your account id and a Workers AI API token, and it behaves like any other BYOK provider. A server operator can also set NL_CF_ACCOUNT_ID + NL_WORKERS_AI_TOKEN to give every user on that instance a shared Workers AI backbone with no login at all.

The desktop — veil-desk

veil-desk is a native desktop dashboard (Zig + raylib, one window, no browser) that talks to the local server. It's where most people actually live: a chat pane with the Veil, a swarm board that shows every running hive round-by-round, and a build console.

  • Talk and it acts. The chat pane is a thin client over the server-side brain: your message runs one server turn, the answer streams back, and you can steer or stop it live. Ask it to build something and the turn casts a hive, watches it, and folds the deliverables back into the conversation — with an auto-loop tier (armed from the desk, driven server-side) that keeps a long build moving without you re-prompting each round. Goal mode: /goal <what to achieve>. The auto-loop with a goal it keeps. Type it in any chat - the desktop, the web app, veil chat - or run veil goal "<text>". Each iteration picks the best improvement not yet tried, does it, and measures it: a judge reads the step's real tool results, never the assistant's claim, and answers improved / same / regressed; when a count was printed (tests passed, checks green) the comparison is the engine's own arithmetic. Every iteration is logged, so the next pick sees what was tried and what it changed. The loop ends on its own when the goal is achieved, when its budget is spent (--budget N, 25 by default), or on a plateau: three iterations in a row that improved nothing. /goal shows where it stands; /goal stop, /goal resume, /goal budget N and /goal check <command> tune it; --forever removes the finish line. The AFK tier below is now exactly that - a forever goal - so it keeps its promise and gains the log and the measurement.

  • AFK: run until you stop it. A second tier for leaving the machine. It never asks whether the goal is complete (a session you opted into running indefinitely has no end state to ask about), the prompting model writes each re-drive from what the transcript shows was just built rather than reposting a canned sentence, it stays anchored to your original goal instead of to the loop's own re-arm text, and it re-grounds itself if a drive step comes back empty twice running. A transient provider error no longer ends a run you were promised only you could end, and the steering column stays reachable for the whole turn. The ordinary auto-loop tier is unchanged — its stop conditions are the tier.

  • You see everything. Live per-round fitness, the files each mind is writing, the tool calls, the shared-memory writes — swarm work is transparent, not a spinner.

  • Sub-chats. Branch a side thread off any conversation (up to five, one level deep) as tabs across the top. The family shares one workspace, so a side question doesn't lose the build.

  • Attach an image. Drop a screenshot or a picture into a message and it is turned into text and searchable regions server-side — so the model reads what's in it whether or not it has vision.

  • Manage the built-in model from the window. A panel shows the weights, a download button, a live progress bar, and one click to serve it.

  • It sits in the tray — and closing the window doesn't kill the server. A system-tray icon and native toasts on Windows; it lights up the moment the server has something to show. The desk and the server are one process, so the titlebar X hides the window and leaves everything running — the turn in flight, the chat workers, the web UI on :8787. Only File → Quit or tray → Quit ends the process. While hidden the loop skips input, layout and drawing entirely and idles at 5 Hz, so a parked desk costs nothing. (Windows only, and only when a live tray icon actually exists to bring the window back — Linux and macOS keep the old quit-on-close behaviour rather than hiding a window behind an icon that isn't there.)

The desktop is compiled into veil, so the released binary is the whole app — double-click and you're in, server and window together. Same from source:

zig build run                 # server + desktop, exactly like the release binary
zig build -Dapp=false         # server-only build: no GUI compiled in, no raylib/GL to link

-Dapp=false is what headless hosts and CI boxes want — the GUI is left out of the compilation entirely rather than merely unused. To keep the GUI compiled in but not opened for a given run, pass --server-only at runtime. (There is a zig build desk step that produces a standalone veil-desk binary; it is a development convenience, not something the release ships or the server spawns.)

It learns

The engine keeps a playbook: fixes it discovered by actually running commands on this machine. When a command fails and a later command in the same arc succeeds, that verified fail→fix transition is captured — never from the model's self-report, only from real exit codes — and recalled into future prompts, gated so only a lesson genuinely relevant to the current failure surfaces. The same fix stops being re-derived every session.

That's one thread of a broader self-improvement loop, all built on verified traces rather than the model's own claims:

  • A progress checkpoint, rebuilt every round from the run's own tool records — what completed, what's blocked, what's still pending — injected back into each mind's prompt with zero model calls.
  • A background review pass that reads the round's real tool trace out-of-band and mints trace-grounded lessons and skills into the live hive.
  • A smoke gate that boots the deliverable and probes it, because passing tests are not a working app — and a judge that grades the arc's ground-truth trace, not its self-narrative.
  • Strengthen-only plasticity: outcome feedback can reinforce a memory that keeps working, but it can never mint one from a reward alone — so a finished goal can't be resurrected by its own signal.
  • Lessons are graded on whether they helped. At the moment a failure resolves into a verified fix, the lesson that was recalled for that failure is reinforced — so one that keeps preceding fixes surfaces first and one that never does simply fades. It runs before the fresh mint, crediting prior knowledge rather than the fix just written, and it is engine-driven, so a mind cannot inflate its own.
  • Recurring successes become candidate procedure. The engine counts repeated successful tool sequences per mind — zero extra model calls, folded from records it already keeps — and proposes the frequent ones into a quarantine. Nothing auto-registers, auto-runs, or reaches a prompt; promotion is a reviewed step like every other proposal.
  • Review closes the loop across casts. Under a lineage, the end-of-run judge's lessons and skills and the mined habits wait in the lineage's quarantine until someone decides: veil lineage show <id>, then veil lineage accept|reject <id> <n>, or keep/drop on the card in the desktop's Memory pane. An accepted proposal goes into the live memory the next cast of that lineage recalls; a rejected one is remembered so the judge and the miner do not propose it again. The same habit no longer comes back every run as a new line.
  • What a lineage keeps is the task, not just the process. The judge also proposes facts: requirements a tool output actually stated (a checker's rule, a function's real signature), which a later cast reads whole as proven task facts. Measured first: with lessons alone, a lineage stored "read the file first" and never the two house rules its checker printed on every run. A proposal that restates one already known, or pastes tool JSON, is dropped before review. The desktop's Swarm → Lineages view shows each lineage's memory, what awaits review, and its casts over time (round score, rounds, tokens), from the history.jsonl every cast now appends to.
  • A round that breaks one check while fixing another is visible. The score is an aggregate, so that round used to read as "no change" to every loop watching it. The best-known-good build's failure set is now the champion and each later round the challenger, so a new break is reported even when the aggregate is flat or rising.
  • Self-edits are verified after the write, not just before it. Every gate around patch_system ran before the edit landed, so a change that left the source un-parseable stood until the next human build. A post-write zig ast-check (or the shared syntax gate for other languages) now reverts a bad write in place from a pre-image and reports it blocked. Authored tools are compiled the same way before they can enter the registry, instead of failing at their first call many rounds later.

The engine stays a fixed floor. What it learns lives in memory, never in an if branch.

Your AI's sub-agent — cast a hive from another assistant

The hive is also a side agent for whatever AI you already talk to. Any assistant that can run a shell command can hand off a research question or a side-build, keep working, and pull the progress and deliverables from the swarm's event stream instead of grinding through it in its own context window:

# start it and follow the stream to completion
veil cast "why does saving a note drop the last line? recommend a fix" --follow

# or start it, keep chatting, and tail it later
veil cast "research WASM memory models and write a brief"
veil events <id> --follow

Casts made this way run detached, stay bounded (--minutes / --minds), never post publicly, and are graded by the engine's own smoke gate and judge.

The MCP relationship runs the other way today. veil is an MCP client: mcp_discover reads the server configs already on your machine (Claude Desktop, Cursor, VS Code) and probes for local AI runtimes, and mcp_call runs their tools inside a veil turn — config scanning is read-only and never returns env values, which is where secrets live. Exposing the hive as an MCP server, so another client lists cast among its tools, is planned and not yet built: there is no veil mcp command, so drive it as a shell command for now.

How it works

Four pieces, each published on its own, that snap together:

  ┌─────────────────────────────────────────────────────────────┐
  │  the veil  (this repo)   the swarm engine + the Veil + CLI   │
  │     │                                                        │
  │     │  every turn: perceive → recall → act → imprint         │
  │     ▼                                                        │
  │  neuron-db               the hive's memory  (one `neuron` bin)│
  │     ▲                                                        │
  │     │  bulk-load clean facts; scouts fetch pre-cleaned docs  │
  │     │                                                        │
  │  nl-rag                  the knowledge substrate (doc packs) │
  │                                                              │
  │  the-veil-12b            the model, served in-process        │
  └─────────────────────────────────────────────────────────────┘

the veil — the engine. Each mind runs a moment loop: it perceives the goal and live state, recalls what the hive knows, acts (write a file, search, run tests, edit, message a peer), and imprints what it learned back into memory. Periodically the hive is integrated into the Veil — a single self (I am / I know / I have / my will) that persists across runs, answers you, and folds your intent into the next move. The engine is a fixed floor; behaviour emerges from live signals (the mode, the plan, the strategy), never from hardcoded use cases.

neuron-db — the memory. A small associative store compiled to one neuron binary the engine calls for every recall and observe. It's what makes the loop an oscillation: perception becomes graph, reasoning becomes graph traversal, and each prompt is rebuilt from a trust-weighted recall instead of carried as flat text — so a small local model gets a large-model floor to stand on, cheaper in tokens and richer in context. Fetched and built for you on first run (needs Rust); reused after that. Its semantic cortex is published separately as gary-neuron-emergent.

It resolves paraphrases. Recall returning nothing reads to a model as an authoritative no and overrides what is sitting in its own context, so an empty spreading-activation pass no longer ends the lookup: it falls through to a semantic pass with a topic gate, which can match a question sharing no words with the stored fact. Released bundles build neuron with that tier compiled in. Measured through the chat stack on isolated conversations with identical seeds and phrasing, grounded answers went 0/3 → 2/3 with one partial — turn-start recall now injects bridges like "machine that routes customer orders" → "the dispatch gateway listens on port 7141" that a lexical lookup never surfaced.

nl-rag — the knowledge. A curated repository of canonical documentation (languages, web, databases, security, ops…) pulled and normalized into clean, frontmattered markdown packs, each with a pack.facts file. The veil's compiled-in source atlas points scouts at these packs first, so a small model reads pre-cleaned markdown instead of fighting HTML — and you can bulk-load a pack straight into hive memory:

neuron import packs/rust/pack.facts --scope knowledge     # from a cloned nl-rag

the-veil-12b — the model. Published as GGUF and served by the server itself, so the fourth piece needs no runtime of its own: veil model pull and the app is self-contained. See the built-in model. Any other OpenAI-compatible endpoint still works exactly as before — this is the floor, not a lock-in.

Optionally, hyperspace grounding (NL_HYPERSPACE=1) settles the most relevant memory into a warm in-RAM field before each call, so a typical round does zero database subprocess calls for grounding. It's bounded (~45 KB/mind) and scales down to an IoT profile (NL_HYPERSPACE_CAP=48).

What you can do

The same binary covers very different jobs — the mode emerges from your words:

  • Build software, graded by its own tests. The hive plans a file tree, splits the work, writes the project, runs the tests it wrote, and folds the results into its fitness — it keeps going until the deliverable actually passes. veil cast "Build a URL shortener with a REST API and tests" --follow
  • Research & brief. In --style discourse the minds research the live web, form and argue their own views, and co-write a briefing. veil cast "Brief me on fusion power in 2026" --style discourse
  • Operate a live device. Point the Veil at a device that drops telemetry on a file bus and it acts to keep it healthy, graded by an acceptance oracle — the device's own measured health, which the Veil never sees and can't write, so only a real fix moves the number. The shipped example is a self-healing security daemon (detect → remove persistence → block C2 → verify); swap the corpus and telemetry, not the code, for any device. See examples/embedded/. veil deploy "Keep this device healthy"
  • Offline knowledge appliance. --offline removes every web tool; a corpus in the run's manifest preloads knowledge (an nl-rag pack, say) into memory. A sealed appliance that reasons only over what you chose. veil cast "Answer only from what I preload" --offline
  • Built-in knowledge base (local corpus mirror). Point NL_RAG_DIR at a clone of nl-rag — or run veil rag sync --from <clone> to copy a tier into the app's data dir (veil rag status shows what's active) — and every pack fetch serves from disk: engine prefetch, scout page reads, deep crawls, all offline-capable. The corpus manifest also EXTENDS the compiled source atlas at runtime (thousands of extra curated domains become routable), with machine-grown packs excluded by default. A clone dropped at vendor/nl-rag before build works too — the app carries its knowledge with it.

Configuration

Each run has a swarm.json manifest — the web UI and the desktop write it for you, or hand-write it:

field meaning
provider / model / base_url the LLM endpoint (any OpenAI-compatible API)
minds the roster of named minds
minutes auto-stop after N minutes (0 = until stopped)
style auto · build · discourse · investigate · debate
internet false runs fully offline
corpus / corpus_cap a .facts/.jsonl pack to preload, and how many facts
bootstrap false disables the engine-run dependency installs of the deliverable's own manifests (npm/pip/cargo/go)
lineage a stable id (veil cast … --lineage my-proj) that persists the swarm's neuron-db across re-casts, so knowledge, playbook, skills, and learned trust compound run over run instead of resetting — a cast that gets better over time
gateway_model optional cheaper/smaller model for mechanical calls (and the chat's fast voice)

Endpoint resolution, everywhere: a run's own swarm.json › NL_LLM_* env › ~/.veil/config.json › local defaults. Secrets never go in swarm.json — use NL_LLM_KEY in the environment or a gitignored keys.env in the run dir.

A note on local-model latency. On a single-GPU box, a running hive and the chat share one model queue, so the Veil's voice can wait behind a generation. Fixes, best first: set OLLAMA_NUM_PARALLEL=2 (share the loaded model), give the chat a tiny side model via the manifest's gateway_model, or use a hosted endpoint — which answers the hive and the chat concurrently and sidesteps the whole issue.

Web control plane

The same binary serves the web app: chat with the veil, watch swarms, run scheduled tasks, browse what a turn built, and manage accounts — the desktop's surfaces, in a browser, on any device on your network. It comes up whenever the server runs:

zig build && ./zig-out/bin/veil

It binds every interface unless you say otherwise, and prints the URLs it is reachable at (see step 3). The admin password for a first run is in data/admin-password.txt — the full first-login sequence is step 4.

The shared provider key

A default model nobody can afford to call is not a default. Admin → provider key stores one instance-wide key (POST /api/v1/admin/keys), sealed in the same vault as everyone else's under a reserved uid that no account can hold (SERVER_KEY_UID = 0, src/worker/chat/service.zig:31).

It is the last resort in the resolution ladder — an explicitly-supplied key wins, then the user's own vaulted key, then this one — so an account that brings its own billing is never silently switched onto yours (src/worker/chat/service.zig, resolveRole).

The trade is deliberate and worth stating: once this is set, every user's turns spend the admin's credit. That is exactly what a LAN or family install wants — nobody should have to hold an API key to use the thing — and exactly what a wider deployment has to think about first. Without it, each new account must configure its own key in Settings before it can chat at all.

Everyone else is sandboxed

Set the default model in the Admin tab. Pick it from the same catalog the Settings tab uses; it applies immediately, with no restart, to everyone who has not chosen their own. The API key is never part of it. Without a default, a brand-new account has to configure a model before it can chat at all.

The web app is multi-user, and a normal account is not trusted with the host. Its turns run a restricted tool surface: the conversation's own workspace, research, and the entire hive-memory surface — but no code execution, no host commands, no engine self-modification, no tool authoring, no browser or MCP drive, no casting swarms or scheduling runs (both execute outside the sandbox), and no cf_ tool belt (deploying to a cloud account is outside it too). The admin account keeps the full surface. Files were always confined to the conversation's workdir; what changed is that the dangerous verbs are now refused inside a turn, not just on the tool endpoint.

variable what it does
NL_BIND the listen address. Unset = every interface, which is the default and is what makes the phone-in-the-next-room case work. 127.0.0.1 (or localhost) pins it to this machine
NL_PORT the port (default 8787). Resolved once and shared by the server bind and the CLI client
NL_ADMIN_EMAIL / NL_ADMIN_PASSWORD the admin account. Defaults to admin@neuron-loops.local with a password generated on first run and written to data/admin-password.txt; set NL_ADMIN_PASSWORD to pin your own and no file is written
NL_MAX_TURNS how many chat turns run at once, server-wide (default 64, ceiling 256). Size it to the rate limit of the provider key everyone shares
NL_MAX_TURNS_PER_USER how many of those one account may hold (default: an eighth of capacity). This is what stops one busy user starving everyone else
NL_TURN_TOKEN_CEILING input tokens one turn segment may spend before the engine compacts its working context and continues (default 400000; 0 disables the ceiling). A runaway backstop, not a budget — a thorough research turn lands well under it
NL_TURN_CONTINUE_MAX how many times one turn may compact and keep going (default 3). 0 restores beta-5's behaviour exactly: the turn settles at the ceiling and waits for you
NL_TURN_CONTINUE_HARD_MAX the ceiling on the earned allowance (default 10). A segment that wrote a file or made a call it had not made before buys one more pass, up to this; a segment that only re-read its own context buys nothing, so a spinning turn still settles at NL_TURN_CONTINUE_MAX
NL_TOOL_MAP experimental — advertise the belt as a MAP the model walks (one open_tools verb, seven branches at a time) instead of a flat list. A small model keeps nine core verbs in hand and walks the rest; a large one walks only the discovered families (security tools, Cloudflare, plugins), the ones that grow without bound. A known tool name is always callable and opens its own branch. Default off (unset or 0); 1 turns it on. See worker/chat/belt in the docs
NL_HANDOFF how the CONTINUATION STATE is written when a turn compacts and continues: net (default) unfolds the turn's own working span with Gary's 4tope — the frontier's last words, the file ledger, the failed calls, the results the frontier rests on, verbatim, no model call; model restores the model-written four-label state
NL_KEEPALIVE_REQUESTS requests one connection serves before recycling (default 200). Drop to 1 if you hit a stuck-socket worker thread
NL_DEFAULT_MODEL / NL_DEFAULT_BASE_URL seeds the default model on a fresh install, for unattended provisioning. Afterwards set it in Admin → Default model — that persists to data/server-config.json and wins over the environment, so a stale launch script cannot undo the admin on the next restart
NL_OPEN_REGISTRATION let people sign themselves up. Off by default; the admin creates accounts from the Admin tab instead
NL_BROWSER_DRIVER / NL_MCP give sandboxed users the browser / local MCP servers too. Admins already have both; leave these unset unless you mean it — the browser inherits this machine's network position and its profile's cookies

Provider keys are entered per model in Settings and sealed server-side per account. They are never stored in the browser, and the server never sends one back — only a last-four and a fingerprint.

The fleet hub — many swarms, one console

The web control plane watches one box, one swarm at a time. veil hub is a fleet console over the same running server: because the server already aggregates every swarm you own, the console is API-backed and honest — no second, out-of-band control plane.

veil hub                     # roster: fleet summary + every swarm's state
veil hub all "<text>"        # broadcast a directive (say) to EVERY running swarm
veil hub goal "<text>"       # set a new goal on every swarm
veil hub stopall             # fleet-wide kill switch (a safety stop for autonomous minds)

Cross-machine aggregation — many machines meshed into one roster, the way the old hub.py worked — is a planned server endpoint, intentionally left as a documented follow-up rather than a second control plane. Today the console operates the local server's fleet.

Project layout

scripts/                   install.sh / install.ps1 (the from-source installers), veil / veil.cmd (the
                           `veil` front-door shim → the compiled binary), build-official.sh and
                           build-release.* (the release builders), check.ps1 / check.sh (the acceptance
                           oracle), bump-version.ps1 (every version stamp at once), sync-models.py
build.zig                  the Zig build (server + CLI + desktop; -Dapp=false = server-only)
src/
  main.zig                 entry point: CLI dispatch, then the server + control plane (auth, routes)
  cli.zig                  the `veil` CLI — a thin client over the server's /api/v1/*
  cli/{chat,hub,tot}.zig   the interactive chat REPL, the fleet console, and `veil --tater`
  gateway/http.zig         the HTTP surface: App context, the auth guard, JSON/file helpers
  auth/  config/  admin/   accounts + API keys, the encrypted key vault, the admin API
    config/lan.zig         which addresses this machine is reachable at (the startup banner's URLs)
    config/server_config.zig  admin-owned runtime settings → data/server-config.json
    config/cf_{oauth,r2,tunnel}.zig  Log in with Cloudflare, the R2 chat backup, the public-URL tunnel
    config/cf_tot.zig      tots: uploads the runtime into the user's account, relays to it, and runs
                           the jobs a tot may send to the owner's machine
  worker/                  the hive and the server-side brain:
    chat/{engine,service,tools,context,overlay,workspace,plan,sync,toolperf,paths}.zig  the chat
                                                   brain — the agentic turn loop, its REST handlers,
                                                   tools, context window and facts ledger, the recall
                                                   overlay, the prompt workspace, plan board, client
                                                   file-sync, tool timings, and conversation paths
    sched.zig              scheduled tasks (each run is a server chat conversation)
    continuity.zig         resume anchors — what a cut unit of work established, banked in
                                                   neuron-db so the next run continues it
    control/{supervisor,writer,fanout}.zig  swarm processes, the control bus, event streaming
    deploy/service.zig     the cast/deploy door + swarm files, bundle, archive, lifecycle
    neuron/client.zig      the neuron-db memory bridge (fail-open)
    builtin.zig  builtin_endpoint.zig  llamaeng.zig  llamashim.c  modelpull.zig  the BUILT-IN model:
                                                   the-veil-12b served by the server itself from a
                                                   downloaded GGUF (-Dbuiltin, default true) — store,
                                                   loopback engine endpoint, embedded inference, and
                                                   the verified weights downloader
    gemma4.zig             the Gemma-4 wire format, rendered here instead of by the local runtime
    browser/               the browser driver: its own profile (launch, cdp) or YOUR Chrome/Edge
                           through the extension (ext, ext_api), behind one session model + the
                           per-machine local-host daemon that keeps a session alive across calls
    mcp/{client,discovery}.zig  find and call the MCP servers already installed on this machine
    run.zig  agi.zig  oscillation.zig  rsi.zig  vcs.zig  tools.zig  writer.zig …  the moment loop,
                                                   the Veil, the self-improvement faculties, the
                                                   micro-VCS for concurrent minds
    locs/atlas.zig         the source atlas — points scouts at nl-rag packs
cloud/tot.js               the tot runtime - one Worker, one Durable Object per tot - embedded into
                           the binary and uploaded as it is; cloud/tot.test.mjs runs it under node
cloud/tot_py.py            the Python a tot runs (a second Worker), with requests, pip and packages
cloud/neuron_core.wasm  neuron-db.mjs   the tot's memory engine and its binding
desk/                      veil-desk, the native desktop dashboard — compiled INTO `veil` as the
                           "desk" module (-Dapp, default true), not a separate shipped binary
docs/                      the docs site: architecture map + annotated source (static, home-built
                           md parser, the desk's own palette from desk/src/theme.zig)
extension/                 the Chrome/Edge extension — a thin CDP relay so the veil can drive the
                           browser you are already signed into (load unpacked; nothing to build)
examples/embedded/         the device-operator worked example
harness/TESTING.md         the house test rules — read before adding a test
web/public/                the control-plane UI — index.html, app.js, styles.css, models.json,
                           embedded into the binary at build time (no bundler, no build step)
models.yaml                the ONE model catalog → generated into web/public/models.json; newer
                           releases arrive as a daily PR (scripts/sync-models.py, models-sync.yml)
bin/neuron[.exe]           the neuron-db memory engine (bundled / built on first run)

Build knobs worth knowing: -Dapp=false (no desktop GUI), -Dbuiltin=false (no embedded inference), -Dvulkan=false (CPU-only built-in engine, ~50 MB smaller binary). Each one skips fetching its dependency entirely rather than compiling it unused.

Release

Current: v1.1.16 — Gary's 4tope: a long chat carries itself forward by graph instead of by summary, keeping the chain of results the current step rests on and writing the continuation from the turn's own text with no model call. Plus the opt-in tool tree that lets a small model walk a belt of hundreds of tools, a 60% lighter Agent Garrett belt, and a spend ceiling that scales with the belt. Full notes.

Previously: v1.1.15 — tater-tots plan correctly with the complete Agent Garrett catalogue enabled: the planner receives the full tool index and execution loads each tool's typed schema on demand.

Previously: v1.1.12 — the complete 166-tool Agent Garrett catalogue is available through typed MCP tools in chats, swarms and tater-tots. Deployment includes a dedicated Cloudflare execution runtime, Settings shows its build progress, feature opt-ins are separate, and the desktop has corrected spacing and panel borders. Full notes · Security tools guide.

Previously: v1.1.11 — chat keeps answering when a hosted model changes how it treats a request: a retired thinking-off setting stays retired, a cut turn's continuation state is read from the reasoning channel, the desktop stops dialing a placeholder endpoint and folds a message into a running turn, and the survey and planning passes show their reasoning in the chat. Full notes.

Previously: v1.1.10 — tot deployment reconnects stale runtimes, run history is easier to read, and saved runs can be removed from the list without deleting their files.

Previously: v1.1.9 — each tot deployment has its own console. Past runs retain events and deployment errors, and redeploying the same name starts fresh.

Previously: v1.1.8 — tater-tots can edit and deploy their shared runtime and work through browser challenges. Admin credentials are reconciled and read back from storage before a listener or Tunnel can expose the account. Full notes.

Previously: v1.1.7 — tater-tots: the goal loop running in your own Cloudflare account with nobody in it. The desktop's new Tater-tots tab (or veil --tater deploy "<goal>") uploads one small Worker into your account; each tater-tot wakes on a timer, makes one measured improvement toward its goal, writes itself lessons from what the measurement said, and moves on to the next best thing when a goal ends. 24 per account by default and more if your setup carries them, the first always named Gary; they share a scratchpad, can cast their own inner swarms, and may use your machine only if you check the box at deployment. Each run is mirrored into a folder on your machine you can open and tail, the model list is your account's own, and deleting the last tater-tot removes its Worker. And goal mode: /goal <text> in any chat gives the auto-loop a stored goal, an iteration log, a measured outcome per step and its own stop rules. Full notes.

Watch the tater-tots trailer (2:20, sound on): the Dots turn on us, and only a side dish can stop them.

TATER-TOTS vs DOTS - the trailer

Play it in the browser · download it · every frame and sound is drawn by docs/video/render (python docs/video/render/render.py builds it again).

Updating: from v1.1.3 or later, use Settings → Updates → App updates → Update & restart. Versions before v1.1.3 require a manual full-bundle installation. Updates verify both executables and preserve data, with backups for recovery. Updating and network troubleshooting.

Previously: v1.1.6 — veil --swarm "<goal>" casts a swarm and opens a terminal view of it: every mind's step on the right, one chat line into the whole swarm on the left, running to completion on its own. Lineages now keep facts a tool output proved (the checker's rules, a real signature) beside their lessons, every cast is on record in the lineage's history, and the desktop's Swarm → Lineages view shows each lineage's memory and its casts over time. The CLI reads the desktop's settings (host, port, chat model), and veil --configure connects a machine that has no desktop.

Before it: v1.1.5 — what a swarm lineage learns can reach its next cast: the judge's lessons and skills and the mined habits are reviewed with veil lineage or keep/drop cards in the Memory pane; a turn whose first reply comes back empty is asked again with its tools; the desktop key survives a server that could not read its key store.

Before it: v1.1.4 — a calmer desktop with visible preparation and planning updates, five organized settings pages, editable starter prompts, steadier streaming, and cleaner code blocks, plus a fix for an HTTP completion race that could report a false timeout. Desktop guide.

Before it: v1.1.3 introduced verified desktop updates, confirmed Cloudflare tunnel readiness, and native release smoke checks on all four platforms.

Before it: v1.1.2 — a point release about secrets on disk, and about work that reaches you. No key becomes a file: every build before this one wrote each model call's Authorization: Bearer <key> into a curl config beside your conversation and left it there — 111 of them were still on the machine this was found on, the oldest from July, in a folder that syncs to the cloud. Curl now takes its config over a pipe, on the server and in the desktop, for model keys and for the Cloudflare bearer; a key that cannot be put in a config line is refused instead of escaping it; GitHub tokens are per call and swept; and git children never ask a terminal for a credential, so a push that cannot authenticate fails in milliseconds instead of blocking the chat thread. If you used a long-lived key with an earlier build, rotate it. Work that reaches you: a cast fired in a sub-chat or a scheduled task shows its run and delivers its files, a conversation's second cast delivers as well as its first, a crashed worker's relaunch is waited for rather than reported done, and retention no longer deletes your conversations — it walked for event files recursively and took _chat/convs/ with it after 14 idle days. Calls that cannot finish stop waiting: same-tag calls in one folder no longer overwrite each other's request bodies and answer the wrong question, and a transfer whose curl died falls back at once instead of waiting out four to fifteen minutes. Everything v1.1.1 listed as found while writing these notes is fixed, the cf_ tool belt included — it works from the desktop and veil chat now. Full notes.

Before it: v1.1.1 — a point release about memory and staying power. A chat without end: every fold of the rolling summary also banks the facts it established in an append-only ledger that each turn projects back for the live question, and the summary, the facts and the verbatim window are sized to the models that read and write them — where every fold used to replace the summary with a fresh 250-word rewrite and lose whatever the rewrite left out. The recall overlay: memory is settled around what the model is doing before every round of a turn, not only at its start, and offered as an advisory block that never enters the transcript; a line the model ignores rotates out, a line it uses is strengthened in the store. Memory you can see: a REMEMBER: / FORGET: the veil applies is recorded as an event rather than re-driven as a step, and the desktop's Memory tab reads the per-user store the server actually writes and refreshes the moment it changes. Staying power: a failed model call in a chat turn is retried ten times over about five minutes, with the Cloudflare token re-resolved between tries and Stop ending the wait, and Windows no longer disowns the live desk window as Not Responding. The model menus now follow a daily, keyless models.dev sync. v1.1.1 notes.

Before that: v1.1.0 — the first stable release. Everything before it — v1.0.0-alpha.1 through v1.0.1-beta-7 — went out under a prerelease label; it was the first build published without one. Three things earned it. A public URL: one switch and the veil is reachable at a Cloudflare address — confidential by default, a random trycloudflare.com hostname that changes on every start and puts nothing on a domain you own, with your own domain behind a Cloudflare Access policy as the explicit alternative; the address is revealed only once Cloudflare's own resolver has published it, because a lookup made before the name exists leaves it unresolvable on that network for half an hour. Two ways the process could stop answering, both gone: the desk's worker loops no longer sleep on the Io runtime's thread alert — one bit shared by everything else on the thread, which froze the window mid-turn while the server was healthy — and in the server one keep-alive client no longer pins an httpz worker and stalls the next socket behind it, where about one new connection in twelve had been waiting 12.5–13 s for its first byte. And the turn loop got stricter with itself: a claim of work with no tool call behind it is not done, a figure that came from nowhere is read off disk before it is stated, a reply cut at the output cap is continued instead of guessed at, and the facts a long turn paid for survive the compaction that used to discard them — each one found by an unattended run graded against what was actually on disk, not by reading code. Log in with Cloudflare shipped in beta-7 and the tunnel is built on it; both are documented in full above. v1.1.0 notes · beta-7 — the veil gets a cloud · beta-6 — a long turn continues instead of starting over.

Builds ship on the Releases page, one per platform. Unzip and run veil — that starts the server and opens the desktop app. No toolchain and no first-run build, and the memory engine ships inside; the one thing it does not bring is the Python the agent's tool belt shells out to (see What this actually is).

asset what it is
veil-…-windows-x86_64.zip the app — Windows
veil-…-macos-arm64.zip / …-macos-x86_64.zip the app — Apple Silicon / Intel
veil-…-linux-x86_64.zip the app — Linux
veil-server-…-<os>-<arch> server only, no desktop — headless hosts, containers, remote boxes
SHA256SUMS-*.txt checksums

Desktop builds are produced natively per-OS (the GUI links the platform graphics stack and can't be cross-compiled); the standalone server cross-compiles to every target from one runner.

To cut your own, the official builder wraps the whole thing and bootstraps its own toolchain — a missing Zig, Rust, C compiler, or (on Linux) raylib dev library is fetched for you first:

sh scripts/build-official.sh              # everything for this host, plus cross-built servers → bin/
sh scripts/build-official.sh --host-only  # skip the cross-compiled server matrix

It stages into a private prefix, so it never disturbs zig-out — you can cut a release while the app is running. NO_BOOTSTRAP=1 skips the dependency step (supply ZIG= / NEURON= yourself). veil doctor prints server + token health; veil doctor --runtime adds what the app has actually been doing — learned tool behavior, schedule fail-streaks, per-model token and latency usage — and reads the on-disk ledgers directly, so it works with the server down.

Related projects: neuron-db (the memory engine) · nl-rag (the knowledge packs) · the-veil-12b (the built-in model) · gary-neuron-emergent (neuron-db's semantic cortex).

License

NL-Veil code is licensed under MIT. The Gary security runtime includes modified ARTEX code under AGPL-3.0. Its attribution, corresponding source, and Cloudflare build sources are available in cloud/ and included in the full release bundles. Dependencies retain their own licenses.

来源:GitHub · github.com