From 0674e550f1721435a8760a76d5256855bac56cd7 Mon Sep 17 00:00:00 2001 From: Luxit Date: Mon, 24 Aug 2026 13:02:29 +0200 Subject: [PATCH] Sync the self-hosting stack --- .env.example | 202 +++++++++++++++++++++-------- CHANGELOG.md | 313 +++++++++++++++++++++++++++++++++++++++++++++ README.md | 275 ++++++++++++++++++++++++++++++++++----- docker-compose.yml | 136 +++++++++++++++++--- 4 files changed, 818 insertions(+), 108 deletions(-) diff --git a/.env.example b/.env.example index f72531f..9925ed8 100644 --- a/.env.example +++ b/.env.example @@ -1,81 +1,146 @@ -# Jarvis — configuration. Copy to `.env` beside the compose file and fill in. +# Jarvis — optional configuration. # -# cp .env.example .env +# THERE IS NOTHING YOU HAVE TO FILL IN. You do not need this file at all: # -# Nothing here has a safe placeholder value: the four secrets must be generated, and the two URLs -# must be yours. The compose file refuses to start rather than booting with something wrong. +# docker compose up -d +# +# then open the address in a browser and the install screen asks the questions. It creates the first +# administrator, checks the deployment, and stores the address, the model and everything else as +# settings you can change later from a screen. +# +# THIS FILE USED TO BE MANDATORY, and that is the change. It asked for six values before the +# container would start, four of them secrets to be generated with `openssl` — and it failed in the +# worst available way, because an invalid one made the API exit 1 and the restart policy turned a +# typo into a crash loop, with the reason on line 40 of a log nobody had a reason to open. +# +# What is left below is for deployments that prefer to state things in writing: pinning image +# versions, running the agent overlay, choosing a host port. Every value here is optional, and the +# ones that overlap with the install screen SEED it — they are read once, when nothing is stored +# yet, and never again. Change them afterwards and nothing happens; change the setting instead. +# +# cp .env.example .env # only if you want any of this # --------------------------------------------------------------------------- # Where your instance lives # --------------------------------------------------------------------------- -# The address a browser reaches Jarvis on, scheme included and no trailing slash. -# -# This is the CORS decision. It accepts exactly ONE origin — not a list — and every request from -# anywhere else is refused, which presents as a sign-in page that cannot sign in. -WEB_ORIGIN=https://jarvis.example.com - -# The address enrolled machines dial. Defaults to WEB_ORIGIN, which is right for a single-origin -# deployment. Set it separately only if the app and the API answer on different hostnames. -#PUBLIC_URL=https://jarvis.example.com - # Host port the web container publishes. Put your TLS terminator in front of it. +# +# The one value on this page that genuinely cannot move to a screen: it is a Docker fact, decided +# before anything in the application is running. JARVIS_PORT=8080 +# The address a browser reaches Jarvis on. ASKED BY THE INSTALL SCREEN, which pre-fills it with the +# address you are already reading it at — so setting it here is only useful if you want it stated in +# writing, or if you are upgrading a stack that already had it. +# +# It decides CORS, what enrolled machines dial, the base of every invitation link, and the WebAuthn +# relying party. Changing it later orphans every passkey already enrolled, so the screen warns you. +#WEB_ORIGIN=https://jarvis.example.com + +# PUBLIC_URL IS GONE. It was a second address for the same thing, kept separate for a split-hostname +# deployment nobody ran; the install screen asks once and every consumer reads that one value. An +# instance that still sets it is not broken — it is simply ignored. + # How many proxies rewrite X-Forwarded-For before a request reaches the API. # -# One is the web container's own nginx, which is always there. So: 1 if nothing else fronts it, 2 if -# your own reverse proxy does — which is the usual case and the default. Raising it is the dangerous -# direction, because the API then trusts that many hops of a header the client can forge, and a caller -# can choose the address that lands in the audit log and in the session list. -TRUST_PROXY_HOPS=2 - -# --------------------------------------------------------------------------- -# Secrets — generate every one of these, never copy them from anywhere -# --------------------------------------------------------------------------- - -# Postgres. Only ever used inside the compose network. -# openssl rand -base64 24 -POSTGRES_PASSWORD= - -# Session signing. Two different values, at least 16 characters each. -# openssl rand -base64 48 -JWT_ACCESS_SECRET= -JWT_REFRESH_SECRET= - -# THE ONE YOU CANNOT LOSE. +# ASKED BY THE INSTALL SCREEN, which is the only place this can honestly be answered: it shows you +# the header chain your own request actually carried and the address the API resolved from it, and +# you confirm what you see. Nobody knows this number in advance. # -# Must decode to exactly 32 bytes: -# openssl rand -base64 32 -# -# Every credential in the vault — SSH keys, API secrets, Microsoft 365 client secrets — is encrypted -# under this key with AES-256-GCM, and so is everything Jarvis seals OUTSIDE the vault: the Microsoft -# Graph client secret it sends its own mail with, this instance's licence identity key, every -# authenticator-app secret, and every terminal recording. It is not stored anywhere but here. -# -# Change it or lose it and none of that data can be read again, by you or by anyone. A database backup -# does not save you: the backup holds the ciphertext. Back this value up separately from the database, -# somewhere you would still have it if this host were gone. An instance whose key has changed keeps -# LOOKING configured — the rows are all there — and fails on every reveal. -VAULT_MASTER_KEY= +# One is the web container's own nginx, which is always there; two if your own reverse proxy fronts +# it, which is the usual case. Raising it past the real number is the dangerous direction, because +# the API then trusts that many hops of a header the client can forge — and a caller can choose the +# address that lands in your audit log and in the session list. +#TRUST_PROXY_HOPS=2 # --------------------------------------------------------------------------- -# The model +# Secrets — there is nothing to fill in here any more # --------------------------------------------------------------------------- +# +# This section used to hold four values and four `openssl rand` invocations. They are generated for +# you now, on your first `docker compose up`, by the `init` service — into the `jarvis_secrets` volume, +# where the database password, the two session-signing secrets and the vault master key live. +# +# WHY THEY LEFT THIS FILE. A vault key is not a preference. Asking for one put the single most +# consequential value in the deployment — everything in the vault is encrypted under it — in front of +# whoever was least equipped to look after it, at the moment they were least interested in it, in a +# file they were trying to get through. Nobody ever chose a better key than `openssl rand` would have. +# +# WHAT YOU STILL HAVE TO DO, AND IT IS THE IMPORTANT ONE: +# +# BACK UP THE `jarvis_secrets` VOLUME, SEPARATELY FROM THE DATABASE. +# +# The vault key is in there and NOWHERE ELSE. A database backup does not save you — the backup holds +# the ciphertext. An instance whose key is gone keeps LOOKING configured, with every row in place, +# and fails on every reveal. It is a separate volume from `postgres_data` precisely so that the two +# can be, and must be, backed up to different places: a dump that travelled with the key that opens +# it is a dump that opens itself. +# +# The setup screen shows you the key once, on your first visit, and will not let you finish until you +# have put it somewhere. That is the moment to do this. +# +# UPGRADING FROM A .env THAT ALREADY HAS THESE? Leave them exactly where they are. The generator +# ADOPTS an existing value rather than replacing it, and never overwrites a secret it has already +# written — so your instance keeps its own keys and nothing about your vault changes. You can delete +# them from this file once the stack has come up once, or leave them; they are read only when the +# corresponding file does not exist yet. +# +# Pointing at secrets of your own instead? Every one of them also accepts a `_FILE` variable +# (VAULT_MASTER_KEY_FILE, JWT_ACCESS_SECRET_FILE, …), which is the convention the postgres image and +# most others already use — so a `docker secret` can be mounted at those paths with nothing else +# changing. + +# --------------------------------------------------------------------------- +# The model — asked by the install screen, with a test button +# --------------------------------------------------------------------------- +# +# All four moved. The install screen asks for the endpoint and the key, lists the models the endpoint +# actually serves, and ASKS THE MODEL FOR A TOKEN before it saves anything — so a key that is +# well-formed and wrong, an account out of credit or a model id the provider does not serve is a +# sentence on screen instead of a fault in somebody's first conversation. +# +# The key is stored encrypted under this instance's vault key, exactly like the outbound mail secret, +# and is never shown again. Change any of it later under Settings -> Platform -> Model. +# +# Setting them here still works and seeds an instance that has nothing stored — for a deployment +# that would rather state its model in a file. They are read once and never again. -# Any OpenAI-compatible endpoint: OpenAI, a self-hosted gateway, a local server. #OPENAI_BASE_URL=https://api.openai.com/v1 - -# Required. Jarvis will not start without it. -OPENAI_API_KEY= - -# Must be a model your endpoint actually serves, and it should be a good one — this model is deciding -# what to run on production infrastructure. +#OPENAI_API_KEY= #OPENAI_MODEL=gpt-4o - # minimal | low | medium | high. Higher costs latency and tokens and is worth it for real work. #OPENAI_THINKING_LEVEL=medium +# --------------------------------------------------------------------------- +# Unattended installation (for fleets and for CI) +# --------------------------------------------------------------------------- +# +# Everything above is optional because the install screen asks for it. This is the other direction: +# somebody rolling out fifty instances from a template has answered those questions once already, +# and making them answer each one in a browser is exactly what this product stopped doing to people. +# +# Set this and the instance installs itself at boot from whatever the variables above provide, marks +# itself installed, and never shows the wizard. +# +#JARVIS_UNATTENDED=on +# +# The first administrator. REQUIRED when JARVIS_UNATTENDED is on and the database is empty; the +# password must be at least 12 characters. +# +# If they are missing or too short the instance says so loudly in its logs and leaves setup +# OUTSTANDING rather than completing — an instance marked installed with no account is one nobody +# can ever get into, and the claim window is gated on setup being unfinished, so there would be no +# way back. Falling through to the ordinary install screen is strictly better than that. +# +#JARVIS_ADMIN_EMAIL= +#JARVIS_ADMIN_PASSWORD= +#JARVIS_ADMIN_NAME=Administrator +# +# Worth setting alongside them, since nobody will be there to be asked: WEB_ORIGIN, OPENAI_API_KEY +# and OPENAI_MODEL above, and JARVIS_LICENSE_KEY below. Each is optional even here — an unattended +# instance with no model comes up and reports that it has none, rather than refusing to start. + # --------------------------------------------------------------------------- # Channels and pinning (optional, recommended in production) # --------------------------------------------------------------------------- @@ -137,6 +202,22 @@ OPENAI_API_KEY= #AGENT_HEARTBEAT_INTERVAL_SEC=30 #RUN_SHUTDOWN_GRACE_SEC=25 +# How long a sign-in lasts, in seconds. Both were fixed in the compose file until now, which made +# them the only tuning values on this page you could read about and not change. +# +# JWT_ACCESS_TTL is how long an access token stays valid — and therefore how long a revoked session +# keeps working before it notices. Lower it if that window matters to you; the cost is a refresh +# round trip more often. +# +# JWT_REFRESH_TTL IS AN IDLE TIMEOUT, NOT A SESSION LIFETIME, and the difference will matter to you +# if you are here because of a policy. The refresh token rotates on every use and the new one starts +# its full term from that moment, so nothing anywhere measures how old a sign-in is: somebody who +# keeps a tab open stays signed in indefinitely. What this value really sets is how long a session +# survives being left alone. Fourteen days is the default. To end sessions by age rather than by +# idleness, revoke them — Settings → Security lists them and kills them individually. +#JWT_ACCESS_TTL=900 +#JWT_REFRESH_TTL=1209600 + # --------------------------------------------------------------------------- # Licence (optional — without one you get the community edition) # --------------------------------------------------------------------------- @@ -167,4 +248,13 @@ OPENAI_API_KEY= # # A LICENSED instance reports regardless — that is part of what a licence is, and this variable does # not change it. +# NOTE, 24 August 2026: this line did nothing before that date, and we are sorry. Compose reads +# this file to fill in placeholders INSIDE the compose file — it does not hand it to the services — +# and the api service never listed this variable, so the API saw nothing and applied its default of +# "on". An instance that had opted out reported anyway. Pull the repository again and +# `docker compose up -d`: the fix is in the compose file, not in the images. +# +# Do not trust this file to tell you whether it took. Ask the instance: +# docker compose exec api printenv JARVIS_TELEMETRY +# Settings -> Licence answers the same question from what the API actually resolved. #JARVIS_TELEMETRY=on diff --git a/CHANGELOG.md b/CHANGELOG.md index 70aea79..5fb4c80 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,319 @@ one of them changed. An entry says which artefact and which version, so `web 0.1 Every version published to a channel appears here. Nothing is written from memory afterwards: a build cannot be published without its entry. +## 2026-08-24 — Losing your database no longer puts your instance up for grabs `api 0.88.0` `web 0.111.0` + +**If your database is ever lost or replaced, your instance was claimable by whoever loaded the page +first.** From the outside it looked exactly like a brand-new deployment — no accounts, no completed +setup — so the install screen opened its 30-minute window and offered a super-admin account. Except +it was not a new deployment: it was yours, on a hostname already in DNS, already in people's browser +history, and already dialled by every agent you have enrolled. + +We found this the way these things get found. A `docker compose down -v` meant for a throwaway test +stack landed on a production one, because the compose file that had been copied into that directory +carried its own project name. The database went. What was left was an instance offering itself to the +internet. + +**What changes.** An instance that cannot prove it is new still opens the window — you need a way back +in — but creating the first administrator now costs the last characters of your vault master key. +You have it, in your `.env` or on the `jarvis_secrets` volume you were told to back up separately. +Nobody else does. + +**A genuine first installation is never asked for it**, and neither is a restart during a slow one. +The test is the age of the vault key at the moment your database first saw it: seconds on a real +first boot, months on a replacement. It is recorded once, so restarting the api to re-open a window +does not change the answer. + +**Nothing to do.** No new variable, no migration step. Instances that upgrade into this keep working +exactly as they are — the rule only ever applies to an instance with an empty database, which yours +is not. + +## 2026-08-24 — A friendlier install, and a vault key you can only be handed once `api 0.87.0` `web 0.110.0` + +**The install screen leads with Jarvis now.** The mark, the name and a welcome come before anything +is asked of you — the first thing this product ever showed a new deployer used to be a password +field with no explanation attached. Ten steps, about five minutes, and the road map on the first +screen is honest about which ones you can skip. + +**Only the address really needs you.** The model step used to refuse to continue without a model id +_and_ a key, which trapped anybody evaluating Jarvis without a provider key in hand — on a product +whose remote terminal, agent fleet and vault all work perfectly well without one. An untouched form +now continues and saves nothing. A half-filled one still refuses: that is a mistake rather than a +decision. + +**The wizard now asks who may create an account.** Self-registration defaults to open, and nothing +in the install had ever mentioned it — so an instance on a public hostname would accept an account +from anybody who found it, the moment you finished. It is a switch on the address step, beside the +field that decides who can reach the sign-in page at all. Change it later under **Settings → +Platform → Sign-in**. + +**The vault key is shown, and downloadable, exactly once.** The install screen hands you the file +rather than telling you to go and run `docker compose exec` — that command is a wall for anybody +deploying through Coolify, Portainer or a managed host, and what is behind the wall is the one value +no backup can reconstruct. The screen says so before you click. Afterwards, and any time later, the +key is on the host: + +```sh +docker compose exec api cat /var/lib/jarvis/secrets/vault-master-key +``` + +### Fixed + +- **An interrupted install could not be finished.** Coming back to the wizard after the organization + step showed you an empty form and demanded a name, and the request behind it could only fail — + the community edition allows one organization and you already had it. There was no way forward and + no way around. It now recognises the organization you already have and moves on. +- **A first boot with no configuration printed a red `ERROR` about `JARVIS_UNATTENDED`** on + instances where it was not set. Cosmetic, but it was the first thing a new deployer read in + `docker compose logs`. +- **`JARVIS_ADMIN_EMAIL` and `JARVIS_ADMIN_PASSWORD` alone would install the instance silently**, + with no `JARVIS_UNATTENDED=on`, and skip the wizard entirely. Those variables are what an + unattended install needs _in addition to_ the flag, never a second way of asking for one. If you + keep them in a shared shell or a fleet template, this is the release where they stop acting on + their own. +- **Installing in German or French left you in an English console** — the account the wizard creates + now records the language you installed in. + +### Changed + +- `JWT_ACCESS_TTL` and `JWT_REFRESH_TTL` are read from `.env` like every other tuning value + instead of being fixed in the compose file. `.env.example` documents what each actually does — + the refresh one is an idle timeout, not the session lifetime its name suggests. +- `PUBLIC_URL` is gone. It was a second variable for the same address as `WEB_ORIGIN` and had not + been read by anything for two releases; it is the stored instance address now, on a settings + screen. Remove it from your `.env` if it is still there — nothing breaks either way. + +### New in the manual + +The published README grew the procedures it was missing: restoring the vault key onto a new host +(**do it before the first `up -d`** — a generated key is never overwritten, so bringing the stack up +first makes yours unusable), getting back in when you are locked out of the only administrator +account, moving an instance to a new address and what that does to enrolled agents, and which +generated secrets can be rotated. + +## 2026-08-24 — `JARVIS_TELEMETRY=off` did not work, and now does + +**If you set `JARVIS_TELEMETRY=off` in `.env`, your instance has been reporting anyway.** We are +sorry. The variable never reached the container: compose reads `.env` to fill in `${...}` +placeholders inside the compose file, it does not hand that file to the services, and the `api` +service never listed this one. So the API saw nothing, applied its documented default of "on", and +checked in — while the README, the changelog and `.env.example` all told you that one line was +enough. + +What was sent is what the README has always listed and nothing more: your instance id and public +key, the version and contract hash it runs, its public address, and counts of organizations, users, +agents and assets. No names, no conversation content, nothing about the machines you administer. + +**The fix is in the compose file, not in the application**, so pulling this repository again is the +whole of it: + +```sh +git pull # or re-download docker-compose.yml +docker compose up -d +``` + +Then confirm it took, from the instance itself: + +```sh +docker compose exec api printenv JARVIS_TELEMETRY +``` + +No output means the fix has not landed yet; `off` means you are silent. **Settings → Licence** says +the same thing on screen, and that is the answer to trust — it reads what the API actually resolved +rather than what a file claims. + +If you would like the entries a silenced instance should never have created removed from our side, +write to and quote the instance id from that screen. + +## web 0.109.0 — 2026-08-24 + +**The welcome now comes before the account.** The very first thing anybody saw of Jarvis was a form +asking for a password — no name, no explanation, no idea what they were about to be given +administrator rights over. The introduction needs no account, so it goes first, and creating the +administrator is step two with a way back to it. + +**Mail and the licence are two steps.** They shared a page and have nothing to do with each other — +one is an app registration in your Entra tenant, the other a key from your supplier — which made one +long screen out of two short questions. The licence step now leads with the thing most people need +to hear: you almost certainly do not need a key, and the community edition is perpetual. + +## web 0.108.0 — 2026-08-24 + +**The install opens on a welcome rather than on a checklist.** It used to begin with "Checks: +database, Redis, vault key" — which tells somebody who has just deployed a product they have never +run that they are already in the middle of something. The first screen now introduces Jarvis, says +what the next steps will ask for, roughly how long it takes, and which of them you can skip. + +**The checks moved to the end, and that is a correction rather than a preference.** Run first, they +had nothing to look at: your public address had not been chosen, so there was no URL to probe and no +hostname to judge security keys against — three of the six findings were about a deployment that did +not exist yet. They now run immediately before the screen that commits the install, against the one +you have just described. + +**Reporting has a step of its own.** It shared a page with mail and the licence, where a question +about what leaves your network sat underneath two forms. It is the only decision in the wizard we +benefit from, which is exactly why it gets a page you cannot scroll past. + +**The step markers no longer collide.** Eight labels never fit the wizard column at any screen width +and ran into each other. The strip is markers now — filled where you are, ticked behind you — with +the step named in full underneath and the whole road laid out on the welcome screen. + +## api 0.86.0 · web 0.107.0 — 2026-08-24 + +**Mail and the licence are configured IN the wizard now, not somewhere else.** That step used to +show two paragraphs whose only affordance was a link to the settings screens — and those links could +not work: an instance that has not finished installing sends every other address back to the install +screen, so clicking one opened a tab that bounced straight back. The real forms are on the step now, +the same ones you meet under Settings afterwards, and nothing on it is required. + +**You can decide there whether this instance reports to us.** Reporting used to be an environment +variable and nothing else, so a fresh install had no way to answer the question without editing a +compose file. It is a switch on that step and on Settings → Licence. + +`JARVIS_TELEMETRY=off` keeps its authority: an instance whose host has switched reporting off shows +the control locked and says why. Deciding what leaves your network stays with whoever runs the host, +which is exactly why it was a variable in the first place — the switch adds the case where the person +installing the instance IS that person, and the case where you want to fall silent later without a +restart. + +**Your vault key is a download.** The last step used to print `docker compose exec api cat …` and ask +you to go and run it. If you deployed through Coolify, Portainer or a managed host, that was a wall +in front of the one thing here that no backup can reconstruct. Now the key is shown on the page with +a button that saves it as a file, and the command is still underneath for anyone who would rather +take it off the host. + +That view exists only while the install is unfinished. Once you press Finish it is refused for good, +and every time it was used is in the audit trail. + +**Unattended installation, for fleets and for CI.** Set `JARVIS_UNATTENDED=on` with +`JARVIS_ADMIN_EMAIL` and `JARVIS_ADMIN_PASSWORD` and the instance installs itself at boot from the +variables you already provide, and never shows the wizard. Everything else stays optional. + +If those credentials are missing or the password is under twelve characters it says so loudly and +leaves the install UNFINISHED rather than completing. An instance marked installed with no account +is one nobody can ever get into, and no restart recovers it — falling through to the ordinary install +screen is strictly better. + +## web 0.106.3 — 2026-08-24 + +**The install screen tells you where you are.** Its step list was six labels in a row where the +current one differed by a font weight — and the difference did not even arrive, because the class +meant to dim the others named a colour this console does not define. Every label rendered +identically, so the one screen whose whole job is to walk you through six steps could not say which +of them you were on. + +The stepper now carries that three ways at once, none of them colour alone: a tick for what is done +and a filled number for where you are, connectors that fill in behind you, and a line that names the +step — "Step 3 of 6 · The model" — which is also what makes it readable on a phone, where the labels +step aside. + +The same missing colour had flattened the rest of the wizard: hints and explanations rendered as +bright as the headings above them, error messages were not red, and the boxes around the warnings +had no edge. All of it reads properly now. + +## api 0.85.3 — 2026-08-24 + +**An instance still being installed no longer reports itself.** It used to check in thirty seconds +after boot, before anybody had chosen its address — so a fresh install announced itself as +`http://localhost:3000`, with every count at zero. That is worse than silence: the address exists in +the report so that your provider can tell one installation from another, and a list of identical +localhost entries answers nothing. + +It now waits until the install screen is finished, then reports normally with the address you chose. +Nothing is lost, only deferred. `JARVIS_TELEMETRY=off` still silences it entirely. + +## web 0.106.2 — 2026-08-23 + +**A language picker on the install screen.** Every other page that can be reached without an account +has one — the sign-in page, a shared transcript — and the install wizard, which is the longest piece +of reading this product puts in front of somebody who has not yet decided to trust it, did not. + +It follows your browser as before; the picker is for the case the browser is wrong, which is +routinely: installing from a colleague's laptop, or on a server whose locale nobody set. Before the +first account exists the choice is remembered in this browser; after it, it is saved to your account +as well — so it also corrects the language the claim guessed. + +## api 0.85.2 · web 0.106.1 — 2026-08-23 + +**Installing in German now ends in a German console.** The install screen has always followed your +browser, in English, French or German — but the account it created took the database default of +English, and the console adopted that the moment the wizard handed over. Somebody who had just read +seven screens in their own language was greeted in another and left to find the language switcher. + +The account now records the language the wizard was read in. Change it whenever you like under +Settings → Profile; this only decides where you start. + +## api 0.85.1 — 2026-08-23 + +**A fresh install no longer stores an address nobody chose.** api 0.85.0 wrote +`http://localhost:3000` as the public address of any instance that started with no configuration — +the schema default for `WEB_ORIGIN`, saved as though somebody had decided it. The boot banner then +told an operator on a remote host to open a URL that leads nowhere. + +The address is now left unset until you choose it, which is what the install screen already assumed: +it fills the field from the address you are reading it at, and the checks say plainly that nothing is +set yet. The banner names the path and not the host — you know how you reach the machine, you were +only missing `/install`. + +Nothing to do if you set `WEB_ORIGIN` yourself: it is still read, once, exactly as before. + +## api 0.85.0 · web 0.106.0 — 2026-08-23 + +**Installing Jarvis no longer starts with a text editor.** + +```sh +docker compose up -d +``` + +Then open it in a browser. An install screen creates the first administrator, checks the deployment +and asks for the rest. **The `.env` step is gone** — it demanded six values before the stack would +start, four of them secrets you had to generate with `openssl`, and the whole file is optional now. + +**A wrong model key is a message instead of a crash loop.** This is the change underneath all the +others. The api validated its configuration at boot and called `process.exit(1)` when anything was +missing — so a mistyped key, an account out of credit or an endpoint that had moved presented as a +container that would not stay up, with the actual reason on line 40 of a log you had no reason to +open. An unconfigured instance now starts, says what it needs, and the model screen has a **test +button that asks the model for a token before saving**, showing the provider's own words when it +refuses. + +**A brand-new instance can be claimed for 30 minutes, and this closes a real hole.** Jarvis used to +accept exactly one registration on an instance with no users and make that account super-admin — so +a freshly deployed instance reachable from the internet belonged to whoever found it first, on a +product that holds SSH keys and opens shells on your clients' servers. The install screen shows a +countdown; `docker compose restart api` opens another window if you miss it. + +**Everything the wizard asks has a permanent screen.** Settings → Platform gained two tabs: **Model** +— endpoint, key, model and thinking level, with the same test button the wizard uses — and +**Address**, for your public address and proxy-hop count. The wizard is a first-run convenience, not +the only way in: an instance whose OpenAI key has been rotated, or whose hostname has moved, says so +on a settings page rather than being re-installed. + +**The proxy setting is now something you are shown rather than asked to guess.** `TRUST_PROXY_HOPS` +decided which address landed in your audit trail, and nobody can know it in advance. The install +screen displays the `X-Forwarded-For` chain your own request actually carried and the address the +API resolved from it, and you confirm what you see. + +**Your secrets are generated for you, into a new `jarvis_secrets` volume.** Back it up, **separately +from the database** — the vault key lives there and nowhere else, and a database backup holds only +ciphertext. The install screen will not finish until you have read the key and typed its last +characters back. The key never leaves the server: there is deliberately no endpoint that returns it. + +### Upgrading from an earlier build? There is nothing to do + +Leave your `.env` exactly as it is. Your secrets are **adopted** into the new volume rather than +replaced, so the vault is untouched. `WEB_ORIGIN` becomes the stored public address, your `OPENAI_*` +values become the stored model settings, and an instance that already has an administrator is marked +installed by a backfill — **you will never see the install screen**. Everything those variables used +to govern is now a settings page; the variables still seed a fresh install and are otherwise ignored. + +One thing did change for you: **`PUBLIC_URL` is no longer read.** It was a second variable for the +same address. There is one now, seeded from `WEB_ORIGIN` and edited under Settings → Platform. + +**Changing your public address orphans enrolled passkeys.** It always did — a passkey is bound to the +hostname it was created under — but it was previously a variable nobody edited twice. Now that it is +a field on a screen, the screen warns you before it saves. + ## api 0.84.0 · web 0.105.0 — 2026-08-21 **Ask for a licence from inside Jarvis.** Settings → Licence has a form: your company, somebody to diff --git a/README.md b/README.md index a0b3736..3b0f3a8 100644 --- a/README.md +++ b/README.md @@ -165,7 +165,8 @@ inventory on every heartbeat. builds for Linux, macOS and Windows. - A hostname and a TLS terminator in front of it. Jarvis speaks plain HTTP and does not manage certificates. -- An API key for an OpenAI-compatible endpoint. The api will not start without one. +- An API key for an OpenAI-compatible endpoint. The install screen asks for it and tests it before + saving; you do not need it in hand before you start. - **No licence key.** Jarvis runs the [community edition](#the-community-edition) out of the box; a key raises the limits when you outgrow them. - Roughly 2 GB of RAM for the stack and room for Postgres to grow. @@ -173,25 +174,78 @@ inventory on every heartbeat. ## Install ```sh -git clone https://git.luxit.be/Luxit/jarvis-selfhost.git -cd jarvis-selfhost -cp .env.example .env -$EDITOR .env # every comment in it is load-bearing; the secrets note especially -docker compose pull +curl -O https://git.luxit.be/Luxit/jarvis-selfhost/raw/branch/main/docker-compose.yml docker compose up -d ``` -First boot syncs the database schema and runs its data backfills before the API listens, so give it -about a minute. Then point your reverse proxy at `JARVIS_PORT` and open the app. +That is the whole first deployment. **There is no `.env` step.** First boot generates the stack's +secrets, syncs the database schema and runs its data backfills before the API listens, so give it +about a minute. Then point your reverse proxy at port 8080 and open the address in a browser. -**Give Jarvis its public address with the right scheme.** `WEB_ORIGIN` is the CORS decision and it -accepts exactly one origin; `PUBLIC_URL` is what gets baked into agent install commands, invitation -links and share links. A wrong `WEB_ORIGIN` presents as a sign-in page that cannot sign in. A wrong -`PUBLIC_URL` does not fail at deploy time — it fails weeks later, on somebody else's server. +An install screen takes it from there. It welcomes you, creates the first administrator inside a +bounded window, then asks for the address this instance answers on and who may create an account on +it, the model, your first organization, and — every one of them skippable — mail, a licence and +whether this instance reports anything about itself. It finishes by showing you what the deployment +actually looks like from inside, and by making you take a backup of the one value nothing can +reconstruct. Ten screens, about five minutes, and **only the address really needs you**: everything +else has a "later" that costs nothing. -**The first account created becomes super-admin.** Self-registration is open by default, so sign up -immediately after the stack is up and then close registration under **Settings → Platform → -General**. Leaving it open means anyone who reaches the sign-in page gets an account. +Everything it asks is a setting you can change afterwards from an ordinary screen. Nothing it asks +needs a container restart to change. + +### The claim window + +**A brand-new instance can be claimed for 30 minutes after it starts, and the first account created +owns it.** That is why the install screen shows a countdown. + +This replaces something worse. Jarvis used to accept exactly one registration on an instance with no +users and make that account super-admin — so a freshly deployed instance reachable from the internet +belonged to whoever found it first, on a product that holds SSH keys and opens shells on your +clients' servers. The window closes that. + +If you miss it, nothing is lost. Restart the api container to open another: + +```sh +docker compose restart api +``` + +**Losing your database does not put your instance up for grabs.** An instance whose database has gone +missing looks exactly like a brand-new one from the outside — no accounts, no completed setup — while +sitting on a hostname the world already knows. On that instance the window still opens, so you can +get back in, but creating the first administrator costs the last characters of your vault master key. +You have it; nobody else does. A genuine first installation is never asked for it. + +### The one thing to do afterwards + +**Back up the `jarvis_secrets` volume, and not to the same place as your database.** + +It holds this instance's vault master key, which is generated on first boot and exists nowhere else. +Every credential in the vault, the licence identity, every authenticator secret and every terminal +recording is encrypted under it. **A database backup does not save you** — the backup holds the +ciphertext. An instance whose key is gone keeps looking configured, with every row in place, and +fails on the first reveal. + +The install screen shows you the key, offers it as a file to download, and will not let you finish +until you have typed its last characters back. **Take the download while it is on screen** — that is +the easiest moment this value will ever be available to you. + +Afterwards, and any time later, read it from the host: + +```sh +docker compose exec api cat /var/lib/jarvis/secrets/vault-master-key +``` + +### If you prefer to configure it in writing + +`.env.example` is still there and every value in it is optional — image pinning, the host port, +the agent overlay. The ones that overlap with the install screen SEED it: they are read once, when +nothing is stored yet, and never again. Change them afterwards and nothing happens; change the +setting instead. + +**Upgrading from a stack that already has a `.env`?** Leave it exactly as it is. Your secrets are +adopted into the volume rather than replaced, your `WEB_ORIGIN` becomes the stored public address, +your model settings are adopted the same way, and an instance that already has an administrator is +marked installed by a backfill — so you will never see the install screen. Nothing to do. ## Your reverse proxy has two requirements @@ -206,9 +260,12 @@ Both are the kind that produce confusing symptoms rather than clean errors. a byte, and a 60s default cuts the response mid-stream. The client sees a connection reset rather than a timeout, which reads like a bug in Jarvis. -Forward `X-Forwarded-For` too, and set `TRUST_PROXY_HOPS` to match how many proxies rewrite it — -that is what puts real client addresses in the audit trail and the session list instead of your -proxy's. +Forward `X-Forwarded-For` too. How many proxies rewrite it is a setting, and the install screen is +where you answer it — **it shows you the chain your own request actually carried and the address the +API resolved from it**, so you confirm what you see rather than counting hops from memory. That is +what puts real client addresses in the audit trail and the session list instead of your proxy's, and +setting it too high is the dangerous direction: the API would then believe that many hops of a +header a caller can forge. Change it later under **Settings → Platform**. ## Upgrading @@ -322,10 +379,19 @@ the grant. Decide who holds those before enrolling anything you care about. These are deliberate and documented rather than surprises waiting to be found. -- **`VAULT_MASTER_KEY` has no recovery.** Read the note in `.env.example`. A database backup does not - protect what it seals — the backup holds ciphertext encrypted under that key, and that covers more - than the vault: the outbound-mail client secret, this instance's licence identity key, every TOTP - secret and every terminal recording go with it. +- **The vault key has no recovery.** It is generated on first boot into the `jarvis_secrets` volume + and exists nowhere else. A database backup does not protect what it seals — the backup holds + ciphertext encrypted under that key, and that covers more than the vault: the outbound-mail client + secret, this instance's licence identity key, every TOTP secret and every terminal recording go + with it. The install screen makes you read it and type its last characters back before it will + finish, which is the only reason anybody would. See [Backups](#backups-and-restoring-one). + **It also cannot be rotated.** There is no procedure that re-wraps existing data under a new key, + so if this value is disclosed — read out on a screen share, pasted into a ticket, on a laptop that + walked — the answer is a new instance and a fresh set of credentials, not a rotation. The other + three generated secrets are ordinary: delete the file from `jarvis_secrets` and restart, and the + init service writes a new one. Doing that to `jwt-access-secret` or `jwt-refresh-secret` signs + everybody out, which is usually the point; doing it to `postgres-password` needs the database's own + password changed to match, so plan that one. - **SSH host keys are not verified.** Every SSH connection trusts whatever key answers. This is the one gap in the execution path with no compensating control. - **The api must run as a single replica.** In-flight runs, pending approvals, presence and the @@ -365,14 +431,18 @@ docker compose ps # who is running, and who is restarting docker compose logs -f api # the api says why it refused to start ``` -The api validates its configuration at boot and **fails fast rather than starting half-configured**, -so a first-boot failure is almost always one line of `.env`: +**An unconfigured instance now starts and says so** rather than refusing to boot. That is the +change: a wrong model key used to make the api exit 1, and the restart policy turned it into a crash +loop with the reason buried in a log. It comes up, shows the install screen, and tells you what is +wrong on the screen that asks for it. | Symptom | Cause | | --- | --- | -| `docker compose up` exits complaining about a variable | That variable is empty in `.env`. Compose refuses before any container starts. | -| api restarts in a loop, logs `Invalid environment configuration` | A value is present but wrong. `VAULT_MASTER_KEY` must base64-decode to exactly 32 bytes. | -| The sign-in page loads but cannot sign in | `WEB_ORIGIN` is not the address the browser used, scheme included. It accepts one origin, not a list. | +| Every page redirects to an install screen | This instance has not been installed yet. That is the wizard, not an error. | +| The install screen says the claim window has closed | Nobody claimed it within 30 minutes of boot. `docker compose restart api` opens another. | +| The assistant answers "this instance has no model configured" | Exactly that. **Settings → Platform → Model**, where the test button will tell you what the provider thinks. | +| api restarts in a loop, logs `Invalid environment configuration` | A value you set yourself is wrong. Only the database, Redis and the vault key are validated at boot now. | +| The sign-in page loads but cannot sign in | The stored public address is not the one the browser used, scheme included. Change it under **Settings → Platform**. | | Chat never answers and nothing streams | The WebSocket upgrade is not being forwarded. | | An answer dies part-way through, every time | The proxy's read timeout is too short. | | Everything works but one kind of thing cannot be created | A limit is full — the community edition's, or your licence's. The message names which. See below. | @@ -381,20 +451,110 @@ so a first-boot failure is almost always one line of `.env`: The api takes about a minute on first boot, syncing the schema before it listens. `docker compose ps` showing `health: starting` for that long is expected, not a fault. +### When you are locked out of the only administrator account + +There is no password reset in this product — no email flow, no "forgot password" link. That is +deliberate, and it means the single super-admin the wizard creates is a single point of failure until +you do something about it. + +**Do this now, not later: make a second super-admin.** **Settings → Users → New user**, platform role +*Super admin*, and set their password on the same screen. Outbound mail is not required for it. Two +minutes, and it turns every case below into somebody else clicking a button. + +**If another account can still sign in**, promote it and let it fix the first: + +```sh +docker compose exec -T postgres psql -U jarvis -d jarvis \ + -c "UPDATE \"User\" SET \"platformRole\" = 'SUPER_ADMIN' WHERE email = 'colleague@example.com';" +``` + +They then reset the locked-out password under **Settings → Users**. + +**If no account can sign in at all**, set a password hash directly. Jarvis stores argon2id, and the +api image carries the library that makes one — so the hash is generated by the same code that will +check it: + +```sh +docker compose exec api node -e \ + "const a=require('argon2');a.hash(process.argv[1],{type:a.argon2id}).then(h=>console.log(h))" \ + 'the-new-password-at-least-12-characters' +``` + +Then write it, and make sure the account is active and privileged: + +```sh +docker compose exec -T postgres psql -U jarvis -d jarvis -c \ + "UPDATE \"User\" SET \"passwordHash\" = '', \"isActive\" = true, + \"platformRole\" = 'SUPER_ADMIN' WHERE email = 'you@example.com';" +``` + +Quote the hash in single quotes — it contains `$` characters your shell would otherwise eat. + +If the account also holds a second factor you no longer have: when another administrator exists, they +do it properly from **Settings → Users**, which strips the factors, ends every open session and files +an audit entry. With nobody left to click it, clear all three parts by hand — the passkeys, the +authenticator secret, and the flag that says a factor is expected — or the account will keep +demanding one: + +```sh +docker compose exec -T postgres psql -U jarvis -d jarvis <<'SQL' +DELETE FROM "WebAuthnCredential" WHERE "userId" = (SELECT id FROM "User" WHERE email = 'you@example.com'); +DELETE FROM "UserTotpCredential" WHERE "userId" = (SELECT id FROM "User" WHERE email = 'you@example.com'); +UPDATE "User" SET "mfaEnabled" = false, "mfaEnabledAt" = NULL WHERE email = 'you@example.com'; +SQL +``` + +Enrol a new factor as soon as you are back in, and note that this leaves no audit trail of its own — +the trail is your shell history. + +**Re-running the install wizard is not a recovery route.** Its claim window only opens on an instance +with no users and no completed setup, so on a working deployment it will refuse — and making it +refuse less by clearing those columns by hand would hand your live instance to whoever reaches it +first. + +### Moving the instance to a new address + +Changing the public address under **Settings → Platform → Instance** is supported, and it has two +consequences that are not obvious and not reversible by changing it back. + +**Enrolled agents keep dialling the old one.** Each machine stores the address it was enrolled with +and does not learn a new one from the server. After a move they go OFFLINE and stay there. Every +agent has to be re-enrolled against the new address, so a move is a job scheduled with whoever +administers those machines rather than a settings change made on a Friday. + +**Every passkey stops being offered.** A passkey is bound to the hostname it was enrolled under. They +do not fail loudly — the browser simply stops presenting them — so accounts quietly fall back to +passwords, and anyone who set up a passkey and never learned their password is locked out. Make sure +the people who use passkeys know their passwords, or enrol them again afterwards, before you move. + +Nothing else moves: conversations, credentials, assets and recordings are rows and do not know the +address. Update your reverse proxy and the `WEB_ORIGIN`-shaped world outside first, then the setting, +then the agents. + ## Backups, and restoring one Postgres holds everything except the vault key — avatars, documents, exports and terminal recordings -are all rows, not files on disk. **Of the stack's volumes, only `postgres_data` is worth backing -up.** `redis_data` carries nonces, rate-limit counters and the socket fan-out, all of which rebuild -themselves; `agent_releases` is refilled by the next `docker compose pull`. +are all rows, not files on disk. **Two volumes are worth backing up, and they must go to different +places:** `postgres_data` and `jarvis_secrets`. `redis_data` carries nonces, rate-limit counters and the +socket fan-out, all of which rebuild themselves; `agent_releases` is refilled by the next +`docker compose pull`. + +They are separate volumes precisely so that they can be, and must be, backed up separately. A dump +that travelled with the key that opens it is a dump that opens itself. ```sh docker compose exec -T postgres pg_dump -U jarvis jarvis | gzip > jarvis.sql.gz ``` -Plus `VAULT_MASTER_KEY`, stored somewhere that is not this host. A dump without the key is a database -whose credentials cannot be read — and an instance restored under a *different* key keeps **looking** -configured, because nothing on a settings page decrypts anything. It fails on every reveal instead. +Plus the vault key, stored somewhere that is not this host and not beside the dump: + +```sh +docker compose exec api cat /var/lib/jarvis/secrets/vault-master-key +``` + +A dump without the key is a database whose credentials cannot be read — and an instance restored +under a *different* key keeps **looking** configured, because nothing on a settings page decrypts +anything. It fails on every reveal instead. To restore, stop the api so nothing writes while you work, then load the dump into an empty database: @@ -409,6 +569,40 @@ docker compose start api web Restore under the **same `VAULT_MASTER_KEY`** the dump was taken with. That value is not in the dump, and no part of the restore will warn you that it differs. +### Putting the key back on a new host + +**Do this before the first `docker compose up -d`, not after.** The generator writes each secret with +`O_EXCL` and never overwrites one that exists, which is what stops an upgrade quietly replacing the +key your vault is sealed under. On a fresh host it cuts the other way: bring the stack up first and +a brand-new key is written, after which anything you put in `.env` is ignored for good. + +If you kept the `jarvis_secrets` volume, restore it and nothing else is needed. If all you have is +the base64 string, put it in `.env` before the first start: + +```sh +# On the NEW host, in the directory holding docker-compose.yml — before any `up -d`. +echo 'VAULT_MASTER_KEY=' >> .env +docker compose up -d +``` + +The `init` service adopts that value into the volume on the first run and the api reads it from +there afterwards, so the line in `.env` is a seed rather than a permanent setting — you may remove it +once the stack is up. + +If you have already started the stack and a wrong key was generated, delete the file and let the init +service run again. **Only ever do this on a host whose vault you are deliberately re-keying** — on a +working instance it destroys every credential, the mail client secret, this instance's licence +identity, every TOTP secret and every terminal recording: + +```sh +docker compose down +docker run --rm -v jarvis_secrets:/s alpine rm -f /s/vault-master-key +# then the `.env` line above, then `docker compose up -d` +``` + +To check you restored under the right one, reveal a stored credential in **Settings → Vault**. The +rows are all there under a wrong key; only a reveal tells you the truth. + ## Removing it ```sh @@ -460,6 +654,21 @@ reach us would just be an email with extra steps. feature and every machine; what you lose is the request form, and our ability to know you exist. That is a fair trade and we would rather you took it than felt watched. +> **This did not work before 24 August 2026, and we are sorry.** The line was read from `.env` by +> compose — which uses that file to fill in placeholders inside the compose file, not to hand values +> to the services — and the `api` service never listed the variable, so the API saw nothing and +> applied its default of "on". An instance that had opted out reported anyway. Pull this repository +> again and `docker compose up -d`; the fix is in the compose file, not in the images. +> +> **Do not trust a file to tell you.** Ask the instance: +> +> ```sh +> docker compose exec api printenv JARVIS_TELEMETRY +> ``` +> +> No output means it still has not landed. **Settings → Licence** answers the same question on +> screen, from what the API actually resolved — which is why that screen exists. + Everything in the next section applies to a community instance as well: the field list is the whole field list, and it is the same one. diff --git a/docker-compose.yml b/docker-compose.yml index 29eba1d..d853675 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -2,10 +2,21 @@ # # Everything runs from published images; nothing is built here and no source is needed. # -# cp .env.example .env # then fill it in — see the comments in that file # docker compose pull # docker compose up -d # +# That is the whole first deployment. Open the address in a browser and an install screen asks the +# questions: it creates the first administrator, checks what this deployment actually looks like +# from inside, and stores the address, the model and the rest as settings you can change later. +# +# THERE IS NO .env STEP ANY MORE. This file used to demand six variables before it would start, +# four of them secrets you had to generate with `openssl` — and it failed badly when one was wrong, +# because the API exited 1 and the restart policy turned a typo into a crash loop. `.env.example` +# still exists and every value in it is optional: pinning, the host port, the agent overlay. +# +# THE ONE THING TO DO AFTERWARDS: back up the `jarvis_secrets` volume, separately from the database. +# The install screen shows you why and will not finish until you have. See the `init` service below. +# # Only the `web` service publishes a port. Its nginx serves the app and reverse-proxies /api and the # websocket to the internal `api` service, so your own TLS terminator has exactly one target and the # API is never reachable from outside this compose network. @@ -30,15 +41,53 @@ name: jarvis services: + # Writes the stack's secrets, once, into a volume the other services read. Runs to completion + # before anything else starts and then exits — `docker compose ps` shows it as `Exited (0)`, which is + # what success looks like and not something to fix. + # + # THIS IS WHY .env NO LONGER ASKS FOR FOUR openssl INVOCATIONS. A vault key, two signing secrets + # and a database password are values no person should be choosing, and asking for them put the + # most consequential one — the vault key, which everything in the vault is sealed under — in the + # hands of whoever was least equipped to look after it, at the moment they were least interested. + # + # It never overwrites. On an upgrade it ADOPTS whatever is still in your .env, so a stack that has + # been running for a year keeps its own keys and this service is inert from its second run onward. + # + # BACK THIS VOLUME UP, AND SEPARATELY FROM THE DATABASE. It is deliberately not `postgres_data`: + # every credential in the vault is encrypted under a key that lives here, so a database dump that + # travelled with its own key would be a dump that decrypts itself. The setup screen shows you the + # key once and will not let you past until you have put it somewhere. + init: + image: ${JARVIS_IMAGE_API:-git.luxit.be/luxit/jarvis-api:stable} + entrypoint: ["node", "/app/apps/api/init-secrets.cjs"] + restart: "no" + environment: + # Only read when the corresponding file does not exist yet — the upgrade path for a stack + # whose .env already holds these. Empty on a fresh install, which is the ordinary case now. + VAULT_MASTER_KEY: ${VAULT_MASTER_KEY:-} + JWT_ACCESS_SECRET: ${JWT_ACCESS_SECRET:-} + JWT_REFRESH_SECRET: ${JWT_REFRESH_SECRET:-} + POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-} + volumes: + - jarvis_secrets:/var/lib/jarvis/secrets + postgres: image: postgres:16-alpine restart: unless-stopped + depends_on: + init: + condition: service_completed_successfully environment: POSTGRES_USER: jarvis - POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:?set POSTGRES_PASSWORD in .env} + # The file, never the variable. The two are mutually exclusive in this image — it refuses to + # start when both are set — which is exactly why `init` adopts an existing .env value into the + # file instead of the compose file trying to choose between them. + POSTGRES_PASSWORD_FILE: /var/lib/jarvis/secrets/postgres-password POSTGRES_DB: jarvis volumes: - postgres_data:/var/lib/postgresql/data + # Read-only: postgres consumes this secret and has no business ever writing one. + - jarvis_secrets:/var/lib/jarvis/secrets:ro healthcheck: test: ["CMD-SHELL", "pg_isready -U jarvis -d jarvis"] interval: 5s @@ -71,33 +120,58 @@ services: condition: service_healthy redis: condition: service_healthy + init: + condition: service_completed_successfully + volumes: + # Read-only. The api reads these keys and must never be the thing that creates one: a + # container that could write here is a container whose bug can replace the key the vault is + # sealed under, and nothing about that failure is visible until somebody opens a credential. + - jarvis_secrets:/var/lib/jarvis/secrets:ro environment: NODE_ENV: production API_PORT: "4000" - # The address a browser reaches Jarvis on. This is the CORS decision: requests from any other - # origin are refused, so a wrong value here presents as a sign-in page that cannot sign in. - WEB_ORIGIN: ${WEB_ORIGIN:?set WEB_ORIGIN in .env} - # The address baked into agent install commands and dialled by every enrolled machine. Usually - # the same string as WEB_ORIGIN; separate because they answer different questions, and a wrong - # value here does not fail at deploy time — it fails weeks later, on somebody else's server. - PUBLIC_URL: ${PUBLIC_URL:-${WEB_ORIGIN}} + # A SEED, not a requirement. The install screen asks for this address and pre-fills it with the + # one you are reading the page at; whatever is stored wins from then on. Passed through so a + # stack that already set it upgrades with its origin intact, and empty on a fresh install — + # which is a supported state, not a broken one. + # + # PUBLIC_URL is deliberately no longer passed. It was a second variable for the same address, + # and every consumer now reads the single stored value. + WEB_ORIGIN: ${WEB_ORIGIN:-} - DATABASE_URL: postgresql://jarvis:${POSTGRES_PASSWORD}@postgres:5432/jarvis?schema=public + # Assembled by the entrypoint from POSTGRES_PASSWORD_FILE, because a password that lives in a + # file cannot be interpolated into a URL by compose. Set DATABASE_URL in .env to override it + # outright — pointing at a managed postgres outside this stack, say. + DATABASE_URL: ${DATABASE_URL:-} + POSTGRES_PASSWORD_FILE: /var/lib/jarvis/secrets/postgres-password REDIS_URL: redis://redis:6379 - JWT_ACCESS_SECRET: ${JWT_ACCESS_SECRET:?set JWT_ACCESS_SECRET in .env} - JWT_REFRESH_SECRET: ${JWT_REFRESH_SECRET:?set JWT_REFRESH_SECRET in .env} - JWT_ACCESS_TTL: "900" - JWT_REFRESH_TTL: "1209600" + # Delivered as files rather than values, by the `init` service above. The `_FILE` suffix is the + # convention this postgres image and most others already use, so a `docker secret` of your own + # can be pointed at these paths instead with nothing here changing. + JWT_ACCESS_SECRET_FILE: /var/lib/jarvis/secrets/jwt-access-secret + JWT_REFRESH_SECRET_FILE: /var/lib/jarvis/secrets/jwt-refresh-secret + # Interpolated like every other tuning value, rather than frozen here. These two were the only + # ones written as literals, which meant the one knob an operator under a session policy asks + # for was also the one they could not reach from `.env`. See `.env.example` for what each + # actually does — the refresh one is not the session lifetime it looks like. + JWT_ACCESS_TTL: ${JWT_ACCESS_TTL:-900} + JWT_REFRESH_TTL: ${JWT_REFRESH_TTL:-1209600} - # READ THE NOTE IN .env.example BEFORE CHANGING THIS. Every credential in the vault is - # encrypted under it; lose it and they are gone, with no recovery of any kind. - VAULT_MASTER_KEY: ${VAULT_MASTER_KEY:?set VAULT_MASTER_KEY in .env} + # THE ONE YOU CANNOT LOSE. Every credential in the vault is encrypted under it, and so is this + # instance's licence identity, every authenticator secret and every terminal recording. It is + # generated into `jarvis_secrets` on your first boot and exists NOWHERE ELSE — back that volume + # up separately from the database. The setup screen shows it to you once. + VAULT_MASTER_KEY_FILE: /var/lib/jarvis/secrets/vault-master-key - # Any OpenAI-compatible endpoint. Defaults to OpenAI itself. + # Any OpenAI-compatible endpoint. All four of these are seeds for the stored settings. OPENAI_BASE_URL: ${OPENAI_BASE_URL:-https://api.openai.com/v1} - OPENAI_API_KEY: ${OPENAI_API_KEY:?set OPENAI_API_KEY in .env} + # NO LONGER REQUIRED, and that is the point of the change. This used to be validated at boot, + # so a key that was merely wrong made the API exit 1 and the restart policy turned it into a + # crash loop. The install screen asks for it, tests it against the provider, and stores it + # encrypted; what is passed here only seeds an instance that has nothing stored yet. + OPENAI_API_KEY: ${OPENAI_API_KEY:-} OPENAI_MODEL: ${OPENAI_MODEL:-gpt-4o} OPENAI_THINKING_LEVEL: ${OPENAI_THINKING_LEVEL:-medium} @@ -111,6 +185,20 @@ services: # Required to create new organizations, users or agents. Without it an instance keeps running # everything it already has, creates nothing new, and contacts nobody. See the README. JARVIS_LICENSE_KEY: ${JARVIS_LICENSE_KEY:-} + + # WHETHER ANYTHING LEAVES THIS NETWORK, and it has to be passed explicitly — this line is the + # whole control. It was missing until 24 August 2026, and its absence made a published promise + # false: `.env` is read by compose for `${...}` INTERPOLATION ONLY, never injected into a + # container, and no `env_file` makes up the difference. So an operator who wrote + # `JARVIS_TELEMETRY=off` in `.env` — exactly as the README, the changelog and `.env.example` all + # told them to — was reporting to the publisher the entire time, because the api saw nothing + # and applied its default of "on". + # + # Defaulted to the word rather than to empty, deliberately. The schema defaults an ABSENT + # value to "on", but `${VAR:-}` passes an empty STRING, which is present — the same trap that + # killed WEB_ORIGIN on a zero-config boot. Passing "on" means the container never has to + # reason about the difference. + JARVIS_TELEMETRY: ${JARVIS_TELEMETRY:-on} # Which distribution channel this instance follows, shown to signed-in operators beside the # version numbers. Set `JARVIS_CHANNEL=stable` (or `dev`) in .env if you track a channel; # LEAVE IT EMPTY IF YOU PIN EXACT VERSIONS, because then you follow no channel — you follow a @@ -121,6 +209,13 @@ services: # one you are on. APP_CHANNEL: ${JARVIS_CHANNEL:-} + # Install from the environment and never show the wizard. For fleets and for CI — see + # .env.example. Off means the ordinary install screen, which is what one deployment wants. + JARVIS_UNATTENDED: ${JARVIS_UNATTENDED:-off} + JARVIS_ADMIN_EMAIL: ${JARVIS_ADMIN_EMAIL:-} + JARVIS_ADMIN_PASSWORD: ${JARVIS_ADMIN_PASSWORD:-} + JARVIS_ADMIN_NAME: ${JARVIS_ADMIN_NAME:-} + AGENT_HEARTBEAT_INTERVAL_SEC: ${AGENT_HEARTBEAT_INTERVAL_SEC:-30} RUN_SHUTDOWN_GRACE_SEC: ${RUN_SHUTDOWN_GRACE_SEC:-25} @@ -153,3 +248,6 @@ services: volumes: postgres_data: redis_data: + # The keys, deliberately apart from postgres_data. Back it up, and not to the same place: a + # database dump is worthless to a thief without this, and worthless to YOU without it either. + jarvis_secrets: