The self-hosting stack
This commit is contained in:
+237
@@ -0,0 +1,237 @@
|
||||
# Jarvis — optional configuration.
|
||||
#
|
||||
# THERE IS NOTHING YOU HAVE TO FILL IN. You do not need this file at all:
|
||||
#
|
||||
# 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
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
# 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.
|
||||
#
|
||||
# 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.
|
||||
#
|
||||
# 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
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# 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.
|
||||
|
||||
#OPENAI_BASE_URL=https://api.openai.com/v1
|
||||
#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 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)
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
# Every image tracks the `stable` channel unless you set these, so `docker compose pull && up -d`
|
||||
# upgrades you to whatever has most recently been promoted. `stable` moves only after a build has
|
||||
# run on the publisher's own instance; `dev` moves on every build and nothing has tried it yet.
|
||||
# `latest` is a second name for `stable`, kept so nothing that already used it has to change.
|
||||
#
|
||||
# Which channel you are on is yours to state, because nothing in the image knows it — a channel is
|
||||
# decided after a build and moves afterwards. Set it and the app shows it beside the version
|
||||
# numbers; leave it empty if you pin below, because then you follow no channel.
|
||||
#JARVIS_CHANNEL=stable
|
||||
|
||||
# Pinning makes an upgrade a decision instead of a side effect of pulling. The app reports its real
|
||||
# version either way — a channel tag is a second name on the same image, not a build that forgot
|
||||
# its number.
|
||||
#
|
||||
# THERE IS NO EXAMPLE NUMBER HERE ON PURPOSE. Pin the version you are ALREADY RUNNING, which the app
|
||||
# footer shows as `web … · api …`. Nothing in the publishing path bumps a number written into this
|
||||
# file, so any number printed here is one that went stale while nobody was looking — and moving the
|
||||
# api pin BACKWARDS runs an old build against a schema that has already been migrated forward.
|
||||
#JARVIS_IMAGE_API=git.luxit.be/luxit/jarvis-api:<the api version in your footer>
|
||||
#JARVIS_IMAGE_WEB=git.luxit.be/luxit/jarvis-web:<the web version in your footer>
|
||||
|
||||
# Only read when the agent overlay is enabled, just below. Its version is the AGENT's, and moves
|
||||
# independently of the two above — a Jarvis release usually does not change the agent at all.
|
||||
#
|
||||
# PIN THIS ONE FIRST if you pin only one. The two above change what your own server runs; this one
|
||||
# changes what runs on every machine you administer. Pinning it decides which build your instance
|
||||
# publishes; a separate UPDATE POLICY decides when a machine takes it, and its default is "let the
|
||||
# agent decide" — which in practice means the next time its service starts. Set that policy
|
||||
# instance-wide under Settings → Platform → General, per organization under Settings → Organization →
|
||||
# Agent updates. An enrolled agent also refuses any version that is not strictly newer, so moving
|
||||
# this back stops a rollout rather than reversing it on machines that already took the update.
|
||||
#
|
||||
# The app footer does not carry this number. Each enrolled machine reports the build it runs, on the
|
||||
# Agents page — that is the one to pin.
|
||||
#JARVIS_IMAGE_AGENT=git.luxit.be/luxit/jarvis-agent-dist:<version>
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# The Jarvis agent (optional)
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
# Enrolling a machine downloads a compiled binary, which the api serves from a directory it can
|
||||
# only read. `docker-compose.agent.yml` supplies that directory as a pullable image, so the release
|
||||
# arrives the same way the rest of the stack does. Uncomment this and every later `docker compose`
|
||||
# command picks up both files with no extra flags:
|
||||
#
|
||||
#COMPOSE_FILE=docker-compose.yml:docker-compose.agent.yml
|
||||
#
|
||||
# Leaving it off is a supported state, not a broken one: everything except the agent works, and the
|
||||
# installer answers 503 saying no build is published. The SSH, Proxmox, Microsoft 365 and MikroTik
|
||||
# connectors all reach machines without it.
|
||||
#
|
||||
# AGENT_RELEASE_DIR is set by that overlay and should NOT be set here — a value in this file would
|
||||
# point the api at a path nothing populates, turning the honest 503 into a 404 per platform.
|
||||
|
||||
#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)
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
# Leave this empty and the instance runs the COMMUNITY EDITION: 1 organization, 3 users, 5 agents,
|
||||
# 10 assets, no expiry and nothing to renew. Every feature works inside
|
||||
# those numbers. A full limit refuses the NEXT thing of that kind and touches nothing already there.
|
||||
#
|
||||
# A key raises the limits — ask antoine@luxit.be for one. It is verified offline, on this machine. A
|
||||
# licensed instance then reports counts and its own address to your provider; the README lists every
|
||||
# field it sends. A key that expires or is withdrawn drops back to the allowances above, never to
|
||||
# nothing.
|
||||
#
|
||||
# This value SEEDS the database on first boot and does not govern it afterwards: a renewal arrives
|
||||
# through the check-in and is stored, so leaving an old key here cannot roll you back. To replace a
|
||||
# key, use Settings → Licence.
|
||||
#JARVIS_LICENSE_KEY=
|
||||
@@ -0,0 +1,13 @@
|
||||
# These files are read by Docker on a Linux host, whatever the machine that cloned them.
|
||||
#
|
||||
# Without this, a clone on Windows checks them out with CRLF, and the carriage return rides into
|
||||
# `.env` as part of a VALUE — Compose passes it through verbatim, so it ends up inside the database
|
||||
# password, the JWT secrets and the vault master key. The failure then surfaces as Postgres refusing
|
||||
# the connection, or as a vault that cannot decrypt what it wrote yesterday, with nothing anywhere
|
||||
# naming a line ending as the cause.
|
||||
* text=auto eol=lf
|
||||
|
||||
# And the screenshots are bytes, not text. `text=auto` above already detects that correctly, but
|
||||
# saying so costs one line and removes the question before somebody adds a JPEG.
|
||||
*.png binary
|
||||
*.jpg binary
|
||||
+12
@@ -0,0 +1,12 @@
|
||||
# Your instance's configuration, which holds every secret it has: the vault master key, the JWT
|
||||
# secrets, the database password and your AI provider's API key. NEVER commit it. `.env.example` is
|
||||
# the template and is the only one of these that belongs in a repository.
|
||||
.env
|
||||
.env.*
|
||||
!.env.example
|
||||
|
||||
# Database dumps, which the README tells you to take before an upgrade. They contain every
|
||||
# conversation, asset and audit row on the instance — and the vault's ciphertext, which is one
|
||||
# leaked .env away from being plaintext.
|
||||
*.sql
|
||||
*.sql.gz
|
||||
+573
@@ -0,0 +1,573 @@
|
||||
# Changelog
|
||||
|
||||
What each published build changes for you, newest first.
|
||||
|
||||
The three images are versioned independently and their numbers are not meant to match — usually only
|
||||
one of them changed. An entry says which artefact and which version, so `web 0.102.0` and
|
||||
`api 0.81.0` sitting under the same date is the ordinary case, not a mismatch.
|
||||
|
||||
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 — Two places still described the switch we had just removed `web 0.115.0`
|
||||
|
||||
The entry above says `JARVIS_TELEMETRY` was removed "from the compose file, from `.env.example`,
|
||||
from the licence screen and from the install wizard". Three of those four were true. **The licence
|
||||
screen still carried a line telling you to set `JARVIS_TELEMETRY=off` in `.env` to stop reporting**,
|
||||
in all three languages, and this README's community-edition table still listed reporting as
|
||||
"Switchable off". Both were wrong the moment that release shipped, and both are corrected here.
|
||||
|
||||
Nothing about the behaviour changes — there was never a switch behind those sentences to begin with.
|
||||
What changes is that the product and this document now say the same thing: every instance reports,
|
||||
the fields are listed below and on your own licence screen, and there is nothing to set.
|
||||
|
||||
## 2026-08-24 — Reporting is no longer optional `api 0.90.0` `web 0.114.0`
|
||||
|
||||
**`JARVIS_TELEMETRY` is gone, and with it the ability to stop your instance checking in.** Three days
|
||||
ago this repository told you the variable worked, and apologised for the fact it had not. We are
|
||||
withdrawing the option rather than leaving you to discover it stopped working: it is removed from the
|
||||
compose file, from `.env.example`, from the licence screen and from the install wizard, which is
|
||||
nine steps now instead of ten.
|
||||
|
||||
The reason is the one already written down for why a community instance reports at all. A free tier
|
||||
the publisher cannot see is a free tier nobody can warn — and the instances that opt out are exactly
|
||||
the ones most likely to be running an old build unattended, which is the population a warning is for.
|
||||
|
||||
**What has not changed is what leaves your network, or your ability to see it.** The field list below
|
||||
is complete and unchanged: 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. **Settings → Licence** shows you the address and
|
||||
the contents resolved by your own running instance, which is the answer that matters rather than
|
||||
anything this document says.
|
||||
|
||||
If that does not suit your deployment, tell us before you deploy — we would rather have the
|
||||
conversation than have you find out from a packet capture.
|
||||
|
||||
### Also in this release
|
||||
|
||||
**The sign-in field is off on a new installation.** The orb you press and hold to reveal the sign-in
|
||||
form is a deliberate piece of the product, and it is no longer what a brand-new instance greets you
|
||||
with — a first-time deployer meeting an unlabelled glowing hexagon has a puzzle rather than a login.
|
||||
Turn it on under **Settings → Platform → Sign-in**; instances that already have it keep it.
|
||||
|
||||
## 2026-08-24 — "Open Jarvis" sent you back to the install screen `web 0.113.0`
|
||||
|
||||
**If you installed on web 0.112.0, the last click of the wizard put you in a loop.** You finished,
|
||||
pressed **Open Jarvis**, and landed back on the install screen reading "This instance already has an
|
||||
administrator" — with a button to sign in that bounced you to the console, which sent you back to the
|
||||
install screen again. Nothing was wrong with your instance and nothing was lost; a reload of the page
|
||||
was enough to break out of it. It was still the worst possible moment for it, on the one screen a
|
||||
new deployer meets exactly once.
|
||||
|
||||
The console asks "does this instance need installing" once, when the page first loads, and it had no
|
||||
way to learn the answer had changed under it — so at the end of the wizard it was still acting on
|
||||
what it had been told before you started. The wizard now tells it.
|
||||
|
||||
Upgrade with the usual two lines:
|
||||
|
||||
```sh
|
||||
docker compose pull
|
||||
docker compose up -d
|
||||
```
|
||||
|
||||
## 2026-08-24 — The claim no longer expires `api 0.89.0` `web 0.112.0`
|
||||
|
||||
**The thirty-minute window on the install screen is gone.** There is no countdown, no "the window
|
||||
has closed", and no more restarting the api container to open another one. The first account created
|
||||
still owns the instance; it can now be created whenever you get to it.
|
||||
|
||||
**Which means the exposure is yours to manage, and we would rather say so than let a timer imply it.**
|
||||
Until somebody creates that first account, anyone who reaches this instance can. The gap that matters
|
||||
is between `docker compose up -d` and you opening a browser — so keep it short, and prefer pointing
|
||||
a public hostname at the instance _after_ you have installed rather than before.
|
||||
|
||||
Everything else about the claim is unchanged, including the part that protects you where a timer
|
||||
never did: an instance whose database has gone missing still costs the last characters of your vault
|
||||
master key to claim, because from the outside it is indistinguishable from a new one.
|
||||
|
||||
## 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 <antoine@luxit.be> 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
|
||||
answer, roughly what you need. Send it and your provider gets it with your instance's version and
|
||||
counts attached, so the conversation starts at "here is what fits" instead of "how many machines do
|
||||
you have".
|
||||
|
||||
**An approved licence installs itself.** The request is signed by your installation, so the licence
|
||||
is issued for that exact deployment and arrives on it at the next report — no key to copy, nothing to
|
||||
paste, and no way to paste it into the wrong instance. You get the key by email as well, for your
|
||||
records and in case you rebuilt the host while you waited. A decline comes back the same way, with
|
||||
your provider's reason on the licence screen and in your inbox. You can withdraw a request you no
|
||||
longer want, and ask again whenever something changes.
|
||||
|
||||
**Community instances now report, and this reverses what we told you this morning.** api 0.83.0 said
|
||||
— here, and in the README — that an instance without a licence contacts nobody at all. That is no
|
||||
longer true. A community instance now sends the same fields a licensed one does: counts, the version
|
||||
it runs, its contract checksum and its public address, to `checkin.luxit.be`. No names, no
|
||||
conversation content, nothing about what you administer; the README lists every field, and it is the
|
||||
same list as before.
|
||||
|
||||
Why: we could not tell whether the community edition had reached anybody, or which builds were
|
||||
running when we needed to warn people about one — and the request form above needs the same channel
|
||||
to work at all.
|
||||
|
||||
**`JARVIS_TELEMETRY=off` in `.env` stops it completely**, and costs you nothing else: every
|
||||
community allowance, every feature and every connected machine stay exactly as they are. What you
|
||||
lose is the licence request form, which has nowhere to send. A licensed instance reports regardless —
|
||||
that has always been part of what a licence is, and the licence screen says so.
|
||||
|
||||
If you would rather not report, set that variable before you upgrade. Nothing is sent until the
|
||||
instance's first check-in after start-up.
|
||||
|
||||
**On the licence screen**: what leaves your network, the exact address it goes to, and whether
|
||||
reporting is on at all — all three visible without opening a compose file.
|
||||
|
||||
## api 0.83.0 · web 0.104.0 — 2026-08-21
|
||||
|
||||
**Jarvis no longer needs a licence key to be useful.** An instance without one now runs the
|
||||
**community edition** — 1 organization, 3 users, 5 agents, 10 assets — perpetually, free, with every
|
||||
feature working and nothing reported to anybody. Install it, connect it to something real, and see
|
||||
what it does before there is anyone to talk to. A licence raises those limits; it does not switch the
|
||||
product on.
|
||||
|
||||
This replaces the behaviour of the last three days, where an instance with no key came up, accepted
|
||||
its first administrator account and then refused everything else. **If you installed Jarvis and found
|
||||
it would not let you create anything, this is the release that fixes it** — upgrade, and the
|
||||
allowances above apply immediately. Nothing needs to be reset, re-run or re-entered.
|
||||
|
||||
**A licence that stops applying now falls back to the same allowances**, instead of refusing every
|
||||
creation. Expired, withdrawn, or a key that will not verify: your instance keeps running everything
|
||||
already set up and may still create up to the community numbers. There is no state left in which
|
||||
Jarvis stops being usable over a billing question.
|
||||
|
||||
**Fixed: an unreadable licence key blocked every creation.** The licence screen said nothing was
|
||||
being withheld, and something was — a key that lost a character in a paste resolved every limit to
|
||||
zero. It now falls back to the community allowances, which is what the screen always claimed.
|
||||
|
||||
**On the licence screen**, a fresh install no longer shows a red "No licence" badge. It says
|
||||
Community edition, lists what that allows against what you use, and states plainly that nothing is
|
||||
reported. The refusal you get when a limit is full names the edition and what raises it.
|
||||
|
||||
Nothing else changes. No configuration, no migration, no new variable — `JARVIS_LICENSE_KEY` is now
|
||||
documented as optional, and it always could be left empty.
|
||||
|
||||
## api 0.82.0 · web 0.103.0 · agent 0.27.0 — 2026-08-21
|
||||
|
||||
**Nothing changes in what these builds do.** They are identical in behaviour to api 0.81.0,
|
||||
web 0.102.0 and agent 0.26.0, rebuilt so that every published image carries the same set of
|
||||
registry labels: what the image is, where its documentation lives, and a link to this file. The
|
||||
package page in the registry now says something useful to somebody who lands on it without
|
||||
context.
|
||||
|
||||
Upgrading is worthwhile only for that. If you are running the previous three and reading this,
|
||||
you already have everything they do.
|
||||
|
||||
**The earlier version tags have been removed from the registry.** Every build before these three
|
||||
is gone; the `stable`, `dev` and `latest` channels point at them. If you had pinned an exact
|
||||
older version in `.env`, that pin no longer resolves and `docker compose pull` will fail — pin
|
||||
one of the versions above instead, or drop the pin to follow `stable`.
|
||||
|
||||
## api 0.81.0 — 2026-08-21
|
||||
|
||||
**Approving a plan now actually authorises it.** When Jarvis asks for consent on a whole plan and you
|
||||
approve it, the steps that touch the machines the plan names are no longer confirmed one at a time.
|
||||
Previously the approval bound nothing at all — the assistant was asked to honour it and the executor
|
||||
never checked — so an approved plan still stopped at every step, and a read-only conversation could
|
||||
be granted a plan it had no way to carry out.
|
||||
|
||||
The clearance is deliberately narrow. It applies only in the conversation the plan was raised in,
|
||||
only to the machines the plan names, only up to a risk ceiling, and only for four hours. Anything
|
||||
outside it still asks. It can never be wider than the approver's own access to those machines.
|
||||
|
||||
**Two ways somebody could get more than they should, closed.**
|
||||
|
||||
- Approving used to lend the approver's own permissions to the plan for the rest of the turn. Work
|
||||
now runs as whoever asked for it, which is what the audit trail already claimed.
|
||||
- Deciding a plan asked nothing about whether the decider may touch the machines it names — the
|
||||
per-command confirmation had checked this for months and this path had not. It does now, for every
|
||||
machine in the plan. Refusing is never restricted: anyone who can see a plan is wrong may stop it.
|
||||
|
||||
**Wording that was not true.** A command that was waiting for a human when the server restarted used
|
||||
to be reported as possibly having taken effect, and told you to go and check the machine. It never
|
||||
ran — the confirmation happens before anything is sent — so it now says so.
|
||||
|
||||
If you have approval requests older than this release, they are recorded as expired. They were made
|
||||
under the old behaviour, where approving authorised nothing; treating them as live permissions would
|
||||
grant something nobody was asked for.
|
||||
|
||||
## web 0.102.0 — 2026-08-21
|
||||
|
||||
**The approval panel says what approving does.** It used to promise "Jarvis runs the steps below" and
|
||||
"Nothing runs until you decide". Neither was true: a pending plan blocks nothing, and approving did
|
||||
not run anything. It now describes the clearance you are actually granting — which machines, what
|
||||
ceiling, how long — and the rejection notice no longer claims that nothing ran.
|
||||
|
||||
The per-command confirmation is unchanged. That one does hold the command until you answer.
|
||||
|
||||
## api 0.80.0 · web 0.101.0 — 2026-08-21
|
||||
|
||||
**The footer says which channel this instance follows**, beside the version numbers, when you set
|
||||
`JARVIS_CHANNEL` in `.env`. Leave it empty if you pin exact versions — you then follow no channel,
|
||||
and the footer shows nothing rather than a label that stopped being true.
|
||||
|
||||
## api 0.78.2 · web 0.100.2 — 2026-08-21
|
||||
|
||||
**Distribution channels.** Images now carry `stable` and `dev` tags as well as their version.
|
||||
`stable` moves only after a build has run on the publisher's own instance; `dev` moves on every
|
||||
build. `latest` is a second name for `stable`, so nothing that already used it has to change, and
|
||||
the compose files default to `stable`.
|
||||
|
||||
A channel is a pointer and a version is a fact. Promotion copies the manifest of an image that has
|
||||
already been published and run — it never rebuilds — so the bytes you receive are the bytes that were
|
||||
tested, not a fresh build of the same source.
|
||||
|
||||
**Fixed: some builds reported their version as `dev`.** The version is stamped into the image and
|
||||
shown in the footer; a build published without it said `dev` while being tagged, pinned and run
|
||||
correctly. Affected api 0.78.0 and 0.78.1 and web 0.100.0 and 0.100.1.
|
||||
|
||||
## web 0.100.0 — 2026-08-21
|
||||
|
||||
**Recorded terminal sessions can be replayed** at the pace they happened, not only read as a finished
|
||||
transcript. Idle time is compressed so a session that was mostly waiting is watchable, and how much
|
||||
was skipped is stated. Sessions recorded before this release can still be read; they carry no timing,
|
||||
so they cannot be replayed, and the panel says so rather than hiding the control.
|
||||
|
||||
## agent 0.26.0 — 2026-08-21
|
||||
|
||||
**The command is now `jarvis`.** `jarvis-agent` keeps working and will continue to: both names are
|
||||
present on every machine, and an agent that updates itself gives itself the missing one. Uninstalling
|
||||
is a verb — `jarvis uninstall` — beside `status`, `start` and `stop`. The old
|
||||
`jarvis-agent-uninstall` command is unchanged.
|
||||
|
||||
`jarvis uninstall` refuses to run from inside a Jarvis terminal on the machine it is removing.
|
||||
Stopping the service would kill the command mid-way, leaving the agent stopped and nothing
|
||||
uninstalled, with no connection left to fix it through. Run it from an ordinary SSH session.
|
||||
|
||||
## agent 0.25.0 — 2026-08-21
|
||||
|
||||
Internal: the agent asks the machine which name its service is registered under rather than assuming
|
||||
one. No visible change; it is what makes a later rename survivable.
|
||||
|
||||
## agent 0.24.0 — 2026-08-21
|
||||
|
||||
**Remote terminal on macOS.** Interactive shells now work on managed Macs, as they already did on
|
||||
Linux and Windows. The session opens a login shell so the PATH matches what an administrator expects.
|
||||
|
||||
**Fixed: sessions were recorded as "closed" instead of "exited".** Typing `exit` ended the session
|
||||
correctly but filed it as though it had been taken away, with no exit code. Sessions from before this
|
||||
release keep the reason they were given.
|
||||
|
||||
**Fixed: a file descriptor and a thread leaked on every clean exit.** Only visible on machines that
|
||||
stay up for a long time with many sessions.
|
||||
@@ -0,0 +1,760 @@
|
||||
<p align="center">
|
||||
<img src="jarvis.svg" width="76" alt="Jarvis">
|
||||
</p>
|
||||
|
||||
<h1 align="center">Jarvis</h1>
|
||||
|
||||
<p align="center"><strong>AI-assisted infrastructure administration for MSPs.</strong></p>
|
||||
|
||||
An operator opens a conversation and asks for the work in words. Jarvis does it: a command over SSH
|
||||
or through an enrolled agent, a Proxmox guest powered on, a Microsoft 365 account blocked, a MikroTik
|
||||
firewall read back — on the machines of one client organization, with credentials it decrypts from
|
||||
the vault and shows nobody. Every call is judged before it runs, and the ones that cannot be undone
|
||||
stop and ask a human in the thread.
|
||||
|
||||
The difference from an assistant that writes commands for you to paste is that these run.
|
||||
|
||||
This repository runs Jarvis from published container images — no source, no build, no account with
|
||||
the project.
|
||||
|
||||
**And no licence key.** A fresh install comes up on the **community edition**: one organization,
|
||||
three users, five agents, ten assets, no expiry, nothing to renew. Everything Jarvis does works
|
||||
inside those numbers — every connector, the agent, the remote terminal, the assistant itself. A
|
||||
licence from your provider raises the limits; it does not switch anything on.
|
||||
|
||||
When you outgrow it, **ask for a licence from inside Jarvis** — Settings → Licence — and the key
|
||||
arrives on your instance by itself. A community instance reports its version and its counts so that
|
||||
we can see which builds are in the field; [what it sends](#licence-keys-and-what-your-instance-reports)
|
||||
is listed in full below.
|
||||
|
||||
- [What it does](#what-it-does)
|
||||
- [What decides whether a tool call runs](#what-decides-whether-a-tool-call-runs)
|
||||
- [What it looks like](#what-it-looks-like)
|
||||
- [How it works](#how-it-works)
|
||||
- [What you need](#what-you-need)
|
||||
- [Install](#install)
|
||||
- [Your reverse proxy has two requirements](#your-reverse-proxy-has-two-requirements)
|
||||
- [Upgrading](#upgrading)
|
||||
- [The agent](#the-agent)
|
||||
- [Things worth knowing before you trust it with production](#things-worth-knowing-before-you-trust-it-with-production)
|
||||
- [When it does not come up](#when-it-does-not-come-up)
|
||||
- [Backups, and restoring one](#backups-and-restoring-one)
|
||||
- [Removing it](#removing-it)
|
||||
- [The community edition](#the-community-edition)
|
||||
- [Licence keys, and what your instance reports](#licence-keys-and-what-your-instance-reports)
|
||||
- [Asking for a licence](#asking-for-a-licence)
|
||||
- [Getting a licence, and getting help](#getting-a-licence-and-getting-help)
|
||||
|
||||
## What it does
|
||||
|
||||
**Five connectors reach a managed system.** Each one is a set of tools the assistant may call, and
|
||||
what it may do with them is decided per call — see the next section.
|
||||
|
||||
| Connector | Reaches |
|
||||
| --- | --- |
|
||||
| **SSH** | Anything with a shell — Linux, Windows, and CLI-driven network gear. An appliance that serves no SFTP is offered the command tool alone, rather than four that would fail at the handshake. |
|
||||
| **Jarvis agent** | A machine running the enrolled agent, which dials out — so it works behind NAT, on a dynamic address, with no inbound rule and no SSH exposed. Commands, files, services, processes, system facts. |
|
||||
| **Proxmox VE** | The cluster API. Read and power only — no create, clone, snapshot, backup or migrate. |
|
||||
| **MikroTik RouterOS** | The native binary API, or the RouterOS 7 REST API. One identical tool surface either way; the connection decides the transport. |
|
||||
| **Microsoft 365** | Graph, app-only, on any Microsoft cloud, authenticating with a client secret or a certificate. Named tools for identity, licences, groups, admin roles, Exchange Online, Intune and the audit logs, plus one that reaches every remaining Graph endpoint. |
|
||||
|
||||
**An asset is the managed thing; a connection is a way of reaching it.** A Linux host commonly
|
||||
answers on OpenSSH *and* through an enrolled agent; a Proxmox node has a cluster API, a shell and an
|
||||
agent. Each route carries its own address, its own credentials and its own health — so a vendor API
|
||||
that stops answering no longer makes a device unmanageable while its console is up.
|
||||
|
||||
**Documents.** A conversation accumulates what the assistant writes: reports and runbooks in
|
||||
Markdown, diagrams in Mermaid, tabular data as a workbook. What it writes is always a *source*, and
|
||||
Jarvis renders the file on download — PDF and Word from prose, Excel and CSV from a workbook, HTML
|
||||
from either. Keeping the source is what leaves a document revisable instead of a dead binary.
|
||||
|
||||
**And what an MSP has to administer about itself**: organizations and members under a role ceiling,
|
||||
invitations, an encrypted vault with its own folder tree and its own grants, an audit trail,
|
||||
passkeys and TOTP, a notification centre, and a console in English, French or German — a property of
|
||||
the account, not of the browser.
|
||||
|
||||
## What decides whether a tool call runs
|
||||
|
||||
Three things, resolved on every single call. The most restrictive wins.
|
||||
|
||||
**Risk is a property of the operation.** A tool declares a baseline and may escalate per invocation:
|
||||
a shell command is mutating in general and destructive for `rm -rf`, `mkfs`, `shutdown`,
|
||||
`iptables -F` and about two dozen other patterns. Escalation is one-way — a per-call assessment can
|
||||
raise the risk, never lower it.
|
||||
|
||||
**Autonomy is a property of the conversation**, chosen by the operator:
|
||||
|
||||
| | Safe | Mutating | Destructive |
|
||||
| --- | --- | --- | --- |
|
||||
| **Read-only** | run | refuse | refuse |
|
||||
| **Ask before every change** | run | ask | ask |
|
||||
| **Ask before destructive changes** (default) | run | run | ask |
|
||||
| **Full access** | run | run | run |
|
||||
|
||||
"Ask" raises an approval request in the conversation, and the run parks until a human answers. The
|
||||
level is re-read on every call, so lowering it takes effect on the very next tool call of a run
|
||||
already in flight. **Full access removes the last in-chat gate for every participant** — destructive
|
||||
operations then run immediately, with no prompt and no second pair of eyes. Make it a deliberate
|
||||
choice.
|
||||
|
||||
**Permission is a property of the person.** A grant says which slice of the asset tree somebody may
|
||||
operate, through which connectors, up to which risk, and whether the tools whose operation the
|
||||
assistant *composes* — a shell command, an arbitrary Graph request, the contents of a file — are
|
||||
admitted at all. Grants resolve by walking outward from the asset: the most specific level that says
|
||||
anything decides entirely, and if nothing has spoken by the root the answer is no. Absent means
|
||||
nothing, so a forgotten grant fails closed rather than open.
|
||||
|
||||
> The conversation's autonomy is a **floor the operator imposes on themselves**; the grant is the
|
||||
> **ceiling imposed on them**. What runs is whichever binds.
|
||||
|
||||
A conversation also carries a **scope** — any mix of assets and folders, a folder granting its whole
|
||||
subtree. Scope is checked *before* the asset is resolved, so a machine out of scope never has its
|
||||
vault secrets decrypted.
|
||||
|
||||
## What it looks like
|
||||
|
||||
<p align="center">
|
||||
<img src="docs/img/console.png" alt="The Jarvis console, on a freshly installed instance" width="900">
|
||||
</p>
|
||||
|
||||
The console on a fresh install. The footer names the two builds you are running, which is the first
|
||||
thing to quote when something is wrong.
|
||||
|
||||
<p align="center">
|
||||
<img src="docs/img/asset.png" alt="One asset, its two routes, its agent and its inventory" width="900">
|
||||
</p>
|
||||
|
||||
One asset, and the distinction the model rests on: **two routes to the same machine**, each with its
|
||||
own address, its own credentials and its own health. One is preferred and untested; the other is
|
||||
disabled without being deleted, so its settings and its history survive and no tool may use it.
|
||||
|
||||
## How it works
|
||||
|
||||
Four containers. **Only `web` publishes a port**: its nginx serves the console and reverse-proxies
|
||||
`/api` and the websocket to `api` over the internal network, so your TLS terminator has exactly one
|
||||
target and the API is never reachable from outside the compose network.
|
||||
|
||||
| Service | What it is |
|
||||
| --- | --- |
|
||||
| `web` | nginx serving the React console. The only published port. |
|
||||
| `api` | REST, auth, the vault, the connectors, the tool-calling loop, and the websocket. |
|
||||
| `postgres` | Everything except the vault master key. |
|
||||
| `redis` | Socket fan-out, nonces and rate-limit counters. |
|
||||
|
||||
The assistant streams a turn, executes the tool calls the model asked for **strictly after the
|
||||
stream is fully drained**, then feeds the results back — up to fifty rounds per user turn. That
|
||||
ordering is what makes retrying a broken stream safe: at the moment a stream fails, no tool of that
|
||||
round has run, so replaying it re-generates intent and never re-runs an operation.
|
||||
|
||||
**A run that was in flight when the api stopped is picked up when it comes back.** A shutdown aborts
|
||||
each loop and lets it write out what it had streamed with a note saying why the transcript ends
|
||||
there; the next process finishes that message, closes any tool call whose outcome is unknown saying
|
||||
in as many words that it is unknown, and resumes the run — instructed to read the current state
|
||||
before repeating anything that writes. This is why the api asks for a stop grace period, and part of
|
||||
why it must run as a single replica.
|
||||
|
||||
**The agent dials out.** Nothing inbound is opened on a managed machine. It holds a websocket to
|
||||
your instance, signs each session with a key whose private half never leaves it, and reports its
|
||||
inventory on every heartbeat.
|
||||
|
||||
## What you need
|
||||
|
||||
- Docker with Compose v2, on **x86-64 Linux**. The api and web images are published for
|
||||
`linux/amd64` only, so an arm64 host — a Pi, an Ampere, a Graviton — fails at `docker compose pull`
|
||||
with no matching manifest. The machines you *administer* have no such limit: the agent ships arm64
|
||||
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 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.
|
||||
|
||||
## Install
|
||||
|
||||
```sh
|
||||
curl -O https://git.luxit.be/Luxit/jarvis-selfhost/raw/branch/main/docker-compose.yml
|
||||
docker compose up -d
|
||||
```
|
||||
|
||||
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.
|
||||
|
||||
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.
|
||||
|
||||
Everything it asks is a setting you can change afterwards from an ordinary screen. Nothing it asks
|
||||
needs a container restart to change.
|
||||
|
||||
### Install it when you deploy it
|
||||
|
||||
**Until somebody creates the first account, anyone who reaches this instance can.** There is no
|
||||
deadline on that and no token to find: the first account created owns the instance, and the claim is
|
||||
open for as long as nobody has taken it.
|
||||
|
||||
So the gap that matters is between `docker compose up -d` and you opening a browser. Keep it short.
|
||||
If you are pointing a public hostname at this, point it after you have installed, or install through
|
||||
the host's own address first — the setting is editable afterwards.
|
||||
|
||||
If you would rather it were not open at all until you say so, do not publish the address yet: the
|
||||
install screen is served by the same web container as everything else, and a Jarvis nobody can reach
|
||||
is a Jarvis nobody can claim.
|
||||
|
||||
**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 one the claim 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
|
||||
|
||||
Both are the kind that produce confusing symptoms rather than clean errors.
|
||||
|
||||
- **Forward the WebSocket upgrade.** Two separate sockets ride `JARVIS_PORT`: the chat, on
|
||||
`/socket.io/`, and enrolled agents, on `/api/agents/ws`. Neither falls back to plain HTTP. Without
|
||||
the upgrade the chat does not lose streaming — it never connects at all, and since the prompt
|
||||
itself travels over that socket, nothing sends. Presence and in-chat approvals go with it, and no
|
||||
agent can connect.
|
||||
- **Give it a long read timeout** — 300s or so. A reasoning model can go 90+ seconds without emitting
|
||||
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. 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
|
||||
|
||||
```sh
|
||||
docker compose pull && docker compose up -d
|
||||
```
|
||||
|
||||
That is the whole upgrade: the api, the web, and — with the agent overlay on — the agent release all
|
||||
track the **`stable`** channel by default. Postgres and Redis are not on a Jarvis channel; they
|
||||
follow their own upstream tags, `postgres:16-alpine` and `redis:7-alpine`. Schema changes apply
|
||||
themselves when the api starts, and the api and the web are versioned independently — their numbers
|
||||
are not meant to match, because usually only one side changed.
|
||||
|
||||
**A schema change is one-way.** Jarvis has no migration history: each boot force-syncs the database
|
||||
to the schema its image carries, adding what a release added and dropping what it removed, without
|
||||
prompting. Pulling an older api image does not undo that — unlike the image itself, the schema stays
|
||||
where the newer build left it. Restoring a dump is the only way back, which is what makes the one
|
||||
below a prerequisite rather than a precaution.
|
||||
|
||||
```sh
|
||||
docker compose exec -T postgres pg_dump -U jarvis jarvis | gzip > jarvis-$(date +%F).sql.gz
|
||||
```
|
||||
|
||||
### Channels
|
||||
|
||||
| Channel | What it means |
|
||||
| -------- | ---------------------------------------------------------------------------------- |
|
||||
| `stable` | Promoted after running on the publisher's own instance. **The default, and what you want.** |
|
||||
| `dev` | Every build, as soon as it is published. Nothing has tried it yet. |
|
||||
| `latest` | A second name for `stable`, kept so nothing that already used it has to change. |
|
||||
|
||||
A channel is a **pointer** and a version number is a **fact**. `0.78.2` means one specific set of
|
||||
bytes for ever; `stable` means whichever set we currently stand behind, and it moves. A build only
|
||||
reaches `stable` by being promoted — and promotion copies the manifest of an image that has already
|
||||
been published and already run. It never rebuilds, so the bytes you receive are the same bytes that
|
||||
were tested, not a fresh build of the same source.
|
||||
|
||||
**Channels do not mean the app forgets which build it is.** The version is stamped into the image
|
||||
when it is built, so the footer in the app, `/version.json` and the agent manifest keep reporting the
|
||||
real number whichever name you pulled it under. That is what lets you tell somebody which build you
|
||||
are on when something goes wrong. Set `JARVIS_CHANNEL` in `.env` and the footer names your channel
|
||||
beside those numbers; leave it empty if you pin, because then you follow no channel.
|
||||
|
||||
Once you are in production, consider pinning: set `JARVIS_IMAGE_API`, `JARVIS_IMAGE_WEB` and
|
||||
`JARVIS_IMAGE_AGENT` in `.env` to explicit version tags. It makes an upgrade a decision rather than a
|
||||
side effect of pulling. Pin the version you are *already running* — the footer shows it — rather than
|
||||
one copied from a document, and remember that moving the api pin backwards runs an old build against
|
||||
a schema that has already moved forward.
|
||||
|
||||
**Pin the agent one first if you pin only one.** The api and the web change what your own server
|
||||
runs; the agent changes what runs on every machine you administer. Pinning the image decides which
|
||||
build your instance publishes; a separate **update policy** decides when a machine takes it —
|
||||
*As soon as available*, *On a schedule*, *Manually only*, *Let the agent decide* (the default, which
|
||||
means on its next service start, and on a server that can be months) or *Never*. The instance-wide
|
||||
answer is on **Settings → Platform → General**; an organization overrides it under **Settings →
|
||||
Organization → Agent updates**, and a single machine overrides that. An enrolled agent also refuses
|
||||
any version that is not strictly newer, so moving that pin back stops a rollout rather than reversing
|
||||
it on machines that already took the update.
|
||||
|
||||
## The agent
|
||||
|
||||
**The Jarvis agent is an optional overlay, off by default.** Enrolling a machine downloads a compiled
|
||||
binary that the api serves from `AGENT_RELEASE_DIR`, and a compose-only deployment has no way to
|
||||
produce one. `docker-compose.agent.yml` supplies it as a pullable image instead. Leaving it off is a
|
||||
supported state rather than a broken one: everything else works, the manifest and download endpoints
|
||||
answer 503 saying no build is published, and the SSH, Proxmox, Microsoft 365 and MikroTik connectors
|
||||
all reach machines without it.
|
||||
|
||||
Turn it on by adding one line to `.env`, so that every later `docker compose` command picks up both
|
||||
files with no extra flags:
|
||||
|
||||
```sh
|
||||
COMPOSE_FILE=docker-compose.yml:docker-compose.agent.yml
|
||||
```
|
||||
|
||||
then `docker compose pull && docker compose up -d`. A one-shot `agent-releases` service copies the
|
||||
release into a volume the api reads, and exits. From there, the **Agents** page in the sidebar issues
|
||||
the install command: click **New install command**, pick Linux, macOS or Windows, and copy the one
|
||||
line. (Settings → Organization → Agent updates is a different screen — it schedules how
|
||||
already-enrolled agents take new builds.)
|
||||
|
||||
Four things worth knowing about it:
|
||||
|
||||
- **Until that publisher exits cleanly, the api does not start.** That is deliberate — a release that
|
||||
failed to arrive should stop the deploy loudly rather than leave you handing 404s to every installer
|
||||
you run this week. The cost is that an unreachable registry blocks the whole stack. The comment in
|
||||
the file names the three lines to drop if you would rather it degraded quietly.
|
||||
- **Upgrading it does not restart anything.** The api re-checks the file on disk on every download
|
||||
request and re-hashes it whenever its size or timestamp has changed, so a new release in the volume
|
||||
is served immediately and the published checksum always describes the bytes actually being served.
|
||||
- **The agent version is its own number.** It moves independently of the api and the web, and a Jarvis
|
||||
release usually does not touch it at all. The app footer does not carry it either — each enrolled
|
||||
machine reports the build it runs, on the Agents page. Pin it with `JARVIS_IMAGE_AGENT`.
|
||||
- **It carries an interactive shell.** Beyond what the assistant can do with it, a person gets a real
|
||||
terminal on an enrolled machine from the browser — Linux, macOS and Windows alike. Sessions are
|
||||
recorded by default, encrypted under `VAULT_MASTER_KEY` and deleted on a retention policy you set
|
||||
under **Settings → Organization → Terminal sessions**. Turning recording off stops the transcript,
|
||||
never the audit entry.
|
||||
|
||||
**What an enrolled agent can do.** Inventory the machine, run commands, read, write and fetch files,
|
||||
list and control services, list processes, update itself, and carry that shell. It runs as root on
|
||||
Linux and macOS and as LocalSystem on Windows, deliberately — its purpose is to administer the
|
||||
machine.
|
||||
|
||||
What the **assistant** does with that reach is bounded by the autonomy policy and the approval gates
|
||||
above. **The interactive shell is not.** There is no command to inspect before a shell opens, so the
|
||||
risk ceiling has nothing to weigh; it is gated instead by a permission and a per-machine switch on
|
||||
the grant. Decide who holds those before enrolling anything you care about.
|
||||
|
||||
## Things worth knowing before you trust it with production
|
||||
|
||||
These are deliberate and documented rather than surprises waiting to be found.
|
||||
|
||||
- **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
|
||||
websocket of every enrolled agent live in one process's memory. The Redis in this stack does not
|
||||
lift that limit: it fans outgoing events out to other replicas, but an incoming one is only ever
|
||||
handled by the replica holding that connection. So a cancel or an approval answered on the wrong
|
||||
replica is silently dropped, and an agent tool call can land on a replica that does not hold the
|
||||
target machine. Worse, a starting replica's recovery sweep claims every run it does not own — so a
|
||||
second instance re-executes, against your real infrastructure, operations the first is still
|
||||
running.
|
||||
- **There is no rate limiting on sign-in.** No throttler, no account lockout, and authentication
|
||||
events are not audited. Credential stuffing is bounded only by the reverse proxy you put in front,
|
||||
which this repository does not ship. If your proxy can rate-limit one route, make it that one.
|
||||
- **An access token dies with its session, with one exception.** Every access token carries the id of
|
||||
the session that issued it and every request re-checks that the session is live, so revoking a
|
||||
session, signing other devices out or deactivating an account cuts that token off on its next
|
||||
request. The exception is changing your own password: it revokes every *other* session and keeps
|
||||
the one you are changing it from, so a token stolen from that session stays valid until it expires
|
||||
— 15 minutes by default (`JWT_ACCESS_TTL`). An admin-forced reset drops every session.
|
||||
- **The api container runs as root**, and so does nginx's master process in the web container,
|
||||
though its workers drop privileges. Neither image declares a `USER`.
|
||||
- **`/api/health` answers 200 with `status: "degraded"`** when the database is unreachable, so the
|
||||
container healthcheck alone is not a liveness signal for the database.
|
||||
- **Outbound mail is Microsoft Graph only.** There is no SMTP and no environment variable for any of
|
||||
it: an app registration with `Mail.Send` and a shared mailbox, set up on the **Mail** tab of
|
||||
**Settings → Platform → General**. Without it, invitations still work — the link comes back to the
|
||||
admin who created it instead of being emailed. Nothing else is emailed: there is no password reset
|
||||
and no address verification.
|
||||
- The assistant executes real operations on real infrastructure. What stops for a human is the
|
||||
conversation's autonomy level, and at *Full access* nothing does. See
|
||||
[What decides whether a tool call runs](#what-decides-whether-a-tool-call-runs).
|
||||
|
||||
## When it does not come up
|
||||
|
||||
```sh
|
||||
docker compose ps # who is running, and who is restarting
|
||||
docker compose logs -f api # the api says why it refused to start
|
||||
```
|
||||
|
||||
**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 |
|
||||
| --- | --- |
|
||||
| Every page redirects to an install screen | This instance has not been installed yet. That is the wizard, not an error. |
|
||||
| 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. |
|
||||
| An agent installer answers 503 | No agent build is published — the overlay is off. That is a supported state. |
|
||||
|
||||
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\" = '<the \$argon2id\$… string>', \"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.** The claim is open only on an instance with
|
||||
no users and no completed setup, so on a working deployment it refuses — 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. **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 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:
|
||||
|
||||
```sh
|
||||
docker compose stop api web
|
||||
docker compose exec -T postgres psql -U jarvis -d postgres \
|
||||
-c 'DROP DATABASE IF EXISTS jarvis;' -c 'CREATE DATABASE jarvis;'
|
||||
gunzip -c jarvis.sql.gz | docker compose exec -T postgres psql -U jarvis -d jarvis
|
||||
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=<the base64 string you saved>' >> .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
|
||||
docker compose down # stops everything, keeps the data
|
||||
docker compose down -v # also deletes the volumes — every conversation, asset and credential
|
||||
```
|
||||
|
||||
`down -v` is not recoverable from anything but a dump you already took.
|
||||
|
||||
## The community edition
|
||||
|
||||
**Jarvis runs without a licence key, for as long as you like.** An instance with no key runs the
|
||||
community edition:
|
||||
|
||||
| | Community edition |
|
||||
| --- | --- |
|
||||
| Organizations | 1 |
|
||||
| Users | 3 |
|
||||
| Agents | 5 |
|
||||
| Assets | 10 |
|
||||
| Term | Perpetual. No expiry, nothing to renew, no key to lose |
|
||||
| Reporting | Version, counts and address — the same fields a licensed instance sends, and not optional |
|
||||
| Features | All of them — every connector, the agent, the remote terminal, the assistant |
|
||||
|
||||
Nothing is disabled, watermarked or time-limited. What a licence buys is a **higher ceiling**, not
|
||||
the product: the same images, the same code, larger numbers. This is deliberate and it is the reason
|
||||
the images are public — you should be able to install Jarvis, connect it to something real, and find
|
||||
out whether it earns a place in your work before there is anybody to talk to.
|
||||
|
||||
When a limit is full, Jarvis refuses **the next** thing of that kind and says which limit it was.
|
||||
Nothing already there is touched: no organization is closed, no user locked out, no agent
|
||||
disconnected, and the assistant keeps working on everything you have. So an instance that grows past
|
||||
the community numbers — through an imported estate, say — keeps running in full; it simply cannot
|
||||
add to that meter until a key raises it.
|
||||
|
||||
### What changed on 21 August 2026, and why we are saying it loudly
|
||||
|
||||
**Until this release, a community instance contacted nobody at all**, and this file said so in those
|
||||
words. That is no longer true: an instance without a licence now reports the same fields a licensed
|
||||
one does — counts, version, contract checksum and its public address — to `checkin.luxit.be`, which
|
||||
is compiled into the build because an instance with no licence has no address in one to read.
|
||||
|
||||
We changed it for two reasons, and neither of them is nicer for you than for us. We could not tell
|
||||
whether the community edition was reaching anybody, or which builds were running when we needed to
|
||||
warn people about one. And the licence request below needs the same channel — a form that could not
|
||||
reach us would just be an email with extra steps.
|
||||
|
||||
**Reporting is part of running Jarvis, and there is no switch.** Every instance checks in —
|
||||
community or licensed, the same fields either way — because it is the only way we see which builds
|
||||
are in the field when one of them turns out to need a warning, and because the licence request form
|
||||
below travels the same channel. What leaves is listed field by field in the next section, and
|
||||
**Settings → Licence** shows you the exact address and the exact contents from the running instance
|
||||
rather than from this document.
|
||||
|
||||
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.
|
||||
|
||||
To go further — more organizations for a real client base, more seats, more machines — ask for a
|
||||
licence from **Settings → Licence** inside your own instance. That form is signed by your
|
||||
installation, so an approved licence lands on it by itself, with nothing to paste.
|
||||
|
||||
## Licence keys, and what your instance reports
|
||||
|
||||
**A licence key raises the limits.** Ask for one (see
|
||||
[below](#getting-a-licence-and-getting-help)) and put it in `.env` as `JARVIS_LICENSE_KEY`, or paste
|
||||
it under **Settings → Licence**. The key is a signed token your instance verifies **offline** — it
|
||||
carries your term and your limits, and it needs no network to be checked.
|
||||
|
||||
**A licence that stops applying drops back to the community edition**, and never to nothing. Expired,
|
||||
withdrawn, or a key that will not verify: the instance keeps every allowance in the table above and
|
||||
keeps running everything already set up. There is no state in which Jarvis stops being usable because
|
||||
of a billing question, and there never will be.
|
||||
|
||||
**Every instance reports**, licensed or not. A licensed one reports to the address written into its
|
||||
key; a community one to `checkin.luxit.be`, compiled into the build. The interval is your provider's
|
||||
to set — every ten minutes on the current arrangement, so that a renewal or a revocation reaches you
|
||||
promptly rather than tomorrow.
|
||||
|
||||
This is everything either of them sends, in full:
|
||||
|
||||
| Field | What it is |
|
||||
| ------------------------ | ------------------------------------------------- |
|
||||
| Product | The literal string `jarvis` |
|
||||
| Licence id | Which licence this is. **Absent on a community instance** — there is none to quote |
|
||||
| Instance id + public key | A key pair your instance generated, identifying it |
|
||||
| Version | Which Jarvis build you are running |
|
||||
| Contract version | Which set of limits this build understands — a checksum, not a document |
|
||||
| Counts | How many organizations, users, agents and assets |
|
||||
| Public URL | Your instance's address — always sent, see below |
|
||||
| Timestamps | When the process started, and when it reported |
|
||||
| Signature + nonce | Proof the message came from this instance, and a one-time value so an old one cannot be replayed. Carries nothing about you. |
|
||||
|
||||
**Counts, not contents.** No names, no email addresses, no conversation text, no asset inventory, no
|
||||
credentials, nothing about what you administer.
|
||||
|
||||
**The public URL is the one field that names your network rather than measuring something, and it is
|
||||
sent.** It used to be a switch on the licence screen; it is not any more, because an installation the
|
||||
publisher can identify only by a fingerprint is one where "which of these is the customer calling
|
||||
about" has no answer. The screen shows you the exact address that leaves, under **Settings →
|
||||
Licence**.
|
||||
|
||||
**There is no way to switch this off**, on a community instance or a licensed one. It is the same
|
||||
report either way, and the licence screen states it rather than offering a control that would make
|
||||
the arrangement a matter of opinion. If that does not suit your deployment, take it up with us before
|
||||
you deploy anything.
|
||||
|
||||
The reply can carry a renewed key, which your instance adopts on its own — so a renewal reaches you
|
||||
without anybody re-pasting anything.
|
||||
|
||||
**Your platform does not stop working because of a licence.** Expiry gives you a grace period — as
|
||||
long as your key says, which on the current plans is 30 days, and none at all on a trial. After it,
|
||||
the instance returns to the community edition's allowances: everything already set up keeps running,
|
||||
the assistant included, and only creating something beyond those numbers is refused. There is no
|
||||
state in which Jarvis disables, deletes or locks you out of something you are already using. If the
|
||||
check-in cannot reach the server, nothing changes at all: the key you hold is what governs, and it is
|
||||
checked without a network.
|
||||
|
||||
## Asking for a licence
|
||||
|
||||
**From inside your own instance: Settings → Licence → Ask for a licence.** Fill in who you are — a
|
||||
company name and somebody to answer are the only required fields — roughly what you need, and send.
|
||||
|
||||
What makes this worth doing from in there rather than by email: the form is **signed by your
|
||||
installation**, so when your provider approves it, the licence is issued for that exact deployment
|
||||
and **lands on it by itself at the next report**. There is no key to copy, nothing to paste, and no
|
||||
way to paste it into the wrong instance. You get the key by email as well, for your records and in
|
||||
case you rebuild the host before the answer arrives.
|
||||
|
||||
Sent with the form: your instance's id, its version, what it currently counts, and its address. That
|
||||
is the same information it already reports, so the form adds only what you typed.
|
||||
|
||||
While it is pending, the licence screen says so. If your provider declines, their reason appears on
|
||||
that screen and in your inbox — and you can ask again from the same button whenever something has
|
||||
changed. You can also withdraw a request you no longer want.
|
||||
|
||||
If your instance cannot reach us at all — an outbound firewall, an air gap — the form says so
|
||||
rather than failing quietly. Write to the address below instead, quoting the instance id from the
|
||||
licence screen.
|
||||
|
||||
## Getting a licence, and getting help
|
||||
|
||||
**Antoine Cavelier — <antoine@luxit.be>.** Licence keys, pricing, and anything wrong with the
|
||||
product.
|
||||
|
||||
When something is broken, the two facts worth putting in the first message are the build you are on
|
||||
and what the api said:
|
||||
|
||||
```sh
|
||||
curl -s https://your-jarvis.example.com/version.json # the web and api versions
|
||||
docker compose logs --tail=100 api
|
||||
```
|
||||
|
||||
`/version.json` is public on purpose, so you can quote it without signing in.
|
||||
|
||||
## Licence
|
||||
|
||||
The images are provided as-is with no warranty, no support and no commitment to future availability.
|
||||
The source is not public and no rights to it are granted. Ask before deploying this commercially or
|
||||
for third parties.
|
||||
@@ -0,0 +1,45 @@
|
||||
# Jarvis — the agent release, as a pullable image.
|
||||
#
|
||||
# OPTIONAL OVERLAY. The base stack runs perfectly without it; what it adds is the one feature a
|
||||
# self-hosted instance cannot otherwise have, because enrolling a machine downloads a compiled
|
||||
# binary and there is nowhere for a compose-only deployment to get one. This carries that release
|
||||
# as an OCI image, so it arrives through the same `docker compose pull` as the api and the web.
|
||||
#
|
||||
# Turn it on by naming both files. Either spelling works:
|
||||
#
|
||||
# docker compose -f docker-compose.yml -f docker-compose.agent.yml up -d
|
||||
#
|
||||
# or, so that a plain `docker compose ...` keeps working for every later command, put this in .env:
|
||||
#
|
||||
# COMPOSE_FILE=docker-compose.yml:docker-compose.agent.yml
|
||||
#
|
||||
# Upgrading is unchanged: `docker compose pull && docker compose up -d`. The publisher re-runs,
|
||||
# replaces the release in the volume, and the api picks it up WITHOUT a restart — digests are
|
||||
# computed from the bytes on disk on every request, not cached at boot.
|
||||
services:
|
||||
# Runs once per `up`, copies its payload into the shared volume, exits. Not a server.
|
||||
agent-releases:
|
||||
image: ${JARVIS_IMAGE_AGENT:-git.luxit.be/luxit/jarvis-agent-dist:stable}
|
||||
# Explicit, because the default would be wrong the moment somebody copies this block: a
|
||||
# restarting one-shot is an infinite loop, and Compose's own default policy is already "no".
|
||||
restart: "no"
|
||||
volumes:
|
||||
- agent_releases:/out
|
||||
|
||||
api:
|
||||
# NOTE THE COUPLING: until the publisher has exited 0, the api does not start. That is
|
||||
# deliberate — a release that failed to arrive should stop the deploy and say so, rather than
|
||||
# leave an instance quietly handing 404s to every installer somebody runs this week. The cost
|
||||
# is that an unreachable registry now blocks the whole stack, so if that trade is wrong for
|
||||
# you, drop these three lines and the api will simply serve no build until the volume fills.
|
||||
depends_on:
|
||||
agent-releases:
|
||||
condition: service_completed_successfully
|
||||
volumes:
|
||||
# Read-only: the API serves these bytes to every managed machine and never writes here.
|
||||
- agent_releases:/srv/agent-releases:ro
|
||||
environment:
|
||||
AGENT_RELEASE_DIR: /srv/agent-releases
|
||||
|
||||
volumes:
|
||||
agent_releases:
|
||||
@@ -0,0 +1,240 @@
|
||||
# Jarvis — self-hosting stack.
|
||||
#
|
||||
# Everything runs from published images; nothing is built here and no source is needed.
|
||||
#
|
||||
# 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.
|
||||
#
|
||||
# UPGRADING: `docker compose pull && docker compose up -d`. Every Jarvis image here tracks `stable` by
|
||||
# default, so that is the whole upgrade. Postgres and Redis are not on a Jarvis channel — they follow
|
||||
# their own upstream tags. Schema changes apply themselves when the api starts, and they are ONE-WAY:
|
||||
# there is no migration history, so pulling an older api image does not put the schema back. Take a
|
||||
# dump first. See the README.
|
||||
#
|
||||
# A channel tag does NOT mean the app forgets which build it is: `stable` is a second name on the same
|
||||
# image as its version tag, and the version is stamped INTO the image when it is built. The footer in
|
||||
# the app and /version.json keep reporting the real number whichever name you pulled it under — which
|
||||
# is what lets you tell somebody which build you are on when something goes wrong.
|
||||
#
|
||||
# To pin instead — recommended once you are in production, because it makes an upgrade a decision rather
|
||||
# than a side effect of pulling — set JARVIS_IMAGE_API and JARVIS_IMAGE_WEB (and JARVIS_IMAGE_AGENT, if
|
||||
# you run the agent overlay) in .env to explicit version tags.
|
||||
#
|
||||
# No version number is written in this comment on purpose. Nothing in the publishing path would ever
|
||||
# bump one, so a number here is a number that goes stale while nobody is looking.
|
||||
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
|
||||
# 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
|
||||
timeout: 5s
|
||||
retries: 10
|
||||
|
||||
redis:
|
||||
image: redis:7-alpine
|
||||
restart: unless-stopped
|
||||
# Append-only so a restart does not lose the queue and the socket fan-out state.
|
||||
command: ["redis-server", "--appendonly", "yes"]
|
||||
volumes:
|
||||
- redis_data:/data
|
||||
healthcheck:
|
||||
test: ["CMD", "redis-cli", "ping"]
|
||||
interval: 5s
|
||||
timeout: 3s
|
||||
retries: 10
|
||||
|
||||
api:
|
||||
image: ${JARVIS_IMAGE_API:-git.luxit.be/luxit/jarvis-api:stable}
|
||||
restart: unless-stopped
|
||||
# The assistant runs long operations, and a deploy is the most common thing that interrupts one.
|
||||
# Given room to stop, the API aborts each loop, writes the partial answer with a note saying why
|
||||
# the transcript ends there, and marks the run interrupted so the next process picks it up.
|
||||
# Docker's 10s default is not enough. RUN_SHUTDOWN_GRACE_SEC must stay the smaller of the two.
|
||||
stop_grace_period: 60s
|
||||
depends_on:
|
||||
postgres:
|
||||
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"
|
||||
|
||||
# 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:-}
|
||||
|
||||
# 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
|
||||
|
||||
# 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}
|
||||
|
||||
# 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. All four of these are seeds for the stored settings.
|
||||
OPENAI_BASE_URL: ${OPENAI_BASE_URL:-https://api.openai.com/v1}
|
||||
# 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}
|
||||
|
||||
# How many proxies sit in front and rewrite X-Forwarded-For. ONE is the web container's own
|
||||
# nginx, which is always there — so 1 is right when nothing else fronts it, and 2 when your own
|
||||
# TLS terminator does. Raising it is the dangerous direction: the API trusts that many hops of a
|
||||
# header the client can forge, and too high lets a caller choose the IP that lands in the audit
|
||||
# log, in the session list and in the enrollment rate limit.
|
||||
TRUST_PROXY_HOPS: ${TRUST_PROXY_HOPS:-2}
|
||||
|
||||
# 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:-}
|
||||
|
||||
# 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
|
||||
# decision — and the footer shows nothing rather than a label that stopped being true.
|
||||
#
|
||||
# It is not baked into the image, and cannot be: a channel is decided after a build and moves
|
||||
# afterwards, so the same image is `dev` one week and `stable` the next. Only you know which
|
||||
# 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}
|
||||
|
||||
# Where the agent binaries live, if you have them. Leaving this unset is a supported state:
|
||||
# everything except the agent installer works, and the installer answers 503 saying no build is
|
||||
# published. See the README — a self-hosted instance has no way to produce these.
|
||||
AGENT_RELEASE_DIR: ${AGENT_RELEASE_DIR:-}
|
||||
healthcheck:
|
||||
test:
|
||||
- CMD
|
||||
- node
|
||||
- -e
|
||||
- "fetch('http://localhost:4000/api/health').then(r=>process.exit(r.ok?0:1)).catch(()=>process.exit(1))"
|
||||
interval: 10s
|
||||
timeout: 5s
|
||||
retries: 12
|
||||
# First boot syncs the schema and runs the data backfills before it listens.
|
||||
start_period: 40s
|
||||
|
||||
web:
|
||||
image: ${JARVIS_IMAGE_WEB:-git.luxit.be/luxit/jarvis-web:stable}
|
||||
restart: unless-stopped
|
||||
depends_on:
|
||||
- api
|
||||
ports:
|
||||
# Put your own TLS terminator in front of this. Jarvis speaks plain HTTP here on purpose and
|
||||
# reads X-Forwarded-Proto to know what the browser actually used.
|
||||
- "${JARVIS_PORT:-8080}:80"
|
||||
|
||||
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:
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 114 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 252 KiB |
+40
@@ -0,0 +1,40 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 96 96" width="96" height="96" role="img" aria-label="Jarvis">
|
||||
<!--
|
||||
The Jarvis identity mark. Two hexagonal shells OUT OF PHASE — a flat-top holding a pointy-top —
|
||||
around a core. That phase offset is the signature: a version that drops the inner ring is a
|
||||
hexagon, not this logo.
|
||||
|
||||
Colours are literals rather than CSS variables, because a README is rendered by a forge that
|
||||
does not load the app's stylesheet. The stroke reads on a white and on a dark ground alike,
|
||||
which is what a mark in a README has to survive.
|
||||
-->
|
||||
<defs>
|
||||
<linearGradient id="shell" gradientUnits="userSpaceOnUse" x1="48" y1="6" x2="48" y2="90">
|
||||
<stop offset="0" stop-color="#6e64d8" />
|
||||
<stop offset="1" stop-color="#6e64d8" stop-opacity=".76" />
|
||||
</linearGradient>
|
||||
<radialGradient id="halo" gradientUnits="userSpaceOnUse" cx="48" cy="48" r="30">
|
||||
<stop offset="0" stop-color="#a49cfc" />
|
||||
<stop offset=".55" stop-color="#a49cfc" stop-opacity=".32" />
|
||||
<stop offset="1" stop-color="#a49cfc" stop-opacity="0" />
|
||||
</radialGradient>
|
||||
</defs>
|
||||
|
||||
<circle cx="48" cy="48" r="30" fill="url(#halo)" opacity=".26" />
|
||||
<path
|
||||
d="M 48 23 L 26.35 35.5 L 26.35 60.5 L 48 73 L 69.65 60.5 L 69.65 35.5 Z"
|
||||
fill="none"
|
||||
stroke="#6e64d8"
|
||||
stroke-width="4"
|
||||
stroke-linejoin="round"
|
||||
opacity=".4"
|
||||
/>
|
||||
<path
|
||||
d="M 90 48 L 69 11.63 L 27 11.63 L 6 48 L 27 84.37 L 69 84.37 Z"
|
||||
fill="none"
|
||||
stroke="url(#shell)"
|
||||
stroke-width="6"
|
||||
stroke-linejoin="round"
|
||||
/>
|
||||
<circle cx="48" cy="48" r="8" fill="#a49cfc" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.6 KiB |
Reference in New Issue
Block a user