2535 lines
170 KiB
Markdown
2535 lines
170 KiB
Markdown
# 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-10-07 — Ctrl+Alt+Del at Windows sign-in `agent 0.30.1`
|
||
|
||
Sending **Ctrl+Alt+Del** in remote desktop now uses the Windows agent service in the watched
|
||
session, including a server with nobody signed in. The helper still verifies consent and the notice
|
||
on the screen before authorizing the request.
|
||
|
||
Expired requests and requests whose helper has disconnected are refused. A slow Windows call no
|
||
longer holds up subsequent input or allows repeated requests to accumulate.
|
||
|
||
Update the agent release image. No API or web image update or database migration is required.
|
||
|
||
## 2026-10-07 — Less clutter on the verification screen `web 0.146.1`
|
||
|
||
The second-factor screen goes straight from its heading to your verification method. The password
|
||
progress indicator and the reminder that two-step sign-in is enabled have been removed.
|
||
|
||
Update the web console only. No API update or database migration is required.
|
||
|
||
## 2026-10-07 — Clearer second-factor sign-in `web 0.146.0`
|
||
|
||
The second-factor screen now shows that your password is verified, highlights the active method,
|
||
and keeps other methods behind **Use another method**. Security verification and first-time setup
|
||
have their own headings and instructions.
|
||
|
||
Six-digit codes are easier to read, accept pasted spaces and separators, and still support browser
|
||
autofill. Email codes are sent when you ask for one, with a 60-second wait before resending. A failed
|
||
request keeps your code ready to retry; a cancelled security-key prompt keeps other methods available.
|
||
|
||
Keyboard focus follows method changes, and French and German buttons wrap on small screens. Setting
|
||
up an authenticator app shows the QR instructions before focusing the code, and completing account
|
||
setup now opens the console.
|
||
|
||
Update the web console only. No API update or database migration is required.
|
||
|
||
## 2026-10-04 — Sign in with a passkey alone `api 0.119.0` `web 0.145.0`
|
||
|
||
The sign-in page now offers **Sign in with a passkey** and passkey suggestions in the email field.
|
||
Your fingerprint, face or device PIN releases the key; no email or password is needed. Cancelling
|
||
the prompt leaves the form ready to use.
|
||
|
||
In **Settings → Security**, register a key with **Use it to sign in without a password** enabled.
|
||
Existing discoverable passkeys can work too; if yours is not offered, register a new one with that
|
||
switch enabled. Passwords and passkeys used as a second factor continue to work.
|
||
|
||
**Settings → Platform → Sign-in** has a **Passkey sign-in** switch, on by default. The option appears
|
||
only on a secure page using the instance's configured public hostname. Turning the switch off
|
||
refuses new passwordless sign-ins, including prompts opened before it was turned off.
|
||
|
||
Successful passkey sign-ins and key enrolment/removal are recorded in the audit log. Removing a key
|
||
ends the other sessions it opened, including their live connections; the session removing it stays
|
||
signed in after confirming the password. The first key enrolment also closes other sessions.
|
||
|
||
Challenge budgets now count unanswered attempts correctly, and anonymous passkey answers are checked
|
||
against the failure budget even when their challenges were opened in advance. IPv6 addresses in the
|
||
same /64 share a budget. Expired suggestions refresh quietly and do not count as failed signatures.
|
||
|
||
Update the API and web console together. The schema update adds three columns with compatible
|
||
defaults; no new environment variable is required.
|
||
|
||
## 2026-10-01 — Remote desktop keeps up, a smaller notice, a simpler viewer `api 0.118.0` `web 0.144.0` `agent 0.30.0`
|
||
|
||
**The picture keeps up.** On a large screen the agent managed 3 or 4 frames a second while anything
|
||
moved, for two reasons. It encoded the whole screen several times over, on one core. And ordinary
|
||
activity — a scroll, a window switch — was sent as the entire screen. It now encodes each pixel once,
|
||
in strips on several cores, sends each strip as soon as it is ready, and sends only the area that
|
||
changed. Measured on a 5120×1440 screen: a full repaint went from about 550 ms to about 40 ms, a scroll
|
||
from about 650 ms to about 20 ms, and a typed character from up to 190 ms to about 2 ms.
|
||
|
||
**The pointer no longer trails yours.** While you have control, the remote pointer is no longer drawn
|
||
over your own, and its position is sent before each frame rather than after it. That lag is what made
|
||
every movement feel slow.
|
||
|
||
**Clicks and keys are no longer lost.** The viewer sends a gesture — a click is a move and a button —
|
||
as one message, and sends keys as soon as they are pressed. The agent no longer refuses input that
|
||
arrives while it is still answering the previous batch, which happened to several percent of quick
|
||
inputs. It never injects input more than 150 ms old.
|
||
|
||
**Less old picture queued on a slow link.** The server opens a session with 320 KiB of credit instead
|
||
of 1 MiB, then sizes it to the speed the viewer actually receives at. A slow connection no longer
|
||
holds a second of stale pixels in front of every click.
|
||
|
||
**A smaller notice on the watched machine.** The banner is now a compact pill at the top centre of each
|
||
screen, with a small amber dot: 553×34 pixels on a 5120-pixel-wide screen, where it used to be a band
|
||
of 5120×72. It carries the same sentence, never shortened. It is still on every screen, still on top,
|
||
and still a condition of the session: if it cannot be shown, nothing is captured. The preview under
|
||
**Settings → Remote desktop** now shows the pill rather than a strip.
|
||
|
||
**A simpler viewer.** One toolbar, icon buttons with tooltips, and no paragraphs. The copy of the
|
||
banner above the screen is gone. The session list hides Windows' services session and the Remote
|
||
Desktop listener, and calls an empty console "Sign-in screen". Screens are numbered 1, 2… instead of
|
||
by Windows' internal number, and the screen list appears only when there is more than one.
|
||
|
||
**Fixed along the way:**
|
||
|
||
- Choosing the screen you were already watching no longer fails after two seconds.
|
||
- After a change of screen, the viewer no longer throws away every frame until the next pause.
|
||
- A session opened at a sign-in screen now names the person who signs in.
|
||
- The session record counts input while the session is live, not only once it ends.
|
||
|
||
**For whoever reads the agent's log:** each opening now records how long each step took, and each
|
||
input batch that times out is noted.
|
||
|
||
Update the three together. Nothing in the server's configuration changes.
|
||
|
||
## 2026-10-01 — Remote desktop, fixed: control, RDP sessions and the sign-in screen `web 0.143.0` `agent 0.29.0`
|
||
|
||
**Three things `agent 0.28.0` promised did not work on real machines, and this release is the fix for
|
||
all three.** The entry below this one describes them as working. They did not, and we found out from
|
||
the first customer machines rather than before shipping. Update your agents; the server needs nothing.
|
||
|
||
**Control works now. In `0.28.0` it never did, on any machine.** With control granted the viewer said
|
||
"You have control", the picture was live, and every click and keystroke was refused by Windows with
|
||
"Access is denied". The agent attached to the person's desktop without the one access right Windows
|
||
checks before it accepts synthetic input. Reading the screen does not need that right, which is why
|
||
everything else looked fine. `0.29.0` asks for it, on the thread that types and on no other.
|
||
|
||
**Watching a Remote Desktop (RDP) session works now. In `0.28.0` it never did.** Every attempt ended
|
||
in a fraction of a second with "The banner naming you could not be shown on this machine". The agent
|
||
took the screen's number from a display Windows invents for background services, and an RDP session
|
||
numbers its screens differently, so the notice was never drawn and nothing was captured. Physical
|
||
consoles happened to match, which is why laptops worked. The screen now comes from the session that
|
||
is about to be watched. If a machine cannot settle on a screen at all, the session ends with "The
|
||
machine could no longer capture its screen" and the reason, instead of a notice that never appeared.
|
||
|
||
**A machine nobody is signed in to can be watched — and signed in to.** In `0.28.0` it ended with
|
||
"There is no desktop session to watch on this machine". Windows does not count a console waiting at
|
||
its sign-in screen as an active session, and the agent required an active one. That console is now
|
||
what you are shown when nobody is signed in, and it is also the way in to a machine whose owner
|
||
closed their RDP window. The notice stands on the sign-in screen as it does everywhere else, and you
|
||
sign in by typing into it. Once somebody signs in, the notice moves to their desktop, and the picture
|
||
waits until it is standing there. What is still never followed is a person who is already signed in
|
||
going to their lock screen or a UAC prompt: the picture pauses there, as before.
|
||
|
||
**The agent now sets `SoftwareSASGeneration` on a machine where it is not set — this reverses the
|
||
`0.28.0` note below.** A domain-joined machine, and any Windows Server, asks for `Ctrl`+`Alt`+`Del` on
|
||
its sign-in screen, and Windows lets a service send it only when this policy allows services. When a
|
||
remote-desktop session opens on a machine where the value does not exist at all, `agent 0.29.0`
|
||
writes `3` — services, plus the accessibility applications an absent value already allows — and logs
|
||
a line saying so in the Windows Application log, source `JarvisAgent`. The limits:
|
||
|
||
- A value that exists is never changed. That includes `0`, a value of the wrong type, and anything a
|
||
Group Policy wrote; a Group Policy configured later overwrites ours on its next refresh.
|
||
- Machines that are never opened in remote desktop are never touched. Installing or updating the
|
||
agent writes nothing.
|
||
- It is `3`, never `1`: `1` would add services and close the accessibility path Windows leaves open.
|
||
- Either value lets every service on the machine send the sequence, not only ours.
|
||
|
||
If you do not want that, give the value an answer before anybody watches the machine. `0` forbids
|
||
everyone, Jarvis included, and the control is then hidden. Across a domain, the Group Policy
|
||
"Disable or enable software Secure Attention Sequence" set to _None_ does the same.
|
||
|
||
**What the agent sees now reaches its own log.** The agent's helper already recorded which screen it
|
||
chose, which desktop it attached to and why Windows refused an input, and `0.28.0` threw all of it
|
||
away. That is how the three failures above shipped without a trace. It now goes to the agent's log —
|
||
on Windows the Application event log, source `JarvisAgent` — one line per event, prefixed with the
|
||
session's id, at most 200 lines per session.
|
||
|
||
**`web 0.143.0`** only corrects one sentence. It is shown when a signed-in person moves to a Windows
|
||
security screen, and it said Windows forbids capturing those screens. It does not; the banner naming
|
||
you simply cannot follow them there.
|
||
|
||
## 2026-10-01 — Remote desktop: watch a Windows machine, and take the mouse `api 0.117.0` `web 0.142.0` `agent 0.28.0`
|
||
|
||
**Remote desktop opens.** **Agents** → a Windows machine → **Watch the screen**, and you see its
|
||
screen, live, for as long as you keep the tab open. Three things about it are worth reading before
|
||
you update rather than after: you can also **type and move the mouse**, not only look; the two
|
||
switches that allow it are turned on for your owners and administrators **by this update itself**,
|
||
once, and **no audit entry is written when that happens**; and the way back out is a switch in the
|
||
product rather than a rollback, because an agent cannot be un-updated.
|
||
|
||
**You can watch, and you can take the mouse and the keyboard.** Sight and control are two separate
|
||
grants and control is the second one — the viewer says "Watching only" and sends nothing at all for
|
||
as long as that is what you hold. With control, the keyboard and the mouse go to the far machine as
|
||
they would from a keyboard plugged into it: modifiers, the numeric keypad, AltGr, the wheel, and a
|
||
right-click. Typing into somebody else's Windows session is a different piece of Windows from reading
|
||
pixels out of it, so it refuses in places reading does not — see the black screens below.
|
||
|
||
**`Ctrl`+`Alt`+`Del` works from a session you hold control of, and only where the machine's own policy
|
||
allows it.** It is the one keystroke user-mode software cannot forge, which is the whole point of it:
|
||
the person at the
|
||
keyboard has to be able to trust that the sign-in screen in front of them is real. A machine permits
|
||
it only with bit 0 of `SoftwareSASGeneration` set, which is a policy **you** set on **your** machines
|
||
and that this release deliberately does not set for you. Where it is not set, the control is not
|
||
offered — and it is not offered on a watch-only session either, so somebody holding "Watching only"
|
||
finds it missing on a machine whose policy is right, with nothing on screen to say why. Write `3`
|
||
rather than `1`: both set bit 0, and `1` also widens the sequence to every service on the box while
|
||
closing the accessibility path Windows leaves open by default, which is how an on-screen keyboard or
|
||
another vendor's remote-control tool stops working. Do not set the policy on a machine whose agent
|
||
predates this release: the policy never travels from an older agent, so the edit buys nothing and
|
||
leaves the machine looser than it was. And what the sequence summons is a screen the viewer
|
||
deliberately will not show you — nor is it a way in to a machine already locked in front of you, where
|
||
the keystroke is refused along with every other one. It is for the machine somebody is signed in to: a
|
||
wedged shell that needs Task Manager, a password to change, a session to sign out.
|
||
`docs/operations.md` has the value, what it means, and how to read it off a machine; the
|
||
agent version to require is this one, `agent 0.28.0`.
|
||
|
||
**Nothing is captured unless a notice naming you is standing on the screen.** Your organisation's
|
||
policy decides how, at
|
||
**Settings → Remote desktop**, and every organisation starts on the banner. On **Show a banner**,
|
||
nothing at all is captured until a notice naming you exists, is mapped and holds the topmost window
|
||
style — which is as close to "visible" as Windows lets anything check: if it cannot be drawn the
|
||
session never starts, and if that window is destroyed or loses its topmost standing the session ends.
|
||
What is left open is a notice still standing that a later topmost window covers; we do not sample the
|
||
screen back to find out, and that is the one hole in this story we know of. On **Ask first**, nothing
|
||
is captured until the person accepts, and a refusal or a minute of silence ends it before it began —
|
||
recorded as their refusal, or as a question nobody answered, deliberately two different endings and
|
||
neither of them a failure. There is no third mode, no
|
||
override, and nothing an operator can do from their side to skip either one. The one deliberate
|
||
exception is a machine with nobody signed in: there is nobody to ask, so the session starts with the
|
||
notice shown and no question. **If somebody then signs in, the notice is taken down and raised again on
|
||
their own screen**, and the picture stops until it is standing there — so they are told, though nobody
|
||
goes back and asks them, which is the honest description of that one case. A machine nobody can log
|
||
into is exactly the machine an operator needs this for.
|
||
|
||
**Being told is not the same as being able to stop it, and the difference is worth stating.** The
|
||
notice is click-through on purpose — no click, no menu, no keyboard — so that it can never sit
|
||
between somebody and their work, and cannot be closed by accident or by a script. The cost is that
|
||
the person at the keyboard cannot dismiss the viewer from the notice itself. Their ways out are
|
||
elsewhere: refusing the prompt under the ask policy, before anything is captured; signing out, which
|
||
ends the session with it; or an administrator turning the feature off for the instance. What the
|
||
banner guarantees is that watching is never secret. Under the banner policy it is not asking for
|
||
consent, and it does not pretend to.
|
||
|
||
**A sentence of your own is added to the notice, never put in its place.** Whatever you write — a
|
||
policy reference, the number of your service desk — appears after the standard wording, so nothing
|
||
anybody can set here makes a session a quiet one. It is a sentence and not a page: 280 characters, and
|
||
longer is refused rather than trimmed. It is added to the banner only, and deliberately not to the
|
||
**Ask first** prompt, which stays the short question it has to be — so a number you add so that people
|
||
can check with you before accepting is not on the window where they accept.
|
||
|
||
**The language is the one the person at the machine reads, which is often not the one you read.** They
|
||
may have no Jarvis account at all: it is a customer's employee looking at their own screen. The server
|
||
composes the finished sentence in the language you pick and hands the machine that text, so the notice
|
||
is never assembled on the far end out of pieces, in a language nobody on your team could have checked.
|
||
English, French and German. The settings page previews it in the language you chose rather than in
|
||
yours — a preview in your own language would reassure you about a sentence nobody is ever shown. The
|
||
two buttons on the **Ask first** prompt are the one exception: a label that arrives empty or too long
|
||
for the button is drawn with the compiled-in English word instead, because a button with its text cut
|
||
off is worse than a button in the wrong language on the window where somebody decides.
|
||
|
||
**Changing any of that takes the right to change the organisation itself** — owners and
|
||
administrators, the same people who can rename it.
|
||
|
||
**Who may watch, and who may type: two new switches on an access grant**, at **Settings → Access**,
|
||
beside the one that gives a shell: **Watch the screen**, and under it **Move the mouse and type**.
|
||
They work exactly like the shell switch — per machine, per folder or for the whole organisation, most
|
||
specific rule wins — and they are independent of it in both directions. A machine room full of
|
||
headless servers is shells with no screen worth seeing; helping a colleague through their own desktop
|
||
is a screen with no business holding root. Neither implies the other, and neither implies anything
|
||
about what the assistant may do. **Control cannot be given without sight:** the second switch only
|
||
turns on when the first is on, and turning sight off turns control off with it. **Who may hold it:**
|
||
operators and above, the same as the shell, with a platform super-admin outside that answer as they are
|
||
everywhere else. A viewer's grant can be written with either switch on and reaches nothing — the
|
||
refusal is at the session, not at the row. **Who may hand it out** is a narrower question with the same
|
||
answer it has always had: writing grants belongs to owners and administrators, so an operator handed
|
||
sight of a client's machines passes nothing on to anybody. Where the ceiling bites is an owner or
|
||
administrator whose own row has been narrowed — holding sight but not control, they can write a
|
||
colleague a watch-only row and cannot write one that types; holding both, which is what the default row
|
||
for both roles carries, they pass on both.
|
||
|
||
**Owners and administrators are given both, once, by the schema sync — and the audit trail does not
|
||
record it.** Without it, turning the feature on at all would mean writing the first row from the
|
||
platform super-admin account, because nobody hands out access they do not themselves hold and no
|
||
ordinary grant would carry it. Only a row that **already** carries a shell on the whole organisation,
|
||
unbounded and not narrowed by a risk ceiling of its own, is widened: somebody an administrator has
|
||
decided not to trust with a terminal — or whose terminal was deliberately narrowed — is not handed
|
||
screens by an update, and needs a grant written by hand. **One exception to that, and it is the row to
|
||
check first:** an organisation where **nobody at all** currently holds a shell has the shell handed back
|
||
to its owners and administrators by a separate, older pass that runs moments earlier — and the screen
|
||
pass then widens those same rows. On a one-owner instance "nobody holds it" is simply "I took it off
|
||
myself", so if that is you, you get the shell back, plus the screen and the mouse, in one start. The
|
||
widening is part of the schema sync, which on compose is the api
|
||
container's own migrate step and on Kubernetes is the `jarvis-migrate` Job, so on Kubernetes nobody
|
||
is widened until that Job has run. It happens once per installation and is then stamped, which is
|
||
the whole of "once": if the stamp fails, or the process is killed between the first widening and it,
|
||
the pass stays armed and the next start hands both switches back to every owner and administrator row
|
||
that has since been revoked. The migrate log says so and names the column to set by hand — except in
|
||
the killed case, where it says nothing at all; rows that fail individually are skipped and counted in
|
||
the same log. Nobody else is touched. The missing entry is the part to know about: every _later_
|
||
change made on the grants screen is audited, but this widening is done by the schema sync rather than
|
||
by a person and writes no entry naming who now holds what — and neither does the default row a new
|
||
owner or administrator is seeded with when they are added or accept an invitation, which carries both
|
||
switches too. If your rule is that screen access is granted deliberately, read the grants of your
|
||
owners and administrators after updating, and again after adding one, rather than trusting the trail
|
||
to tell you.
|
||
|
||
**The audit trail now records the shell and the screen when a grant changes**, with their previous
|
||
values, which it did not before. "Who gave this person a shell on that client, and when" could not be
|
||
answered from the trail — the entry said that a grant had changed and which connectors it named,
|
||
while the switch that decides whether a human gets administrative control of the machine was
|
||
invisible. It is a gap that predates screen access; it is closed here rather than left for a third
|
||
switch to inherit.
|
||
|
||
**Windows security screens go black, deliberately.** A sign-in prompt, a `Ctrl`+`Alt`+`Del`, an
|
||
administrator-approval dialog, a lock screen: Windows puts each of those on a desktop of its own, and
|
||
the agent captures nothing but the one desktop its notice is standing on — so one that comes up during
|
||
a session arrives as a blank canvas. The refusal is ours rather than something the operating system
|
||
does for us, which is worth knowing if you are deciding how much of this to take on trust. **A session
|
||
you open while one of them is already standing is the exception, and it is deliberate:** the notice is
|
||
raised on whatever desktop is receiving input at that moment, so on a machine sitting at its sign-in
|
||
screen — the case this feature most exists for — the notice goes on the credential desktop and the
|
||
picture is of that. The rule being obeyed is the one rule in all of this, that the notice stands on the
|
||
desktop being captured and never beside it, rather than a list of screens by name. What the agent
|
||
refuses is to _move_ a notice onto a security desktop once a session is running, where the alternative
|
||
is a blank canvas rather than a secret one. The viewer tells you a Windows
|
||
security screen is up — a sign-in prompt, or a
|
||
request for administrator approval — without saying which, because what the machine can report is that
|
||
the desktop changed and not what is on it; a screen saver, or a desktop some other vendor's tool
|
||
created, reports the same thing under the same words. The picture resumes on its own when the screen
|
||
goes away, rather than showing you a frozen frame of the desktop underneath, which is the thing a
|
||
remote viewer must never do. Keystrokes do not reach those screens either. `Ctrl`+`Alt`+`Del` goes by
|
||
a route of its own for a separate reason: three injected keystrokes arrive on an ordinary desktop as
|
||
three ordinary keys and the sign-in process ignores them, so the sequence has to be asked for through
|
||
a dedicated Windows call rather than typed — and that call reports nothing back, so the only
|
||
confirmation anybody gets is the picture going black.
|
||
|
||
**Nothing is recorded, and what is kept is not nothing.** No video, no screenshot, no transcript, and
|
||
no setting anywhere that turns one on. What is kept is the session record: who connected — or that it
|
||
was a platform role rather than a grant — which machine, which consent policy applied, **whether
|
||
control was allowed**, how much data went each way, **how many input events were accepted and how
|
||
many were dropped**, when it started, when it ended and why; and then, once per screen watched,
|
||
which Windows session and the account signed in to it, which monitor, whether consent was agreed and
|
||
when, and whether it was accepted without anybody clicking — which is every session under the banner
|
||
policy, and an ask-policy session with nobody signed in. A session that moves to another screen keeps
|
||
one of those rows per stretch, each with its own consent, and that is the part a staff disclosure most
|
||
needs: whose desktop, for which stretch. The size of the picture is deliberately not stored at all.
|
||
Unlike a terminal transcript, that record has no retention setting and nothing sweeps it — but it is
|
||
keyed on the machine: deleting an asset takes its session rows with it, and deleting an account blanks
|
||
who connected. What outlives both is the platform audit trail, which keeps the open, the consent and
|
||
the close with no retention at all. Write your staff disclosure from that list rather than from the
|
||
first sentence.
|
||
|
||
**Windows only, and a machine that cannot do it says so instead of failing.** Your Linux and macOS
|
||
agents announce no screen and are never offered one — they go on doing everything they did, and the
|
||
server refuses a screen on any machine whose agent did not claim to have one. The row for a Mac or a
|
||
Linux box carries the action dimmed and **named** "Watch the screen — Windows only", and _named_ is
|
||
the exact word rather than a hedge: on the **Agents** page that entry is promoted into an icon-only
|
||
button, so a screen reader reads the sentence out, a touch device and the machine's own row on
|
||
**Assets** print it as words in the menu, and a mouse-driven browser shows a greyed monitor glyph with
|
||
the OS column beside it as the visible answer. The tooltip does not open, because a disabled control
|
||
takes no pointer. **And that control being live is a statement about the machine, not a promise about
|
||
your grant.** It is drawn for anybody whose organisation role may watch screens at all: a Viewer never
|
||
sees it, while an operator who holds the role but no **Watch the screen** grant sees it enabled on
|
||
every connected Windows row and is refused in words when they use it. The grant is checked when the
|
||
session opens, not when the menu is drawn.
|
||
|
||
**Sessions end by themselves.** Four hours for any one session — and the card announcing that ending
|
||
offers to open another, so the ceiling bounds a forgotten tab rather than how long somebody can be
|
||
watched. A session also ends if the viewer stops keeping up with the picture for twenty seconds, which
|
||
on a bad connection is a thing you will meet; it comes back with the same button. Switching to another
|
||
browser tab does not end a session: the picture pauses, the machine stops sending while the tab is
|
||
hidden, and both resume when you come back — a window merely covered by another window is not hidden
|
||
and goes on costing what it cost, which is a trade of liveness for bytes made on purpose. A tab left
|
||
paused for fifteen minutes
|
||
is ended, give or take the fifteen seconds it takes the next re-authorisation sweep to notice, on the
|
||
grounds that a viewer which has said it is not looking and has gone on not looking for that long
|
||
should not hold a helper process and a banner on somebody's screen until the four-hour ceiling. A
|
||
session you are actually watching is not idle no matter how long you watch it without touching
|
||
anything, which is the right answer for a screen and the wrong one for a shell.
|
||
|
||
**Two sessions at once on any one machine, and sixteen on any one api pod.** A third attempt on the
|
||
same machine is refused and says how many people are already watching it — that count is read for the
|
||
sentence and never for the gate, so when it cannot be read the refusal still stands and says only that
|
||
the machine is showing its screen to as many people as it allows. Neither wording names the machine.
|
||
Nothing already running is ended to make room, and two tabs
|
||
belonging to one operator fill the two seats exactly as two operators would. The per-machine number is
|
||
compiled into the agent and is the only thing that enforces it, so it cannot be changed from the
|
||
instance and moves only with an agent release. The per-pod number is the one a single-replica instance
|
||
meets first, and its refusal says to try again in a moment.
|
||
|
||
**Separately, a terminal session's flow-control window is now clamped to what the server actually
|
||
sent.** A terminal holds output back until the browser confirms it has caught up, and that is what
|
||
makes a runaway process on the far machine slow down instead of flooding the session. A browser could
|
||
confirm more than it had been sent, which switched that brake off for the rest of the session on a
|
||
machine reached through its Jarvis agent — no privilege beyond a terminal the person was already
|
||
entitled to open, and nothing to see afterwards but load. Sessions reached over SSH were never
|
||
affected: that transport re-applies its own brake every 256 KiB, and has in every released version. The confirmation is now capped at what was sent and not yet confirmed, so it can never be worth
|
||
more than the output it answers. A malformed confirmation is ignored rather than jamming the session.
|
||
Nothing changes for an ordinary session.
|
||
|
||
**Self-hosters: there is one switch that turns all of it off, and it is the way back.** Settings →
|
||
Platform → **Agents** → the **Remote desktop** card, where the switch reads "Screens can be watched".
|
||
The tab used to be called "Agent updates" and is called **Agents** now, renamed because an operator
|
||
looking for a kill switch had no reason to open it. Do not confuse it with the page also called
|
||
**Remote desktop** at Settings → Remote desktop: that one is per organisation, holds the consent
|
||
policy, and cannot stop anything. Turning the instance switch off refuses every new session and ends
|
||
the ones already
|
||
running within fifteen seconds, on every machine at once, without touching terminals, agent updates or
|
||
anything else the agents do. It exists because the api and the web images roll back by digest and an
|
||
agent release does not: once `0.28.0` is out on your fleet it is out, so the instance needs a way to
|
||
say no that does not depend on recalling it. Know where it is before you need it — and know that the
|
||
page is platform-super-admin only, so an organisation's own administrators cannot reach the switch and
|
||
will have to ask whoever holds that account.
|
||
|
||
**Self-hosters: the agents update themselves on the default policy — and on `MANUAL` or `DISABLED`
|
||
they do not, so there you push the update yourself before there is anything to watch.** Screen capture
|
||
and input live in `agent 0.28.0`. An older agent connects and works exactly as before; the action on
|
||
its machine is dimmed and named "Watch the screen — agent too old", read the way described above,
|
||
rather than opening a session that cannot start. An agent that is merely offline is named that
|
||
instead — a third distinct label, in the same place and read the same way. Publish the api first,
|
||
then the web, then the agent — on the bundled compose overlay those are not three steps, since it
|
||
couples the agent release to the api's start, and the new release lands in its volume first. That is
|
||
harmless, and not a sign you have done it in the wrong order.
|
||
|
||
**Self-hosters: check your reverse proxy before you update.** The api now listens on a **third**
|
||
WebSocket path on the same `JARVIS_PORT` — `/api/agents/ws/desktop`, beside `/api/agents/ws` and the
|
||
browser's socket. A proxy that forwards `/api/agents/ws` by **exact path** rather than by prefix will
|
||
pass the first two and fail this one: terminals keep working, agents stay green, and every attempt to
|
||
watch a screen fails at once, with the agent naming the URL it could not reach — "the picture
|
||
connection to wss://…/api/agents/ws/desktop was refused (HTTP 404)", in the agent's log and on the
|
||
operator's error card, which is the diagnosis handed to you. Match by prefix. Both
|
||
configurations shipped here already do — the bundled nginx matches `^~ /api/agents/ws`, and the
|
||
Kubernetes ingress uses `pathType: Prefix` — so a stock deployment needs no change. It is a
|
||
hand-written proxy in front of one of those that is worth a look.
|
||
|
||
**Self-hosters: the schema.** `pg_dump` first, as for any api minor bump. It gains two tables,
|
||
`DesktopSession` and `DesktopSessionTarget`, two enum types, three columns on an organisation for the
|
||
consent policy, two columns on an access grant for the switches, and two on the instance's own
|
||
settings — the kill switch further down, and a timestamp recording that the widening has run.
|
||
Everything is defaulted and it only adds, so the schema guard has nothing to refuse. No new
|
||
environment variable, and nothing to run by hand. **On Kubernetes, the schema is applied by the
|
||
`jarvis-migrate` Job and not by the api pods** — an arrangement that landed in the _previous_ release
|
||
rather than in this one, so if you maintain your manifests by hand, check you actually have
|
||
`03-migrate-job.yaml` and `JARVIS_SKIP_MIGRATIONS=true` on the Deployment before you rely on it. **That
|
||
Job has to finish before the api Deployment, and there is no safe order the other way round.** An api
|
||
image that reaches a database the Job has not touched may not get as far as listening: its boot seeds
|
||
the instance settings row, that write now returns the two new columns with it, and on an installation
|
||
whose row is still missing a public URL, a proxy-hop count or the vault-key timestamp — the last of
|
||
which is every Kubernetes install made before api 0.114.1 — the column is not there, the boot hook
|
||
rejects, and the process exits before anything is served. Nothing answers the health check, so the pod
|
||
fails to come up rather than coming up wrong. Where the api does start, every read of an access grant
|
||
fails instead: not only watching a screen, but opening a
|
||
terminal, the asset-scoped tools an assistant may use, and **Settings → Access** itself, because one
|
||
query now reads the two new columns for all of them. What that failure says is the database's own
|
||
complaint about a column that does not exist, not a tidy "try again in a moment". Under Argo CD the Job
|
||
is a Sync hook in wave 0 and runs on its own; applied by hand, run it and wait for it. On docker
|
||
compose none of that applies — the api container still applies the schema itself before it serves, and
|
||
refuses to serve at all rather than serving half of it. **Reload the app in every open browser tab
|
||
once the web image is out**: a tab still running the previous build does not know about the two new
|
||
switches, and saving an existing grant from it writes them off — an edit that does not mention a
|
||
switch turns that switch off, which is how a grant edit has always worked. That is also the one thing
|
||
that quietly undoes the widening above, and nothing puts it back: the pass runs once per instance, so
|
||
a grant saved from a tab that was open across the deploy loses both switches for good.
|
||
|
||
**One caution, stated because it is the kind of thing a changelog should not leave for a support
|
||
ticket.** The screen is captured — and the keyboard and mouse delivered — by a short-lived helper
|
||
process that Jarvis starts inside the machine's interactive Windows session, because a Windows service
|
||
cannot reach another session's screen or press a key into it at all. It runs as the service's own
|
||
account moved into that session rather than as whoever is sitting there, and it needs nobody to be
|
||
sitting there: an active console session with no one signed in is an ordinary target, and on a server
|
||
it is the usual one. It is started and stopped with the Jarvis session — four hours at the outside —
|
||
and it asks Windows for no privilege the service did not already hold. What it gains is reach rather
|
||
than privilege, and that is the part to weigh: a system account standing on an interactive desktop can
|
||
attach to the desktops Windows keeps for its own prompts, and can deliver a click into a window running
|
||
elevated. The second is deliberate — control that stopped at an administrator's own dialog would be
|
||
control that does not work — and of the first we take only the sign-in desktop of a machine nobody is
|
||
signed in to, which is the case this feature exists for. It is also the reason the notice can be drawn
|
||
where the person will actually see it. On a machine with unusually strict software-restriction policy,
|
||
that helper is a new executable path your policy has not seen before.
|
||
|
||
## 2026-09-27 — The install's last step no longer strands a first deployment `api 0.116.1` `web 0.141.2`
|
||
|
||
**The symptom.** On a first installation the wizard could stop on its last step, the vault key
|
||
backup: the key was not shown, and the step still asked for its last eight characters. It happened
|
||
on the published Kubernetes stack. It had two independent causes.
|
||
|
||
**A deployment finished the install for you.** The data repair that runs with every schema sync
|
||
marked an instance installed as soon as it had a platform administrator. That was meant for
|
||
instances installed before the wizard existed, but the wizard's own second step creates that
|
||
administrator. On Kubernetes the sync is the `jarvis-migrate` hook, which runs on every Argo CD sync,
|
||
image updates included; on compose it runs at each api start. So a deploy between the second step
|
||
and the last one marked a half-installed instance installed, and an installed instance no longer
|
||
shows its vault key.
|
||
|
||
**The last step hid the key on Kubernetes.** It left out the "Show my vault key" button whenever the
|
||
key came from the environment, and said you already had it in your `.env`. On Kubernetes the key
|
||
always comes from the environment, from the `jarvis-generated` Secret the minting Job writes, and
|
||
nobody has seen it. The only command offered was a `docker compose` one. This cause alone was enough
|
||
to strand an operator, even when no deploy ran during the install.
|
||
|
||
What changed:
|
||
|
||
- The repair now marks only an instance whose administrator the install did not create, meaning
|
||
one with no `install.claim` entry in its audit trail, and only once that administrator is more
|
||
than fifteen minutes old. A sync in the middle of an install leaves it alone. Instances from before
|
||
the wizard are still recognised, and an instance already marked installed stays marked. An install
|
||
that finished and later lost its mark (an `install.complete` entry, or an unattended install) gets
|
||
it back, as it did before.
|
||
- The wizard writes the administrator and its `install.claim` entry together, so a sync can no
|
||
longer land between the two. If that entry cannot be written, the claim fails and nothing is
|
||
created; submit it again.
|
||
- The last step always offers the one-time reveal, which is audited, wherever the key comes from.
|
||
- The command for reading the key yourself matches the deployment, and prints the key on a line of
|
||
its own, so your prompt no longer sits against its last characters. On Kubernetes it is `kubectl`
|
||
on `jarvis-generated`, with your namespace filled in when the api can read it, plus the same on
|
||
`jarvis-secrets` if you set `VAULT_MASTER_KEY` there; it runs as is in PowerShell too. With the
|
||
published compose file it is `docker compose exec`. A refused reveal names the same command, the
|
||
checks step names the Secret on Kubernetes, and the claim step shows both published commands.
|
||
- The `.env` sentence is now only a hint beside the button, shown when the api itself receives
|
||
`VAULT_MASTER_KEY` in its environment outside Kubernetes. The published compose file never does
|
||
that: its `init` service copies a key you set in `.env` into the `jarvis_secrets` volume, so the
|
||
step shows the `docker compose exec` command instead.
|
||
- If the instance is already installed when the last step runs, the step says so, gives that
|
||
command, and lets you continue into Jarvis. You can type the key's last eight characters there to
|
||
check the copy you kept; that is optional.
|
||
- Reloading `/install` as the administrator of an instance that was marked installed before its
|
||
last step shows that same screen, instead of going straight into Jarvis. It stops once you have
|
||
checked your copy there, or if the key had already been shown to you.
|
||
- Finishing on an instance that turns out to be already installed still checks the eight
|
||
characters; it used to accept any eight. The api reports that the instance was already installed,
|
||
and the step continues into Jarvis as usual. On Kubernetes, a wrong answer points at
|
||
`jarvis-secrets` in case that is where your key is.
|
||
- The key is never revealed on an installed instance, even by an api pod that has not yet heard
|
||
that another one finished the install.
|
||
|
||
**Stuck on that screen now?** You do not have to wait for this release. On the version you run
|
||
today, read the key with the command below and type its last eight characters on the last step.
|
||
That finishes the install, or is accepted if a deploy already marked the instance installed. Keep
|
||
the key somewhere that is neither the cluster nor next to your database dumps.
|
||
|
||
```sh
|
||
kubectl -n jarvis get secret jarvis-generated -o go-template="{{.data.VAULT_MASTER_KEY | base64decode | println}}"
|
||
```
|
||
|
||
`jarvis` is the namespace in the published manifests; use yours if you changed it. Use
|
||
`jarvis-secrets` instead of `jarvis-generated` if you set `VAULT_MASTER_KEY` there yourself. On
|
||
compose it is `docker compose exec api sh -c "cat /var/lib/jarvis/secrets/vault-master-key; echo"`.
|
||
|
||
After the upgrade, reload the install page, signed in as the administrator (signed out, you are
|
||
brought back to it after signing in):
|
||
|
||
- If a deploy had already marked your instance installed, the page says so and shows the command
|
||
for your deployment. Back the key up, check it there if you like, and continue into Jarvis.
|
||
- Otherwise your instance is not installed yet, and the install resumes at its address step. Your
|
||
settings are saved; click through to the last step, which now shows the key.
|
||
|
||
**Self-hosters:** api and web; publish the api first, since the web reads what it now reports about
|
||
where the key is. No schema change, no new environment variable, and nothing to run by hand.
|
||
|
||
## 2026-09-27 — No notice under every picture in the viewer `web 0.141.1`
|
||
|
||
**A picture opened in the viewer no longer carries a notice saying it is the copy that was sent.**
|
||
It appeared under every attached picture and said the same thing each time. The viewer still says
|
||
what is worth knowing about a particular file: that the model could not see a picture, that a
|
||
picture is animated, or that a PDF has no text or was read only in part.
|
||
|
||
## 2026-09-27 — A viewer for every file in a conversation `api 0.116.0` `web 0.141.0`
|
||
|
||
**Open any file in a full-screen viewer.** A click on an attachment in the transcript, on a file in
|
||
the workspace, on a chip in the composer before the message is sent, or on a picture in a reply
|
||
opens it, and previous/next walk through the other files of the same message, or of the workspace
|
||
in the panel's order. The viewer has a dark stage in both themes, an info panel (size, dimensions,
|
||
pages, who sent it, when), download, copy, and a keyboard map; on a phone, swipe between files and
|
||
down to close.
|
||
|
||
- **Pictures:** zoom at the cursor or between two fingers, fit, fill and actual size, pan with
|
||
inertia, rotate, a minimap once zoomed, and animated pictures that play. A mouse wheel notch
|
||
zooms; a trackpad scrolls, and pinches to zoom.
|
||
- **PDFs are drawn in the page**, with thumbnails, find, zoom and fit-width, and their text can be
|
||
selected. A scanned PDF, which used to show nothing, now shows its pages. They are rendered by
|
||
pdf.js 6.3.289 with scripting, XFA forms and editing turned off; a link inside a PDF opens in a
|
||
new tab. On a browser older than Chrome or Edge 125 or Safari 18, a PDF shows its extracted text
|
||
instead.
|
||
- **Text files:** logs, code, scripts and configuration open in a reader with line numbers,
|
||
wrapping, find, go to line, colour codes in logs and syntax highlighting, and stay fast on a
|
||
10 MB log. CSV and TSV open as a table, JSON as a tree you can fold, NDJSON as records, and
|
||
certificates and certificate requests as their decoded fields. Markdown opens rendered, up to
|
||
1 MB, with its source a click away; pictures from other sites in it still load only on a click.
|
||
- Files the assistant fetched from a machine are not opened in the viewer, as before.
|
||
|
||
**Viewing a file uploaded to a workspace is no longer an export.** Opening one used to go through
|
||
the export route, which filed a `document.export` entry on every view; it now reads the file
|
||
directly and files nothing. Downloading it is still an audited export.
|
||
|
||
**Diagrams** zoom and pan on the same engine, and behave as before.
|
||
|
||
**Self-hosters:** api and web; publish the api first, since the web uses its new read route. No
|
||
schema change and no new environment variable. **The web app now needs Chrome or Edge 107,
|
||
Firefox 104 or Safari 16 or later.** The web image serves the PDF engine's worker and its
|
||
WebAssembly from `/_assets/pdfjs-6.3.289/`; a reverse proxy in front of it passes them through
|
||
like the other assets.
|
||
|
||
## 2026-09-26 — Attach files to a message, and upload files to a conversation's workspace `api 0.115.0` `web 0.140.0`
|
||
|
||
**Drop, paste or pick files into the composer.** Pictures, text files and PDFs, up to 10 per
|
||
message, upload as you add them and travel with the message; they are not listed in the
|
||
conversation's workspace. A text file or a PDF can be up to 10 MB. A picture is made smaller in your
|
||
browser before it leaves — scaled to 2,048 pixels on its long side and stripped of camera metadata —
|
||
so one of up to 40 MB can be dropped, and a full-page screenshot is scaled to fit rather than
|
||
refused for its height, up to 100 megapixels. The server checks what each file actually contains,
|
||
whatever its name says, and strips metadata again. SVG and HTML, Office documents, archives,
|
||
executables, and key or secret files are refused, and so is a text file that holds a private key;
|
||
scripts and public certificates are accepted as text.
|
||
|
||
**Upload files to the workspace** from the Documents panel, by button or by drop, up to 10 MB each.
|
||
They become documents everyone in the conversation sees, marked with who uploaded them. A picture
|
||
is kept as it was dropped, and the server strips its metadata. Your browser changes it only when it
|
||
has to: it scales a picture larger than 8,000 pixels on a side or 40 megapixels in all, turns a
|
||
sideways phone photo upright, converts a format Jarvis does not store, such as HEIC, when the
|
||
browser can read it, and re-encodes a picture over 10 MB at its full size until it fits — the
|
||
upload says so when it does. A download gives back the picture that was stored. A large picture
|
||
also travels with a smaller copy made for the model, so the assistant can look at it; people are
|
||
always served the picture itself.
|
||
|
||
**The assistant can use them.** It reads text files and the text of PDFs — extracted once, when the
|
||
PDF arrives; a scan with no text layer has none — and looks at pictures. **What the model
|
||
receives:** an attached picture is sent as the picture itself when the assistant answers the
|
||
message it came with, and again only if the assistant asks to look at it; a file's text is sent as
|
||
the passages the assistant reads. If the model refuses pictures, the turn is answered without them
|
||
and the picture says the model could not see it. This is the same on your own provider and on the
|
||
managed model. **If you use the managed model,** what it receives now includes the files and
|
||
pictures people attach, when the assistant uses them.
|
||
|
||
**Who can:** exactly who can send a message in the conversation. **Public links never include
|
||
attachments or uploaded files**; a guest sees that a message had attachments, nothing more. The
|
||
assistant's replies are still included, and a reply that describes a file describes it to the guest
|
||
too.
|
||
|
||
**A sent file can be removed** by whoever attached it, or by the conversation's owner. It is deleted
|
||
for everyone, the model stops receiving it from the next request on, and the message keeps a note
|
||
saying who removed it. That cannot take back what the assistant already read: whatever it quoted in
|
||
its replies or in tool output stays in the conversation.
|
||
|
||
**Pictures from other sites are no longer loaded until somebody clicks them**, in replies, in
|
||
workspace documents and on public links — in conversations written before this version too. A reply
|
||
can point a picture at any address, and fetching it tells that site the conversation is being read;
|
||
its address can carry text out of the conversation. The address is shown instead, and a click loads
|
||
it. Pictures written into the text itself (`data:` images) now display, where they used to draw as
|
||
an empty frame.
|
||
|
||
**On the audit trail:** an upload to a workspace is `document.upload` and a removal is
|
||
`attachment.remove`. An administrator who attaches or removes a file in a conversation they reach
|
||
only through their role is also recorded as `conversation.admin_attachment`, and a message they
|
||
send records how many files it carried. These entries hold ids, kinds and sizes, never a file's
|
||
name.
|
||
|
||
**Self-hosters:** api and web; publish the api first. The schema gains two tables, three types and
|
||
new columns beside a captured file's bytes, applied automatically when the api starts; it only
|
||
adds, so the schema guard has nothing to refuse. No new environment variable. **Your reverse proxy
|
||
must accept request bodies of 13,376 KiB on `/api/uploads/`** ([README](README.md#your-reverse-proxy-has-three-requirements));
|
||
below that, files between its limit and 10 MB fail with the proxy's own error while smaller ones
|
||
keep working. The web image's own nginx already does. **The api needs more memory:** an upload of
|
||
10 MB with its model copy was measured at about 310 MB while it is checked, so each api process now
|
||
takes three uploads at a time, and `04-api.yaml` raises the api's memory limit from 2Gi to 3Gi.
|
||
Give a compose host the same headroom. **Kubernetes:** apply the new `06b-ingress-uploads.yaml`,
|
||
an Ingress for uploads alone, and patch its host and TLS block like the main Ingress's: an overlay
|
||
that patches only the first leaves every file between 4 and 10 MB refused by the controller.
|
||
`06-ingress.yaml` also had its two `annotations:` blocks merged into one; `kubectl apply -f` kept
|
||
only the second, and so dropped the sync-wave.
|
||
|
||
**Rolling back past this version is not a plain image change.** An older build's schema sync would
|
||
drop the two attachment tables, the new columns and the three types. The schema guard refuses that
|
||
as soon as one file document exists — a single file copied off a machine, even before this version,
|
||
is enough, since the new columns are filled for every row — or anything has been attached, and the
|
||
api does not start. **On compose, set `JARVIS_SKIP_MIGRATIONS=true` in `.env` for the rollback**:
|
||
the older build then starts without syncing the schema, runs on the newer one without touching what
|
||
it does not know, and nothing is lost; take the variable out again before you upgrade. **On
|
||
Kubernetes** the `jarvis-migrate` Job has no such switch; it fails before the api is touched, so
|
||
this version keeps serving. Restore the dump you took before upgrading, or set
|
||
`SCHEMA_ACCEPT_DATA_LOSS=true` on the Job for that one run and put it back. Forcing it deletes every
|
||
attached file for good, and on the next upgrade files uploaded to a workspace come back as captured
|
||
files with no source, which the assistant can no longer read or preview — they still download. Take
|
||
a dump before upgrading.
|
||
|
||
## 2026-09-25 — A fresh Kubernetes install no longer asks for its own vault key `api 0.114.1`
|
||
|
||
**On the published Kubernetes stack, creating the first administrator asked for the last characters
|
||
of the vault master key, every time.** That proof exists for one case: an instance whose database has
|
||
gone missing while its key survived, which looks brand new from the outside. A genuine first
|
||
installation is never meant to be asked. On Kubernetes it always was. The minting Job writes
|
||
`VAULT_KEY_CREATED_AT` beside the key it generates, but the api read that date only when the key came
|
||
from a file, and this stack passes the key as an environment variable. The date went unread, the
|
||
key's age was unknown, and an unknown age counts as "cannot prove this instance is new".
|
||
|
||
**The api now reads `VAULT_KEY_CREATED_AT` wherever the key comes from.** A fresh install whose api
|
||
starts within fifteen minutes of the key being minted is claimed without it. The case the proof
|
||
exists for is unchanged: a `jarvis-generated` Secret that outlives its database still carries its
|
||
old date, and the claim still asks for the key.
|
||
|
||
**The Job now binds its date to its key**, with a fingerprint in the same value
|
||
(`<date> sha256:<16 hex>`), and on this stack the api counts a date only when it is bound to the key
|
||
in use. The api merges `jarvis-generated` and `jarvis-secrets` name by name, so a key you put in
|
||
`jarvis-secrets` yourself would otherwise meet the date the Job minted for another key; a key
|
||
restored before its database would then read as brand new, and its first administrator would be
|
||
free. A date without the fingerprint, or bound to another key, is ignored, and the claim asks for
|
||
the key as before. To have a first install on the manual path claimed without it, bind the date as
|
||
`01-secret.example.yaml` shows. A date more than five minutes in the future is refused too.
|
||
|
||
**If you apply the files one by one, move `01-secret-job.yaml` to the same api digest as the rest.**
|
||
A Job on an older image writes a date without the fingerprint, and a first install keeps asking for
|
||
the key. Kustomize and Argo CD already give the Job the api's image, from the one `images:` entry.
|
||
|
||
**An instance deployed before this version and not installed yet will still ask.** Its
|
||
`jarvis-generated` was minted by an older Job, whose date carries no fingerprint and does not count.
|
||
Give the key's last characters. The key is in `jarvis-secrets` if you set
|
||
`VAULT_MASTER_KEY` there, and in `jarvis-generated` otherwise:
|
||
`kubectl -n <namespace> get secret jarvis-generated -o jsonpath='{.data.VAULT_MASTER_KEY}' | base64 -d`.
|
||
|
||
**The published Compose stack is not affected.** There the key is a file, and its age was already read
|
||
from it.
|
||
|
||
## 2026-09-25 — The licence report counts operators, and the paid plans are Business and MSP `api 0.114.0` `web 0.139.0`
|
||
|
||
**The licence report sends one more number.** Next to organizations, enabled users, agents and assets,
|
||
it now carries **operators**: enabled accounts with a platform administrator role, or an OWNER, ADMIN
|
||
or OPERATOR role in at least one organization, counted once per account. It is a count, like the
|
||
others — no name, no address, nothing about who they are — and the
|
||
[field table](README.md#licence-keys-and-what-your-instance-reports) lists it. A community instance reports it too.
|
||
|
||
**Paid licences are now Business and MSP / Service Provider, plus the Trial.** A key issued on one of
|
||
these plans shows its name on the licence screen. Kubernetes is listed among the features a licence
|
||
can carry. Nothing an existing key already allows changes.
|
||
|
||
**Reactivating a disabled account now counts, as creating one does.** On an instance at its user
|
||
allowance, turning an account back on is refused with the same sentence as adding one. Before, an
|
||
account could be disabled, a new one created, and the first turned back on, ending above the
|
||
allowance.
|
||
|
||
## 2026-09-25 — A licensed instance can choose the managed model while it is being installed `api 0.113.0` `web 0.138.0`
|
||
|
||
**The install screen now asks for the licence before the model, and offers the managed model
|
||
there.** The model step used to come first and knew only your own provider. The managed model can
|
||
only be offered to an instance your Jarvis provider has linked to its licence, that link is made by
|
||
the instance's licence report, and an instance being installed sent none — so a licensed customer
|
||
had to type a key for a provider they did not want, or skip the model and choose the managed one
|
||
afterwards under **Settings → Platform → Model**.
|
||
|
||
**Adding a paid or trial key on the install screen now sends the licence report straight away**,
|
||
rather than once the install is finished — and so does opening the licence step with a key the
|
||
instance already holds from `JARVIS_LICENSE_KEY`. That is a new moment of sending, so here is
|
||
exactly what it is: the same report with the same fields as every other — the licence id, this
|
||
instance's id and public key, its version, contract checksum, counts and public address, the
|
||
timestamps and the signature; no names and no conversation content. The
|
||
[README](README.md#licence-keys-and-what-your-instance-reports) lists every field. It goes early
|
||
because it is what links the instance to its licence. The licence step says so beside the key field,
|
||
and shows how the report went — linked, refused by your provider and why, or not sent and why — with
|
||
**Report now** to try again.
|
||
|
||
**Nothing else reports earlier than before.** Only the licence step sends a report while the
|
||
instance is being installed; nothing reports on its own until the install is finished, even when
|
||
the key and the address both come from the environment. An instance without a licence still sends
|
||
nothing until then, and the licence step sends nothing for a licensed one whose address is not set
|
||
yet, is `localhost` or another loopback address, or is an example address such as
|
||
`jarvis.example.com`.
|
||
|
||
**The model step is now the same choice as the settings screen**: your own provider, with the test
|
||
button and "Skip for now" as before, or the managed model, with the same statement of what it sends
|
||
and the same box to tick before it is selected. Choosing the managed model there is enough to
|
||
continue, so a licensed install can be finished without a model key of your own. Without a licence,
|
||
the managed option is shown as unavailable, with the reason and a way back to the licence step.
|
||
Reaching the model step on a licensed instance asks your provider whether the managed model is
|
||
offered, exactly as opening **Settings → Platform → Model** does: a signed request with no
|
||
conversation content.
|
||
|
||
**Also in this release.**
|
||
|
||
- "Report now", under the managed model's option in the model settings, now says what your provider
|
||
answered when a report did not link the instance — a licence it does not know, one already in use
|
||
on as many instances as it allows, one it has withdrawn — where it used to say nothing and leave
|
||
the option unavailable.
|
||
- When your provider refused to link the instance for a reason another report cannot change — the
|
||
licence is already in use on as many instances as it allows, or your provider does not know it —
|
||
the managed option now says that, on the settings screen and on the install screen, instead of
|
||
"available after this instance's next report". The licence step keeps saying it when you come back
|
||
to it.
|
||
- "Your own provider" is marked **In use** only once an endpoint's model and key are saved. A new
|
||
instance used to show it over an empty form.
|
||
- An install that has been started can be resumed from a browser that is not signed in: the install
|
||
screen asks you to sign in, and the sign-in page now opens instead of sending you back.
|
||
- The Kubernetes `02-config.yaml` no longer sets `WEB_ORIGIN` to the placeholder
|
||
`https://jarvis.example.com`; it is commented out. Applied unedited, it was stored as the
|
||
instance's address until the install screen replaced it.
|
||
|
||
**Self-hosters:** api and console. The schema gains one column, the licence server's answer to the
|
||
last report, applied automatically when the api starts; it only adds, so the schema guard has
|
||
nothing to refuse. No new environment variable. An instance that is already installed reports
|
||
exactly as before. On Kubernetes, nothing to do either: the install screen saved your real address
|
||
over the placeholder when you installed.
|
||
|
||
**Rolling back past this version** drops that column, which the schema guard refuses once a report
|
||
has filled it, and the api does not start. To go back anyway, set `SCHEMA_ACCEPT_DATA_LOSS=true`
|
||
for that one deploy — in `.env` on compose, on the `jarvis-migrate` Job on Kubernetes — then put
|
||
it back. What is lost is only that last answer, which the next report writes again.
|
||
|
||
## 2026-09-25 — The model is chosen in one place, and the form you are not using is out of the way `web 0.137.1`
|
||
|
||
Under **Settings → Platform → Model**, the choice between your own provider and the managed model is
|
||
now made with the selector and nothing else. The separate "Use your own provider" button is gone:
|
||
picking that option in the selector does the same thing, and still asks first when your own provider
|
||
is not configured, because switching would then leave the assistant with no model.
|
||
|
||
Your own provider's form now appears only while the selector is on it. While the managed model is in
|
||
use — or has been picked and is waiting for you to confirm — the form is hidden rather than shown with
|
||
a note saying it is not in use. Its settings, key included, are kept exactly as they were, and come
|
||
back with the form when you switch back.
|
||
|
||
## 2026-09-24 — A licensed instance can choose a managed model, and this is exactly what it sends `api 0.112.0` `web 0.137.0`
|
||
|
||
**No conversation content leaves unless somebody chooses it.** The assistant keeps using your own
|
||
OpenAI-compatible provider after this upgrade, exactly as before. What is new is a second option
|
||
under **Settings → Platform → Model**: a model your Jarvis provider runs for instances with a paid
|
||
or trial licence, with no endpoint and no key of your own to manage. Only a platform administrator
|
||
can choose it, only on that screen, and only after ticking a box under a statement of what leaves
|
||
the instance. No environment variable, licence or server answer selects it, and a community instance
|
||
cannot use it. On a licensed instance, opening that screen asks the licence server whether the
|
||
managed model is offered and which model it is, with a request signed like the licence report that
|
||
carries the product, the licence id, the instance id and public key, and the version — no content.
|
||
|
||
**Once chosen, conversation content goes to your Jarvis provider rather than to your own endpoint.**
|
||
Every request the assistant makes to the model carries each message, the conversation so far, the
|
||
instructions and the list of tools Jarvis gives the model, and the output of every tool the
|
||
assistant ran — command output, file contents and API responses from the systems you administer —
|
||
including runs nobody is watching, such as scheduled wake-ups and runs resumed after a restart; your
|
||
own provider receives the same on every request it answers. It goes over HTTPS to the host your
|
||
licence reports to (`checkin.luxit.be` on current licences), never following a redirect away from
|
||
it, with a short-lived credential your instance obtains by signing a request exactly as it signs its
|
||
licence report, and from there to the model provider named on the settings screen. A relay in front
|
||
of the licence server must also forward `/api/inference/`.
|
||
|
||
**What your provider keeps is metadata, never content**: when, which model, the Jarvis version, the
|
||
request's size, token counts, timings and outcome — each request's record for 90 days, and daily and
|
||
monthly totals for 26 months. **Usage counts against a monthly allowance per licence**, in UTC and
|
||
shared by every instance on the licence. When it is used up, or the licence is no longer in force,
|
||
the assistant refuses new requests with a sentence saying why and, for a spent allowance, when it
|
||
renews. It never falls back to your own provider by itself; everything else in Jarvis keeps working,
|
||
and your own provider's settings, key included, are kept as they were so switching back is one
|
||
click. The [README](README.md#if-you-choose-the-managed-model) has the full statement, including how
|
||
a request whose usage the model did not report is counted.
|
||
|
||
**What earlier entries said about "no conversation content" is still true of the licence report**:
|
||
the report's fields have not changed, and the managed model travels on a separate channel, which
|
||
carries content only once a platform administrator has chosen it.
|
||
|
||
**Also in this release.** The model step of the install wizard says the managed model exists and
|
||
where to choose it later — it cannot be chosen during installation, because an instance is only
|
||
bound to its licence once it has reported, after setup. A run the managed model refuses, or cannot
|
||
reach, now explains itself in the reader's own language in the conversation, with a link to the
|
||
model settings for a platform administrator; a refusal Jarvis decided on its own — a licence no
|
||
longer in force, an identity key it cannot decrypt — is no longer presented as something your
|
||
provider said. When the managed model is waiting on a licence report — an instance not yet bound, or
|
||
a licence your provider has just reactivated — the model settings offer to send one now.
|
||
|
||
**A licence key you remove stays removed.** The licence server answers every report with the licence
|
||
it still has your instance bound to, and the next report used to put a removed key straight back,
|
||
silently. That licence is now taken back only when somebody pastes it on the licence screen — not
|
||
from a report, and not from `JARVIS_LICENSE_KEY` at a restart. Every licence your instance does take
|
||
from a report, a renewal included, is now on the audit trail as `settings.license.adopted`.
|
||
|
||
**A provider that is briefly unable to answer is no longer called a misconfiguration.** A rate limit
|
||
or a 5xx from your own provider, or from a proxy in front of it, was retried and then written into
|
||
the conversation as "a configuration or account fault … it will repeat identically until it is
|
||
fixed". It now says the provider could not answer, how many times Jarvis tried, and that you can ask
|
||
it to continue.
|
||
|
||
**Self-hosters:** api and console. The schema gains five things, applied automatically when the api
|
||
starts: which of the two providers is chosen, when that last changed, which version of the
|
||
disclosure was accepted, on each assistant message which of the two answered it, and which licence
|
||
was last removed on the licence screen. They only add, so the schema guard has nothing to refuse. No
|
||
new environment variable.
|
||
|
||
**Rolling back past this version is not a plain image change.** An older build's schema sync would
|
||
drop `aiProvider`, `aiProviderChangedAt`, `aiManagedDisclosure` and `licenseReleasedId` on the
|
||
instance settings, `aiProvider` on messages, and the `AiProvider` type; the schema guard refuses
|
||
that and the api does not start. To go back anyway, set `SCHEMA_ACCEPT_DATA_LOSS=true` for that one
|
||
deploy — in `.env` on compose, on the `jarvis-migrate` Job on Kubernetes — then put it back. What is
|
||
lost is only the choice itself, the record of the disclosure accepted, which of the two answered
|
||
each message and which licence was removed — no conversation and none of your own provider's
|
||
settings — and the older build answers with your own provider, and takes a removed licence back at
|
||
its next report. Take a dump first either way.
|
||
|
||
## 2026-09-12 — Long conversations open quickly, and you can search inside them `web 0.136.0`
|
||
|
||
A conversation of several hundred turns had become slow in a way that spread past itself. Opening one
|
||
took the better part of half a minute. Once it was open the rest of the console went with it — the
|
||
notification bell, which has nothing to do with the transcript, took fifteen seconds to drop its
|
||
list. The cause was one number: the browser was laying out sixty thousand elements, and every part of
|
||
the app that asked it a question about the page had to wait for all of them.
|
||
|
||
**The transcript now only measures the turns you can actually see.** Everything above and below stays
|
||
in the page, exactly where it is — nothing is unloaded, nothing is fetched again when you scroll back
|
||
— but the browser stops doing the arithmetic for turns that are off screen. On a seven-hundred-turn
|
||
conversation, opening it went from 24 seconds to under 3, and the notification bell from 29 seconds
|
||
to 2.6. The last few turns are deliberately left out of this, so a streaming reply still lands
|
||
exactly where it should.
|
||
|
||
**Coming back to a conversation now puts you back where you were, on the turn you were reading**, not
|
||
at a remembered pixel. The distinction matters now that most of the page is unmeasured: a pixel
|
||
offset means something different every time, and restoring one dropped you tens of turns from where
|
||
you had been. It now remembers the message, which does not move.
|
||
|
||
**There is a find bar for the conversation you are in.** ⌘F, Ctrl+F, or the magnifier in the header —
|
||
at every width, including a phone, where scrolling back through four hundred turns is worst. Type and
|
||
every matching message is marked down the side of the transcript, with the words lit inside the one
|
||
you are on; Enter and Shift+Enter walk forward and back through them and wrap around; Escape closes
|
||
it. The counter counts **messages**, not occurrences, and it is exact: it is computed from the
|
||
conversation itself rather than from what happens to be on screen, so "12 / 35" means thirty-five
|
||
turns mention what you typed, including the ones six hundred rows back. Accents and case are ignored
|
||
in both directions — _deploiement_ finds _déploiement_, and so does _DÉPLOIEMENT_.
|
||
|
||
Your browser's own Ctrl+F still works on this screen if you press it again while our bar has focus.
|
||
|
||
**Self-hosters:** console only. No schema change, no configuration, no new key, nothing to do.
|
||
|
||
## 2026-09-12 — Half the tooltips in the app were the browser's, not Jarvis's `web 0.135.0`
|
||
|
||
Hovering the mark beside a conversation drew a dark rounded bubble with a little arrow, in the
|
||
console's own colours, right away. Hovering a badge on a dock row drew a white square box in the
|
||
system font, a second later, with no arrow and nothing in common with the first one. Both were
|
||
tooltips. They looked like they came from two different programs, because one of them did: the second
|
||
was the browser drawing its own, which no stylesheet can reach.
|
||
|
||
**Every tooltip in the console is now the same tooltip.** Thirty-eight places were still handing the
|
||
job to the browser: the dock's rows, its badges and its pin; the timestamps in the notification list;
|
||
the subject and the due time of a scheduled wake-up in a transcript; what a tool call was aimed at
|
||
and how risky it was; truncated names in the vault, in the documents panel, in the scope picker, in
|
||
the agents and enrolment-token lists; the fields in the audit log; the shell and process id on a
|
||
terminal, and the state of a recorded session; the next run of a schedule; the step labels in the
|
||
installer; the version detail on the update banner; the licence details; the ceiling chips on an
|
||
asset's access row. All of them draw the console's bubble now — same background, same corners, same
|
||
size of text, same arrow, same delay — and they no longer wait a second to appear.
|
||
|
||
**On a phone they now behave like the thing they are attached to.** A browser tooltip never appeared
|
||
on a touch screen at all, so none of this text was reachable there. Where it sits on something you
|
||
tap — a dock row, a conversation link, a menu entry — it still is not, because a bubble that opens on
|
||
tap would race the tap that opens the conversation, and the row prints what matters in its own line
|
||
instead. Where it sits on something inert — a timestamp, a status chip, a step label — tapping it now
|
||
shows the text. That is new.
|
||
|
||
**One mark was drawing two of them.** The tile that shows what Jarvis is doing in a conversation
|
||
raised the console's bubble on hover and then, a second later, the browser's underneath it with the
|
||
same word inside. The second one is gone. What a screen reader announces for that tile is unchanged;
|
||
the name never came from the part that was removed.
|
||
|
||
**The platform mark in the asset wizard now has no tooltip at all**, rather than a better-looking one.
|
||
It repeated the platform's name in a bubble directly above the same name, printed in bold, an inch to
|
||
its right.
|
||
|
||
**And the count of open conversations has gone from the top bar.** It sat next to the notification
|
||
bell and reported how many conversations were on the rail — which the rail says itself, in full, a
|
||
few centimetres away.
|
||
|
||
**Self-hosters:** console only. No schema change, no configuration, no new key, nothing to do.
|
||
|
||
## 2026-09-12 — The lightning bolt on a dock row would not say what it meant `web 0.134.0`
|
||
|
||
A row in the dock can carry a small glyph after its title: a shield, a lightning bolt, or a dot. The
|
||
lightning bolt is the consequential one — it marks a conversation the assistant is allowed to run to
|
||
the end without asking, destructive steps included. There was no way to find that out from the rail.
|
||
Hovering it produced nothing, and a screen reader read the row without mentioning it.
|
||
|
||
**Hovering a badge now names it**, in the rail and in the phone sheet: _Autonomous: runs everything
|
||
without asking, including destructive operations_, _1 plan waiting on you_, _Unsent draft_. The
|
||
wording for the lightning bolt is the same sentence the autonomy setting itself uses, so the badge
|
||
and the control that sets it no longer describe the same permission in two different vocabularies.
|
||
|
||
**And a dock row now announces everything that is true of it**, not only what fitted in the slot.
|
||
There is room on a row for one glyph, so a conversation that is both waiting on a decision and
|
||
allowed to run unattended drew only the shield. The spoken name lists both, plus an unsent draft,
|
||
after the title and the activity.
|
||
|
||
## 2026-09-12 — The dock stayed on one computer, and its menu offered to add a conversation that was already there `api 0.111.0` `web 0.133.0`
|
||
|
||
Two complaints about the same rail, a day apart. They turn out to be the same mistake made twice:
|
||
the dock was described as a place things get put into and taken out of, when by the time you can see
|
||
a conversation's menu the conversation is already in it.
|
||
|
||
**The overflow menu no longer offers to add what is already there.** It read "Pin to the dock" above
|
||
"Remove from the dock" — a matched pair, one the opposite of the other, and neither true. Opening a
|
||
conversation files it in the dock on its own; there was nothing left to add. The menu now has a
|
||
heading that says the conversation is **In the dock**, a **Pinned** line you switch on and off that
|
||
shows you which it currently is, and a separate **Close** underneath. Closing takes the conversation
|
||
off the rail. It does not delete it, and it is not the opposite of pinning — the two were never a
|
||
pair, and now they do not look like one.
|
||
|
||
**The same is true of the rail and the panel**: the button at the end of a row is called _Pinned_ and
|
||
reports its own state, rather than naming an action whose effect you could not see. Screen readers
|
||
announce it as a switch, with its position.
|
||
|
||
**Closing the conversation you are reading now works.** It appeared to do nothing: the dock put the
|
||
open conversation straight back on the rail, so the row you had just closed was redrawn before you
|
||
could see it go. It stays closed, and comes back the moment you open that conversation again.
|
||
|
||
**Your dock follows you between computers.** Pins and recents were kept in the browser, so a
|
||
conversation pinned on the desk machine was not pinned on the laptop, and signing in somewhere new
|
||
gave you an empty rail with no way to tell whether that was right. They are now held on the server,
|
||
per account and per organization, and every window you have open moves together: pin something on one
|
||
machine and the other machine's rail reorders within the second. Pins already on a device are taken
|
||
up once, the first time that browser talks to this release, so nothing you pinned before the upgrade
|
||
is lost.
|
||
|
||
**Self-hosters:** one new table, applied on start-up like every other schema change. It only adds —
|
||
nothing is dropped or narrowed — so the destructive-change guard does not trip and
|
||
`SCHEMA_ACCEPT_DATA_LOSS` stays off. No configuration, and no action on the browsers: the adoption of
|
||
existing pins happens by itself. The dock rides the same websocket the console already holds open, so
|
||
there is no new port and no new outbound connection.
|
||
|
||
## 2026-09-11 — The assistant was reading the first and last pages of a file and nothing in between `api 0.110.0`
|
||
|
||
Anything the assistant read — a document in a conversation's workspace, a config file on one of your
|
||
machines — was cut down to about 12,000 characters before it reached the model. That cut was the
|
||
problem rather than the size: it kept the beginning and the end and deleted the middle, then joined
|
||
the two halves together. What the assistant received looked like a complete file. It read as
|
||
continuous text, with nothing to indicate that 97% of a long document was missing, so the assistant
|
||
had no way to know it was answering about text it had never seen — and neither did you.
|
||
|
||
**`read_document` now returns the document itself.** Up to 60,000 characters by default and 200,000
|
||
on request, always a single unbroken run starting where the assistant asked, with a line at the end
|
||
saying how much is left and where to continue. On a document longer than that the assistant reads it
|
||
in order, page after page, instead of receiving a collage. This matters most for editing: a patch has
|
||
to quote the existing text exactly, and text welded together from two ends of a document cannot be
|
||
quoted.
|
||
|
||
**`ssh_read_file` and `agent_read_file` had the same fault**, plus one of their own: both offered a
|
||
`maxBytes` setting that went up to two megabytes, and everything above roughly 12,000 was discarded
|
||
after being read. They now return what they were asked for — 60,000 bytes by default, 200,000 at
|
||
most — always from the start of the file, with a note saying how much of it is left. Asking for more
|
||
than they can return is now refused with a message, instead of accepted and quietly ignored, and the
|
||
note points at `head`, `tail`, `sed` or `grep` when a file is genuinely too large to read whole.
|
||
|
||
**Every other tool is unchanged.** A command's output, a directory listing, a query result — these
|
||
are still cut at the same place as before, because for a log or a table the beginning and the end are
|
||
usually the answer. The larger budget belongs only to the tools whose result is meant to be a file.
|
||
|
||
**Self-hosters:** nothing to do. No schema, no configuration. Note that a large read now genuinely
|
||
occupies the model's context, which it previously only appeared to — if your `OPENAI_MODEL` has a
|
||
small context window, the assistant will be reading in more, smaller pages.
|
||
|
||
## 2026-09-11 — Pinning a conversation appeared to do nothing `web 0.132.0`
|
||
|
||
The rail on the left of a conversation held two ideas that were never explained apart: a "working set"
|
||
that the product called something else everywhere you actually touched it, and a pin whose effect was
|
||
invisible. Pinning a conversation left the list looking exactly as it had a moment earlier — same
|
||
rows, same order, no mark on the one just pinned — so the only reasonable conclusion was that the
|
||
pin had not worked.
|
||
|
||
**It had worked, and the list was refusing to redraw.** The rail deliberately holds its order still
|
||
for a moment after you touch it, so a row never slides out from under a tap. Pinning was the one
|
||
action where nothing came along afterwards to release the hold: the pin was saved, the freeze was
|
||
applied by the same click, and the rail then went on drawing the pre-pin order for as long as the tab
|
||
stayed open. It now lets go on its own the moment the pause is over.
|
||
|
||
**Pinned conversations have their own section**, above the ones you have merely been in, and a
|
||
conversation stays under "Pinned" even while it is the one you are reading. A pin that moves somewhere
|
||
else when you open it is a pin you cannot find at the moment you go looking for it.
|
||
|
||
**The pin is now visible without hovering.** It was drawn only when the pointer was over the row,
|
||
which on a touch screen means never, and on a mouse means the state of the list changes depending on
|
||
where the cursor happens to rest.
|
||
|
||
**One name for one thing.** "Working set" is gone; the rail, the panel, its tooltips and the remove
|
||
action all say "dock", in English, French and German.
|
||
|
||
**Self-hosters:** nothing to do. Pins and recents are stored in the browser as before, and no pin is
|
||
lost by the upgrade.
|
||
|
||
## 2026-09-11 — Fixing one paragraph meant resending the whole document `api 0.109.0`
|
||
|
||
The assistant could write a document into a conversation and replace its body, but replacing was the
|
||
only edit on offer: correcting a single paragraph of a long runbook meant resending the entire thing,
|
||
including everything that had not changed. On a long document that is expensive, and it leaves room
|
||
for the model to quietly drop a section it was not actually trying to touch.
|
||
|
||
**`read_document`** lets the assistant look at a document's current content before touching it, rather
|
||
than acting on what it remembers having written earlier in the conversation.
|
||
|
||
**`patch_document`** replaces one exact piece of text with another, so a correction no longer requires
|
||
resending the rest of the document unchanged. It refuses to guess: if the text to replace does not
|
||
appear, or appears more than once, nothing is changed.
|
||
|
||
**Self-hosters:** nothing to do. No schema or configuration changed — the assistant simply has two
|
||
more tools available in every conversation.
|
||
|
||
## 2026-09-08 — "Last active" told you someone had been seen, not that they had said anything `api 0.108.0` `web 0.131.0`
|
||
|
||
The per-user table on the Statistics page counted what each account had written, and showed when that
|
||
account was last active. Those two are further apart than they look. Being active is bumped by any use
|
||
of the product at all, so an account opened every morning to read reports that it was here today —
|
||
and says nothing about whether the person has typed since March. On a screen whose job is to show who
|
||
is actually using the instance, that gap is usually the thing being looked for.
|
||
|
||
**A "Last message" column**, sitting next to the message count it belongs to: when each account last
|
||
wrote, or "Never" if it never has. On a narrow screen it becomes its own labelled line on the card,
|
||
rather than a second bare "3 days ago" beside the first one, which would have named neither.
|
||
|
||
**This one column does not sort**, and the reason is worth stating rather than hiding. Every other
|
||
column there is ordered by the database while it selects the page; this one is worked out afterwards,
|
||
for the fifty accounts that page already chose. That is what keeps it from costing anything on a large
|
||
instance, and it is also what leaves the server nothing to order by. "Last active" beside it still
|
||
sorts, and answers the same question more coarsely.
|
||
|
||
**Self-hosters:** one index on the messages table is replaced by a wider one leading on the same
|
||
column — the old one is dropped and the new one built in its place, automatically when the api starts,
|
||
like every schema change here. No column is dropped and nothing becomes unique, so the schema guard
|
||
has nothing to refuse. On an instance with millions of messages the build is not instantaneous, and
|
||
the api finishes starting when the index finishes.
|
||
|
||
## 2026-09-08 — Nobody running Jarvis could tell how much it was actually being used `api 0.107.0` `web 0.130.0`
|
||
|
||
A platform administrator had no way to answer "is this instance quiet or busy, growing or stalled,
|
||
used by three people or by thirty" — only the individual screens for organizations, conversations and
|
||
agents, none of them a total, none of them over time.
|
||
|
||
**A new "Statistics" page**, under Settings, visible only to a platform administrator. It shows the
|
||
instance as a whole: how many users, organizations, assets and agents exist right now, how many
|
||
conversations and messages have been sent, and how each breaks down — assets by type, agents by
|
||
status, messages by who sent them.
|
||
|
||
**Trends, not just totals.** A day-by-day chart of new users, new conversations and new messages, over
|
||
the last week, month, quarter or year — each on its own scale, because a shared axis flattens a
|
||
handful of new users next to a few thousand messages into a line that says nothing.
|
||
|
||
**Who is actually using it.** A sortable, searchable table of every user, ranked by conversations
|
||
owned and messages sent, alongside when they last did anything and when they last signed in — the list
|
||
an administrator reaches for when a licence renewal asks whether a seat is worth keeping.
|
||
|
||
Two new database indexes ship with this release, applied the same way every schema change here is:
|
||
automatically, when the api starts. They only add — nothing is dropped, nothing becomes unique, so the
|
||
schema guard has nothing to refuse.
|
||
|
||
No new environment variable, no new permission: an existing platform administrator sees the page the
|
||
day this lands, anyone else does not see it in the menu, and a direct API call is refused.
|
||
|
||
## 2026-09-04 — An approval asks for your decision once, not twice `web 0.129.0`
|
||
|
||
**A request waiting on you carried Approve and Reject on the card itself, and pressing either one
|
||
opened a dialog that asked the same question over again.** Two consents for one decision — and the
|
||
second one explained consequences the first had already accepted. It read as a confirmation step. It
|
||
was really the only place the plan had ever been shown in full.
|
||
|
||
So the card decides nothing now. A pending request is a single row: its risk, its title, the first
|
||
two lines of what Jarvis intends to do, and how long it has been waiting, how many steps it runs and
|
||
how many machines it touches. Opening the row opens the request, and that is where the decision
|
||
lives — the whole plan, every step in order, the machines by name, what approving authorises and
|
||
what rejecting does, the note if you want to leave one, and the two buttons. The note is still
|
||
optional on both, and it is still recorded with the decision.
|
||
|
||
**Reading the plan is no longer gated on being able to act on it.** Somebody with view-only access
|
||
opens the same request and reads the same thing; they simply have nothing to press. That mattered
|
||
more than it sounds: the plan used to sit on the card, so folding the card down would have hidden it
|
||
from exactly the people who can only read.
|
||
|
||
Approvals already decided are unchanged — they still expand where they sit, with who decided and any
|
||
note they left. So is the approval Jarvis raises mid-conversation for a single command: that one is
|
||
one question about one thing, and it is still answered where it is asked.
|
||
|
||
No API change; take the web image on its own.
|
||
|
||
## 2026-09-01 — One byte in a command's output could destroy the turn that read it `api 0.106.0`
|
||
|
||
**If you have been seeing `Invalid prisma.message.update() invocation` in conversations, this is
|
||
it.** A single character in what a command printed made the assistant's turn impossible to store,
|
||
and the run died at the write — after the work had already been done on your machines. Reported this
|
||
morning, reproduced against a database within the hour.
|
||
|
||
Two characters do it, and neither is exotic:
|
||
|
||
- **A null byte.** PostgreSQL refuses it outright, in text and in JSON alike. It turns up in the
|
||
output of perfectly ordinary commands — reading a file with embedded nulls, `find -print0`, a
|
||
journal or LDAP dump, anything that touches something binary.
|
||
- **Half of an emoji.** This one was ours. Long output is shortened by cutting it, and the cut was
|
||
made without checking whether it landed in the middle of a character. An emoji in a log line, at
|
||
an unlucky offset, was enough to lose the turn that read it.
|
||
|
||
Neither carries any information, so both are now removed before anything is stored — as close to the
|
||
source as possible, so what the assistant reads, what you see and what is written down are the same
|
||
text. Tabs, carriage returns, ANSI colour and whole emoji are untouched; a character that was cut in
|
||
half shows as `�`, which is the truth about it.
|
||
|
||
**And a failed run no longer leaves a tool card spinning for ever.** The same report showed one that
|
||
had been turning for eighty-five minutes, above a turn that had already said it failed: closing a
|
||
run wrote its text but never its tool calls, so a call it never returned to kept the status it was
|
||
dispatched with. Those calls are now closed as cancelled, saying plainly that the command may have
|
||
reached the machine and its outcome is unknown — except one still waiting for an approval, which is
|
||
told it never ran, because it never did. That matters beyond the screen: the assistant reads those
|
||
rows back on the next turn, and "still running" makes it wait for an answer that cannot come.
|
||
|
||
No schema change, no console change. Take the api image.
|
||
|
||
## 2026-09-01 — Jarvis administers Kubernetes clusters `api 0.105.0` `web 0.128.0`
|
||
|
||
**Register a cluster the way you register a server, and ask for the work in words.** A Kubernetes
|
||
cluster is now a kind of asset, under **Virtualization & containers**, reached either by pasting a
|
||
kubeconfig into the vault or by pointing Jarvis at the API server with a ServiceAccount token. Any
|
||
conformant cluster: k3s, RKE2, kubeadm, EKS, AKS, GKE.
|
||
|
||
**What it can read.** Workloads (pods, deployments, statefulsets, daemonsets, replicasets, jobs,
|
||
cronjobs), nodes, namespaces, networking, storage, events, pod logs — including the previous
|
||
container's log, which on a CrashLoopBackOff is the only copy holding the error — live CPU and
|
||
memory where metrics-server is installed, and the Helm releases with their revision and status. The
|
||
output is shaped like `kubectl`, because that is what everyone reading the transcript already knows.
|
||
|
||
**What it can do.** Scale, restart a rollout, delete a pod so its controller replaces it, cordon and
|
||
uncordon a node, drain one, suspend and resume a CronJob. Draining uses the eviction API, so a
|
||
PodDisruptionBudget refuses an eviction that would take a service below its floor instead of the
|
||
drain quietly ignoring it.
|
||
|
||
**And, for what no fixed list covers:** read any API path (which is how custom resources —
|
||
cert-manager, Argo CD, CloudNativePG — are reached), apply a manifest with server-side apply, and
|
||
run a command inside a container. Those three are marked as tools whose operation the assistant
|
||
composes itself, so an access grant has to admit them deliberately rather than inheriting them from
|
||
a risk level.
|
||
|
||
**Risk is judged per call, not per tool.** Scaling is an ordinary change; scaling to zero takes the
|
||
service down and asks first. An apply is an ordinary change; an apply carrying RBAC, an admission
|
||
webhook, a CRD or anything at all into `kube-system` asks first, and says which of those it found.
|
||
Deleting anything always asks. Applying a namespace with the deployment that goes in it does not —
|
||
an approval on every deploy is an approval nobody reads.
|
||
|
||
**Two things it will not do.** No tool reads a Secret: the resource lists exclude them, and the Helm
|
||
tool reads the release index rather than decoding a release, which would carry the values a chart was
|
||
installed with. And a kubeconfig whose credential comes from running a program (`exec` /
|
||
`auth-provider` — the EKS and GKE default) is refused by name, with the ServiceAccount route named as
|
||
the answer, rather than failing later as an authentication error.
|
||
|
||
**Testing a connection records what the cluster is** on the asset: version, distribution, node count,
|
||
total capacity and node addresses.
|
||
|
||
Take both images. The schema gains one enum value and the migration is uneventful.
|
||
|
||
## 2026-08-31 — The Tasks panel now looks like the rest of the workspace `web 0.127.0`
|
||
|
||
**And its actions menu is reachable with a mouse again**, which is the part that was not merely
|
||
cosmetic. Pause, Run now and Cancel are revealed by hovering the card — and the card was missing the
|
||
one class that makes that work, so on any device with a pointer the menu was invisible and those
|
||
three actions were keyboard-only. Nobody would have found them.
|
||
|
||
The rest is how it sits beside Documents and Approvals. It did not: a hairline in the wrong token,
|
||
brighter than every other border in the panel; no card background; and a two-column list of labels
|
||
nothing else in the workspace uses. Tasks now renders the same card as an approval — status, title,
|
||
when it next fires, then the instruction — grouped under the same section headings, from the same
|
||
component rather than a copy of it.
|
||
|
||
Two things are easier to see as a result. A wake-up the platform **switched off** after three
|
||
failures is bordered in red and sorted to the top of the inactive group, instead of sitting below
|
||
the finished ones; its symptom is silence, so it needs to be the card you notice. And the
|
||
instruction is no longer clipped to two lines — it is one or two sentences Jarvis wrote to itself,
|
||
and the clipped half was usually the half saying what "check it" means.
|
||
|
||
No API change; take the web image on its own.
|
||
|
||
## 2026-08-31 — A wake-up no longer arrives under your name `api 0.104.0` `web 0.126.0`
|
||
|
||
**When a scheduled wake-up fired, the transcript showed it as a message you had sent.** Your avatar,
|
||
your name, and twenty lines of preamble written for the model — for something you never typed, at
|
||
whatever hour the clock came round. Reported within an hour of the feature shipping, and it was the
|
||
whole of what a transcript is for: knowing who said what.
|
||
|
||
A wake-up is now its own kind of row. It sits in the middle of the thread rather than on either
|
||
side, under a header saying which wake-up fired and when it was due, with one thing in the body:
|
||
the instruction Jarvis left itself. The scaffolding that turn actually ran on is folded away behind
|
||
**Show the note sent to Jarvis** — still there when a wake-up behaves oddly and you want to see
|
||
exactly what it was told, and out of the way the rest of the time. Wake-ups never merge into the
|
||
message above them either, so the header cannot be lost to grouping.
|
||
|
||
The same is true on a share link. A guest sees the same card; what reaches them is the title, the
|
||
time and the instruction, and not the internal id of the wake-up.
|
||
|
||
Underneath, the row is still recorded as coming from the person who owns the wake-up, because that
|
||
is whose access the turn runs with and it is how a wake-up interrupted by a restart is picked up
|
||
again. What changed is that the console no longer reads that as authorship.
|
||
|
||
**Nothing to do on the way in.** The schema gains one nullable column and the migration is
|
||
uneventful. Notes already in your transcripts keep rendering the old way — the structure is written
|
||
when a wake-up fires, so they have none; anything that fires from now on gets the card.
|
||
|
||
## 2026-08-31 — A scheduling loop, fixed at the shape rather than at the wording `api 0.103.0`
|
||
|
||
**If Jarvis got stuck trying to schedule something, this is the fix.** Reported within the hour:
|
||
asked to send a message a minute later, it tried four times, each time announcing it would correct
|
||
itself and each time sending the same thing. No wake-up was created, and every attempt showed as
|
||
"Succeeded" in the transcript.
|
||
|
||
Two mistakes of ours, and neither was in the scheduling itself.
|
||
|
||
**The tool asked for a choice it then punished.** It took an optional "repeat pattern" and an
|
||
optional "single time", and refused unless exactly one was given — a rule the assistant had to
|
||
enforce on itself, got wrong, and then could not recover from. It now takes **one** value for when
|
||
to wake, and the shape says which kind it is: a timestamp wakes it once, a five-field pattern
|
||
repeats. There is no longer a combination to get wrong. The value it kept sending was, as best we
|
||
can tell, a fragment of an example in our own field description — so the documentation was feeding
|
||
the mistake the tool was refusing.
|
||
|
||
**A refused call reported success.** The transcript showed four green rows for four calls that did
|
||
nothing, which is the opposite of what a transcript is for. A wake-up that cannot be created now
|
||
fails and says so on its row.
|
||
|
||
Nothing about existing wake-ups changes and there is no schema change. Take the api image; the
|
||
console is unaffected.
|
||
|
||
## 2026-08-31 — Jarvis can wake itself up, knows what day it is, and can read the web `api 0.102.0` `web 0.125.0` `browser 0.1.0`
|
||
|
||
**The schema step is uneventful for almost everyone** — unlike `api 0.101.0`. Read the upgrade
|
||
note below only if you created a scheduled task through the Settings screen this afternoon.
|
||
|
||
### Taking this build: what the schema step does
|
||
|
||
Reshaping scheduled tasks around the conversation drops the columns the organization-wide version
|
||
needed. **On almost every instance this passes without stopping**, and that is not luck: the guard
|
||
refuses a column drop when the table has rows in it, and `Schedule` is empty on every instance that
|
||
upgraded from `0.100.0` or earlier — the table only existed from `0.101.0`, published a few hours
|
||
before this. Watched on our own instance: dropped six columns and an enum, no warning, no
|
||
intervention.
|
||
|
||
**Read the rest of this only if that table is not empty**, which means you created a scheduled task
|
||
through the Settings screen that this release removes:
|
||
|
||
```sql
|
||
SELECT count(*) FROM "Schedule";
|
||
```
|
||
|
||
If it returns anything but 0, the migration will refuse and print what it would destroy. Those rows
|
||
are wake-ups you made today; note down what they were, then let it through **once** and put the
|
||
guard back:
|
||
|
||
- **compose** — set `SCHEMA_ACCEPT_DATA_LOSS=true` in `.env`, `docker compose up -d`, wait for the
|
||
api, then remove the line. Leaving it set would let a genuinely destructive change through
|
||
unannounced on some future release.
|
||
- **Kubernetes** — set the same variable on the `jarvis-migrate` Job, run it to completion, then take
|
||
it off. The api pods keep serving the old build while the Job fails, so there is no outage.
|
||
|
||
Then recreate them by asking Jarvis inside the conversation you want them to run in — which is where
|
||
they now live.
|
||
|
||
### Scheduled tasks are now part of a conversation
|
||
|
||
`api 0.101.0` shipped these a few hours ago as an organization-wide screen, and that was the wrong
|
||
shape. **The "Scheduled work" section and the `schedule:view` / `schedule:manage` permissions are
|
||
gone**; if you had started using them, nothing is silently broken — but read on, because what
|
||
replaced them is not in the same place.
|
||
|
||
The thing people actually want this for needs the conversation. Two cases, and both:
|
||
|
||
- **Waiting.** A rebuild is running, a DNS change is propagating, a maintenance window opens at
|
||
03:00. Jarvis leaves itself a note and picks the work up later with everything it had already
|
||
established still in front of it.
|
||
- **A standing check.** "Look at that file every day at noon." Every occurrence lands in the same
|
||
thread, so the seventh day can say "unchanged since Tuesday" instead of reporting into a vacuum.
|
||
|
||
The old version opened a _fresh_ conversation for each occurrence — which meant "carry on where you
|
||
left off" was impossible, because the woken assistant arrived knowing nothing. Now a wake-up belongs
|
||
to one conversation, wakes back into it, and appears under a new **Tasks** tab in that
|
||
conversation's workspace, beside the documents and the approvals. You pause, run and cancel from
|
||
there; you create one by asking ("check this again tomorrow morning"), because the instruction is
|
||
written with the conversation around it.
|
||
|
||
**It runs as whoever scheduled it, checked when it fires rather than when it was written.** If
|
||
somebody's access to a machine is revoked, the work they left behind stops reaching it the same day
|
||
and says so on its row. Three failures in a row switch a wake-up off, so a broken one becomes a
|
||
thing you find rather than a notification you learn to ignore.
|
||
|
||
Wake-ups inherit the conversation's autonomy and its asset scope; they cannot raise either.
|
||
|
||
### Jarvis knows what the date is
|
||
|
||
It did not before, and it did not know that it did not. Asked what day it is, a language model
|
||
answers from its training data — confidently, and usually months out. Everything an administrator
|
||
asks is dated: "the logs from the last week", "has this certificate expired", "wake me tomorrow at
|
||
noon". Each of those was a subtraction from a number nothing was giving it.
|
||
|
||
The current time is now stated at the start of every turn, and there is a `current_time` tool for
|
||
reading the clock during a long run and for converting into a customer's time zone. Nothing to
|
||
configure. If you have ever been given a date that was quietly wrong, this is why.
|
||
|
||
### Jarvis can read the web — if you turn it on
|
||
|
||
Two new abilities: search the web, and open a page and read it. A real browser, so pages that build
|
||
themselves with JavaScript work — vendor advisories, status pages, release notes, consoles.
|
||
|
||
It also fixes the same class of problem as the clock: a model's knowledge has a cut-off. Versions,
|
||
end-of-life dates, advisories and prices are exactly what it will otherwise answer from memory and
|
||
get wrong, so it is now told to look things up instead.
|
||
|
||
**It is off by default, and turning it on is a deliberate act.** An assistant that can read the
|
||
internet is a new kind of input into a system that administers your customers. To enable it:
|
||
|
||
- **compose** — `COMPOSE_PROFILES=browser docker compose up -d`, and set `BROWSER_URL=http://browser:3000`.
|
||
|
||
The browser image is on the `dev` channel and pinned by version in the compose file, not on
|
||
`stable` like the other three. That is deliberate and not an oversight: promotion to `stable` means
|
||
"this has run on a canary", and no canary deploys a browser — it is opt-in there too. So it is
|
||
pinned to bytes that exist rather than to a channel that does not.
|
||
|
||
- **Kubernetes** — uncomment `09-browser.yaml` in `kustomization.yaml` and `BROWSER_URL` in
|
||
`02-config.yaml`.
|
||
|
||
With it off, the two tools report themselves unavailable and nothing else changes.
|
||
|
||
**Chromium runs in its own container, never in the api.** The api holds your vault key and talks to
|
||
your database; a browser's whole job is to run code written by strangers. Keeping them apart is what
|
||
stops a web page being used to read your database, your cache or your cloud provider's metadata
|
||
endpoint. The browser container holds no secret, no volume and no database credentials, and the
|
||
Kubernetes manifest ships a NetworkPolicy that denies it every private address range. **That policy
|
||
needs a CNI that enforces NetworkPolicy** — Calico, Cilium and most managed clusters do; plain
|
||
flannel does not, in which case the in-process address check is the only control and it cannot
|
||
survive a redirect.
|
||
|
||
**What comes back is treated as untrusted.** Page text is delimited and labelled as data rather than
|
||
instruction, because a page can be written specifically to be read by an assistant with access to
|
||
your systems. The tools only read: no clicking, no forms, no signing in.
|
||
|
||
## 2026-08-31 — Jarvis works while you are asleep, and can finally administer Exchange `api 0.101.0` `web 0.124.0`
|
||
|
||
Two capabilities, and both are about the half of managed services that does not happen in a
|
||
conversation: the work that is on a calendar, and the half of Microsoft 365 that Graph does not
|
||
reach.
|
||
|
||
**The schema step stops once on the way in — see the upgrade note directly below.**
|
||
|
||
### Taking this build: the schema step stops once, on purpose
|
||
|
||
**Read this before you pull.** The migration refuses the first time and says so, and on compose that
|
||
leaves the API down until you act. It is not a bug and there is nothing wrong with your data — it is
|
||
the data-loss guard doing its job over a change that happens to be harmless.
|
||
|
||
You will see:
|
||
|
||
```
|
||
⚠️ There might be data loss when applying the changes:
|
||
• A unique constraint covering the columns [scheduleId,scheduledFor] on the table
|
||
Task will be added. If there are existing duplicate values, this will fail.
|
||
Error: Use the --accept-data-loss flag …
|
||
```
|
||
|
||
The guard cannot know that the constraint is harmless; it only knows a unique index _could_ fail on
|
||
existing duplicates. **The `Task` table has never had anything writing to it** — it was a placeholder
|
||
until this release gave it a producer — so on any instance upgrading from an earlier build it is
|
||
empty, and adding the index to an empty table cannot lose anything. Check it yourself if you would
|
||
rather not take our word for it:
|
||
|
||
```sql
|
||
SELECT count(*) FROM "Task"; -- expected: 0
|
||
```
|
||
|
||
Then let it through **once**, and put it back afterwards:
|
||
|
||
- **compose** — set `SCHEMA_ACCEPT_DATA_LOSS=true` in `.env`, `docker compose up -d`, wait for the
|
||
api to come up, then remove the line again. Leaving it set would let a genuinely destructive
|
||
schema change through unannounced on some future release.
|
||
- **Kubernetes** — set the same variable on the `jarvis-migrate` Job, run it to completion, then
|
||
take it back off. The API pods keep serving the old build while the Job fails, so there is no
|
||
outage — the rollout simply waits.
|
||
|
||
If that query returns anything other than 0, stop and ask before forcing it.
|
||
|
||
### Scheduled work
|
||
|
||
Everything Jarvis did until now happened inside the turn that asked for it. That is right for an
|
||
investigation and wrong for the licence reconciliation on the first of the month, the mailbox audit
|
||
every Monday, or the service restart at 03:00 on Sunday because that is when nobody is using it.
|
||
|
||
**A new section, "Scheduled work".** Write an instruction, choose when it runs — once, or on a
|
||
repeating pattern — and it runs on its own, in a conversation you can read afterwards. You can also
|
||
just ask: "check the backups every morning at seven" now works in a conversation, and what Jarvis
|
||
writes down is an ordinary row on the same screen.
|
||
|
||
**Times are yours, not the server's.** A repeating schedule is stored with the time zone you wrote
|
||
it in, so 02:00 stays 02:00 when the clocks change. The form shows you the next three runs it will
|
||
actually make, because a cron expression is otherwise something you find out about afterwards.
|
||
|
||
**It runs as the person who scheduled it, and that is checked when it fires — not when it was
|
||
written.** If somebody's access to a machine is revoked, the work they left behind stops reaching
|
||
that machine the same day and says so on its row. Nothing about their rights is stored on the
|
||
schedule, so there is no standing permission to forget about.
|
||
|
||
**Nobody is at the keyboard, and Jarvis behaves accordingly.** An occurrence that would stop for
|
||
your approval has nobody to ask: a scheduled _instruction_ waits in the transcript as usual, and a
|
||
scheduled _single operation_ is refused with a sentence telling you to raise the conversation's
|
||
autonomy or narrow the operation. It will not sit there for an hour and then fail anyway.
|
||
|
||
**Failures are bounded so they stay readable.** One failure changes nothing — a tenant throttled on
|
||
Tuesday is usually fine on Wednesday. Three in a row switch the schedule off and mark it, because
|
||
the thirtieth identical failure is one nobody reads, and a switched-off schedule is what makes
|
||
somebody go and fix the credential.
|
||
|
||
**Two new permissions**, `schedule:view` and `schedule:manage`. Viewers can see what the
|
||
organization has scheduled; operators and above can write and cancel. Neither reaches a machine on
|
||
its own — what an occurrence may actually do is still the asset grant of whoever owns it.
|
||
|
||
**Worth knowing before you use it:** scheduled work is visible to everyone in the organization who
|
||
can see the section, including the conversation each occurrence writes into. If you pin a schedule
|
||
to an existing private conversation, that conversation's name becomes visible alongside it. Leave
|
||
the schedule to open its own conversation each time if that matters to you.
|
||
|
||
**Nothing to configure.** The schema change is applied by the usual migrate step; there is no new
|
||
environment variable and no new service.
|
||
|
||
### Exchange Online
|
||
|
||
Jarvis reaches Microsoft 365 through Microsoft Graph, and Graph does not reach Exchange. Mail flow
|
||
and transport rules, connectors, accepted domains, journaling, retention and litigation hold,
|
||
mailbox and calendar permissions, shared and resource mailboxes, anti-spam and anti-phishing policy,
|
||
address lists, message trace — none of it has a Graph endpoint. Until this build, Jarvis could
|
||
manage users and licences in a tenant and could not touch any of that.
|
||
|
||
**It now runs the Exchange Online PowerShell module**, which is the only administrative surface
|
||
Microsoft supports for those areas. It uses the same app registration you already configured for the
|
||
Microsoft 365 connection — including one that authenticates with a client _secret_, which the
|
||
documented Microsoft recipe does not allow for app-only Exchange.
|
||
|
||
**You have to grant it two things, and only the first looks like a normal permission.** On the app
|
||
registration, add `Exchange.ManageAsApp` as an **application** permission and grant admin consent.
|
||
Then assign the application's service principal a directory role Exchange honours — **Exchange
|
||
Administrator** to make changes, **Global Reader** to read only. Graph consent alone authenticates
|
||
perfectly and is then refused by every cmdlet, which is the usual way this ends up looking broken;
|
||
`m365_check_permissions` cannot see either of these, because neither is a Graph permission. **Test
|
||
connection** on the tenant now tells you whether the PowerShell side is available at all.
|
||
|
||
**The two new operations are treated as unrestricted**, like the raw Graph request and the shell
|
||
commands: a person needs the "unrestricted operations" switch on their asset grant to use them, and
|
||
that switch is off by default. This is deliberate and worth taking seriously — an Exchange script is
|
||
as open-ended as a shell command, and the risk classification in front of it is a list of cmdlet
|
||
verbs that cannot be complete.
|
||
|
||
**The api image is about 210 MB larger** because it now carries PowerShell 7 and the module. If you
|
||
mirror images or run on constrained storage, this is the change to plan for. Nothing else about
|
||
running the stack changes, and every existing tool works exactly as before on an image without it —
|
||
only the two Exchange operations depend on it.
|
||
|
||
## 2026-08-31 — Nothing in the console changes, and that is the whole of it `web 0.123.0`
|
||
|
||
**This is the same console as `web 0.122.0`.** No behaviour changed, no bug was fixed, nothing in
|
||
the interface moved. The two images differ only in the version they report about themselves.
|
||
|
||
**It exists to exercise a rollout end to end**, against a cluster that is actually serving: a new
|
||
digest pinned, the pods replaced one at a time under a disruption budget, the previous ones still
|
||
answering until the new ones are ready. A rollout path nobody has run is a rollout path nobody
|
||
knows about, and the honest way to learn it is on a build where being wrong costs nothing.
|
||
|
||
**Nothing to do.** If you are on 0.122.0 there is no reason to take this, and if you take it there
|
||
is nothing to notice. The next entry describes a real change.
|
||
|
||
## 2026-08-29 — Keep several conversations on the go, and switch between them `api 0.100.0` `web 0.122.0`
|
||
|
||
Opening a conversation used to mean being _in_ it: to reach another you left it, found the one you
|
||
wanted in the list, and went in. That is a poor deal when Jarvis runs unattended for twenty minutes
|
||
against a real machine — the thing you most need to know is that a plan is blocked in a thread you
|
||
are not looking at.
|
||
|
||
**The console now keeps a working set.** The conversations you are actually in appear in the sidebar,
|
||
under the main navigation, alongside anything of yours that is running or waiting on a decision. It
|
||
costs no width — the transcript is exactly as wide as it was — and it shows nothing at all until you
|
||
have opened something. On a phone the same list is one tap away from the top bar and from the
|
||
navigation drawer.
|
||
|
||
**A blocked plan is decidable without opening it.** The row carries the plan's own title, its
|
||
summary, its risk, the machines it names and how long it has been waiting. Choosing it lands you on
|
||
the transcript with the approval already open, rather than on a transcript with the decision hidden
|
||
behind a panel.
|
||
|
||
**Switching is two keystrokes.** `Ctrl+K` (or `Cmd+K`) now opens with your recent conversations
|
||
first and the previous one already selected, so `Ctrl+K` then `Enter` goes back to it. `Ctrl+\`
|
||
toggles between the last two. You can pin up to five conversations so they stay in place.
|
||
|
||
**Your browser tab is finally named after what is in it** — the conversation and the client, rather
|
||
than "Jarvis" on every tab. If you keep two tabs open on two clients, this is the change you will
|
||
notice first.
|
||
|
||
### Two things that were wrong, and are fixed here
|
||
|
||
**Activity was announced to everyone in the organization.** The "Jarvis is working" marks in the
|
||
conversation list were broadcast to every member of an organization, for every conversation in it —
|
||
including private conversations they take no part in and cannot open. Nothing in the interface ever
|
||
displayed them, but the identifiers, the live state and the _name of the colleague running the
|
||
thread_ were delivered to browsers that had no business receiving any of it. They now go only to the
|
||
people who take part in the conversation, and organization membership is re-checked at the moment of
|
||
sending rather than trusted to a stale row. **Take both images to close this**; the api is the half
|
||
that matters.
|
||
|
||
**A half-typed prompt could follow you into another conversation.** Leaving a conversation
|
||
mid-sentence did not discard the text — it carried it into whichever conversation you opened next,
|
||
in a box that looked empty. Where one operator works across several clients, that is a command
|
||
written for one machine sitting one keypress away from being sent to another. Drafts are now kept
|
||
per conversation, survive a reload, and are cleared when the message is sent.
|
||
|
||
Two smaller ones alongside them. **Deleting a conversation now takes anybody reading it off the
|
||
screen** and says why, instead of leaving them on a transcript whose every control has quietly
|
||
stopped working. And **signing out now clears what the console left on the machine** — the
|
||
notification history, which conversations had been read, and any unsent drafts. Your theme, language
|
||
and sidebar preference are deliberately kept: a preference is not a secret.
|
||
|
||
**Nothing to do.** Take both builds. No new settings, no schema change.
|
||
|
||
## 2026-08-29 — The conversation list shows what is running, wherever it is running `api 0.99.0`
|
||
|
||
**Only affects deployments running more than one copy of the api.**
|
||
|
||
A conversation could sit in the list with no activity mark while Jarvis was, at that moment, running
|
||
a tool in it — and opening that conversation showed the real state straight away. Both views were
|
||
honest about what they asked; only one of them was asking about the whole deployment.
|
||
|
||
The list is fed by a live stream of changes, so a browser that connects part-way through is first
|
||
sent a summary of what is already in progress. That summary was assembled from whichever replica the
|
||
browser happened to connect to, and it only knew about the runs it was executing itself. With three
|
||
replicas the list was right about a third of the time, and everybody's browser re-rolled that when a
|
||
proxy restart made them all reconnect at once.
|
||
|
||
The summary now comes from the database, which knows about every replica's runs and already tracks
|
||
which of them are still alive. A conversation being worked on elsewhere shows as thinking until its
|
||
next update arrives — a moment later — rather than showing as idle for as long as the work lasts.
|
||
|
||
A replica that is killed mid-run also stops appearing on its own, so nothing can leave a permanent
|
||
spinner in your list.
|
||
|
||
**Nothing to do.** Take the build.
|
||
|
||
## 2026-08-28 — A machine that moves between replicas lets go of the old one `api 0.98.0`
|
||
|
||
**Only affects deployments running more than one copy of the api.** Nothing here changes a single-
|
||
container stack.
|
||
|
||
**Reconnections are tidied up immediately instead of being waited out.** When a machine reconnects
|
||
to a different replica, the one it left still held its old connection until its own liveness check
|
||
noticed — up to thirty seconds in which two replicas each believed they could talk to the same
|
||
machine, and a command could be sent down the connection the machine had already abandoned. The
|
||
replica that takes over now tells the other one to let go, straight away and precisely: only the
|
||
connection that was actually replaced is closed.
|
||
|
||
**And you can now see what the presence machinery is doing**, at
|
||
`GET /api/admin/agents/presence`, for platform administrators. It reports five counters, added up
|
||
across every replica rather than only the one that answered your request:
|
||
|
||
- how often a machine's connection moved from one replica to another;
|
||
- how often a closing connection was found to have already been replaced, and correctly ignored;
|
||
- how often a command followed a machine that moved instead of failing;
|
||
- **how often a command could not be delivered at all** — this is the one worth alerting on, because
|
||
it is what someone experiences as "the agent is not connected";
|
||
- how often a stored status had to be corrected because it said a live machine was offline. This
|
||
should sit at zero; if it does not, please tell us.
|
||
|
||
It is JSON, every counter only ever goes up, and a replica that has stopped reporting is excluded
|
||
from the totals and counted separately — so you can point a scraper at it with a service account.
|
||
There is deliberately no unauthenticated `/metrics` endpoint: your replica count and machine totals
|
||
are facts about your estate, not about Jarvis.
|
||
|
||
**Nothing to do.** Take the build.
|
||
|
||
## 2026-08-28 — Machines no longer flicker offline when they reconnect `api 0.97.0`
|
||
|
||
**Only affects deployments running more than one copy of the api.** On Docker Compose one of these
|
||
could still happen — a machine reconnecting to the single container — and the fix applies there too,
|
||
but the wave of them does not.
|
||
|
||
**The one to read if you run several replicas.** When something in front of Jarvis closes a batch of
|
||
agent connections at once — a rollout of your ingress controller, a proxy being replaced, a load
|
||
balancer reloading — every affected machine reconnects within a second. That is normal and the
|
||
agents handle it. Jarvis did not: the reconnections raced the bookkeeping for the connections they
|
||
were replacing, and machines that were connected and working were recorded as offline. Calls to them
|
||
were refused with "the agent is not connected" while a terminal to the same machine was open.
|
||
|
||
Four things were wrong underneath, and all four are fixed:
|
||
|
||
- **A connection that had already been replaced still did the bookkeeping for its own ending** —
|
||
including writing "offline" about a machine whose new connection was already live.
|
||
- **Two connections from the same machine to the same replica could not be told apart**, so the old
|
||
one's cleanup deleted the new one's registration, and the old one's keep-alive timer kept a dead
|
||
registration alive.
|
||
- **Registration was published without waiting**, leaving a moment where a replica held a working
|
||
connection that none of the others could see.
|
||
- **A command sent to the replica that held the machine a moment ago gave up** instead of asking
|
||
again where it had moved. It now retries once, so a reconnection costs a pause rather than an
|
||
error.
|
||
|
||
**What you should see afterwards.** Reconnections still happen when proxies restart — that part is
|
||
not something Jarvis can prevent — but they stop producing false "offline" states, and commands in
|
||
flight during one survive it.
|
||
|
||
**Nothing to do.** Take the build.
|
||
|
||
## 2026-08-28 — Models other than OpenAI's work again `api 0.96.0`
|
||
|
||
**Read this if you point Jarvis at anything other than OpenAI.** Self-hosted models, Qwen, Llama,
|
||
Mistral, anything behind LiteLLM or a similar gateway. If you use OpenAI, nothing here changes for
|
||
you.
|
||
|
||
A conversation in which the assistant had used a tool would fail from that point on, with the
|
||
provider answering:
|
||
|
||
```text
|
||
400 System message must be at the beginning
|
||
```
|
||
|
||
and failing the same way on every retry and every later message in that conversation. There was no
|
||
way round it except starting a new conversation and avoiding tools, which is most of what Jarvis
|
||
does.
|
||
|
||
The cause was on our side. Jarvis records what a tool returned as its own entry in the context,
|
||
placed immediately after the assistant turn it belongs to — the position is what says "these are the
|
||
results of the turn above". That entry was labelled as a system instruction, which OpenAI accepts
|
||
anywhere in a conversation and almost every other provider rejects outright.
|
||
|
||
It is now labelled in a way every provider accepts at any position, and it still says in its own
|
||
first sentence that the platform wrote it and the assistant did not — which is what stops a model
|
||
learning to write tool results instead of asking for them. Nothing changes about what the model is
|
||
told; only the envelope it arrives in.
|
||
|
||
A guard now also sits on the last line before any request leaves for your provider, so a
|
||
mislabelled entry is corrected and logged rather than costing you a conversation.
|
||
|
||
**Nothing to do.** Take the build; conversations that were failing will work on their next message.
|
||
|
||
## 2026-08-27 — A machine could show Offline while you were using it `api 0.95.0` `web 0.121.0`
|
||
|
||
**Only affects deployments running more than one copy of the api.** On Docker Compose this cannot
|
||
happen and there is nothing here for you beyond taking the build.
|
||
|
||
An agent could sit in the list marked **Offline** while its last heartbeat was seconds old and a
|
||
terminal was open on it. The machine was fine throughout — it was the row that was wrong, and it
|
||
stayed wrong until that machine happened to reconnect, which for one that never drops is never.
|
||
|
||
It took a restart to start. When an api replica stops it closes its agents' sockets and records
|
||
them as offline, which is true of the sockets it just closed. An agent notices immediately and
|
||
reconnects to another replica in milliseconds, and that replica records it as online — then the
|
||
stopping one's bookkeeping ran and overwrote it. Whichever machines happened to reconnect inside
|
||
that window were left marked offline for good.
|
||
|
||
Two things changed so that it cannot stick. A replica shutting down now checks whether each machine
|
||
has already been picked up elsewhere and leaves those alone. And a heartbeat arriving on a socket
|
||
repairs a row that claims the machine is gone — so any window left over closes itself within the
|
||
minute, without restarting anything.
|
||
|
||
**A machine that is revoked or waiting for approval is never changed by this.** Those are decisions
|
||
you made, and a heartbeat is not one.
|
||
|
||
**And the list no longer disagrees with the machine's own page.** The page has always reported
|
||
whether a socket is attached right now; the list printed the stored status instead, so the two could
|
||
say different things about the same machine. When they disagree the list now leads with the live
|
||
connection and marks the row as catching up. The opposite case is unchanged: a machine that claims
|
||
to be online with no socket attached still reads **Online · reconnecting**, because there the stored
|
||
value is the one over-promising and you want to see it.
|
||
|
||
## 2026-08-27 — Redis can survive losing a node `api 0.94.0`
|
||
|
||
**Nothing to do on Docker Compose.** Every setting below is optional and absent by default, and an
|
||
unchanged stack behaves exactly as it did. This release is for deployments that run more than one
|
||
copy of the api.
|
||
|
||
**A Redis that fails over is now something you can point at.** `REDIS_URL` names one server, and
|
||
there is nowhere in a URL to put what Redis Sentinel needs: the sentinels themselves, and the name
|
||
of the primary they are watching. So there are two new settings, and setting them switches the
|
||
shape — see `.env.example`:
|
||
|
||
```
|
||
REDIS_SENTINELS=a:26379,b:26379,c:26379
|
||
REDIS_SENTINEL_NAME=jarvis
|
||
```
|
||
|
||
Set one without the other and the api **refuses to start and says why**, rather than starting and
|
||
waiting forever for a primary nobody has heard of. `REDIS_PASSWORD` and `REDIS_SENTINEL_PASSWORD`
|
||
are there too, because the sentinels' password is frequently not the data node's. Redis **Cluster**
|
||
is deliberately not supported: the realtime adapter and the inter-pod bus both publish across what
|
||
would be different shards.
|
||
|
||
What a failover costs, measured rather than estimated: commands issued while the primary is gone
|
||
queue and land on the new one, so a failover is a pause and not an error storm. **Messages published
|
||
during it are lost** — Redis pub/sub has no buffer and no redelivery, and nothing on the client side
|
||
can change that. In practice that is a few seconds in which something happening on one api replica
|
||
may not reach another. Nothing is corrupted by it: what must survive is in Postgres for exactly this
|
||
reason.
|
||
|
||
**Losing Redis now appears in the log.** Nothing was listening for connection errors on those
|
||
sockets, which meant a Redis blip printed dozens of stack traces per container, none of which said
|
||
what had broken or that it had come back. It is now one line when a connection drops and one when it
|
||
returns, naming which connection and how long it took.
|
||
|
||
**For Kubernetes: the secrets can now be minted by the cluster.** Generating them exactly once used
|
||
to depend on every container sharing one filesystem, which no longer exists once there are several
|
||
pods — so it was a manual step that silently corrupted the vault if you got it wrong. There is now a
|
||
Job that mints them once, using the API server's refusal to create something twice as the
|
||
guarantee. It can create a Secret and cannot read one back. See `deploy/kubernetes/`.
|
||
|
||
**Also in `deploy/kubernetes/`, which has now been applied to a real cluster rather than only
|
||
validated:** manifests for a Sentinel-backed Redis, replicas that are spread across nodes instead of
|
||
being allowed to share one, and two faults that stopped every pod in the directory from starting
|
||
under the `restricted` Pod Security Standard. If you have been working from those files, take the
|
||
new ones.
|
||
|
||
## 2026-08-27 — The console container listens on 8080 and runs unprivileged `web 0.120.0`
|
||
|
||
**Check your port mapping before you pull.** The web container used to listen on **80** and now
|
||
listens on **8080**, because binding a port below 1024 needs root and the container no longer runs
|
||
as root. The compose file in this repository is already updated — `${JARVIS_PORT:-8080}:8080` — so
|
||
if you use it unchanged there is nothing to do. If you wrote your own mapping, or your reverse proxy
|
||
targets the container port directly rather than the published one, change `:80` to `:8080`.
|
||
|
||
Nothing else about how you reach it changes: same URL, same paths, same TLS terminator in front.
|
||
|
||
**The container's upstream is configurable.** `JARVIS_API_UPSTREAM` and `JARVIS_DNS_RESOLVER` now
|
||
say where the API is and who resolves that name; they default to what compose has always used, so an
|
||
unchanged stack behaves identically. They exist for anybody running these images somewhere the name
|
||
`api` and Docker's own DNS do not mean anything.
|
||
|
||
**A terminal no longer opens a second shell when your connection drops.** Reconnecting used to
|
||
silently start a fresh login on the machine — a new session, a new audit entry, a new recording —
|
||
underneath a window still showing the old scrollback. It now tells you the connection was lost and
|
||
offers to open a new session, which is a decision rather than something that happened to you.
|
||
|
||
**A part of the console that fails to load after an upgrade now says so.** The interface ships as
|
||
content-hashed files that are replaced on every upgrade, so a tab left open from before can reach
|
||
for something the server no longer has — which used to be a blank panel and a line in the browser
|
||
console. It now raises the same notice an update raises, with words, and offers the reload that
|
||
fixes it.
|
||
|
||
## 2026-08-27 — An upgrade refuses to drop a column instead of doing it `api 0.93.0`
|
||
|
||
**The one to read.** Until now, every boot ran the schema sync with `--accept-data-loss`, so an
|
||
upgrade that needed to remove a column simply removed it. That was survivable while one container
|
||
was the whole deployment and is the wrong default for anything else — and it meant a destructive
|
||
schema change happened with nobody having decided anything.
|
||
|
||
It now **refuses**, prints which columns or tables it would have dropped, and stops. Nothing is
|
||
altered and the api does not start. If those drops are what you want, set
|
||
`SCHEMA_ACCEPT_DATA_LOSS=true` in `.env` for that one upgrade and put it back afterwards. Take a
|
||
dump first either way: there is still no migration history, so pulling an older image does not put
|
||
the schema back.
|
||
|
||
The schema phase is also its own entrypoint now (`migrate.sh`) rather than part of the api's boot.
|
||
On compose nothing changes — the api still runs it, because one container is the whole deployment —
|
||
and it takes a database lock while it works, so `--scale api=2` waits rather than races.
|
||
|
||
**Two health endpoints, for anything watching this instance.** `/api/health/live` says only that the
|
||
process is running, and `/api/health/ready` answers 503 when Postgres **or Redis** is unreachable.
|
||
`/api/health` is unchanged, including its 200-while-degraded behaviour, so whatever you already
|
||
point at it keeps working.
|
||
|
||
**Two tabs no longer sign you out of everything.** Both tabs share one session cookie, so both
|
||
refreshing at the same moment looked exactly like a stolen cookie being replayed, and the protection
|
||
against that revoked every session you had. Concurrent refreshes are now told apart from a replay by
|
||
how long ago the token was rotated. A genuine replay still revokes the family, which is what it is
|
||
for.
|
||
|
||
**An enrolment token's use limit is now actually a limit.** Two machines running the same install
|
||
command in the same second could both enrol on a token that permitted one — an Ansible play across a
|
||
fleet does exactly that. The check and the increment are one operation now.
|
||
|
||
**A terminal transcript survives the process being killed.** Output used to live only in memory until
|
||
the session ended, so an out-of-memory kill or a host losing power took the whole recording of a root
|
||
shell with it. It is written down every few seconds now, and a session that ended that way hands back
|
||
what was captured, clearly marked as stopping early. A session with no transcript at all also says
|
||
which of the three reasons applies rather than guessing — recording was off, retention has passed, or
|
||
it was lost — because telling an auditor a recording was never made when it was made and lost is the
|
||
answer that stops them looking.
|
||
|
||
**A restart no longer writes an ending into a session that is still open.** Starting up used to close
|
||
every terminal session row it found, on the reasoning that a row with no ending belongs to a process
|
||
that died. With one container that was true. It is now scoped to the process's own sessions.
|
||
|
||
**The api container runs as a non-root user.** Nothing you do changes; it matters if you run these
|
||
images under a policy that refuses root containers.
|
||
|
||
**Optional: `VAULT_KEY_CREATED_AT`.** Only needed where the vault key file is rewritten by your
|
||
platform rather than sitting still in a volume — on compose it is inferred correctly from the file
|
||
and you can ignore this. See `.env.example` for what it decides.
|
||
|
||
**And the reason for most of the above: more than one api replica is now possible.** It is not yet
|
||
supported, and this compose file still runs one. Nobody has served real traffic on more than one, so
|
||
if you are considering it, read the note in the README first.
|
||
|
||
## 2026-08-25 — A run gets 200 tool rounds instead of 50 `api 0.92.0`
|
||
|
||
**If the assistant has been stopping mid-investigation with "the results above were not analysed",
|
||
this is why.** A run that reads a config, checks a service, greps a log and correlates two hosts
|
||
spends its budget on legitimate steps and then hits a ceiling meant for runaway loops. Asking it to
|
||
continue re-sends the whole conversation and pays for the same work twice, which is the expensive
|
||
outcome the ceiling was supposed to prevent.
|
||
|
||
The ceiling exists to stop a model that has stopped making progress, and at that job 200 is as good
|
||
as 50 — a loop that is genuinely stuck is stuck within a few rounds. Nothing else about how a run is
|
||
governed changes: you still watch it, the autonomy level still gates every write, and anything
|
||
consequential still asks for approval.
|
||
|
||
**One thing to expect on very long runs.** With more rounds available, the likelier ending is now
|
||
the model's context window rather than the round limit. That has always been reported plainly —
|
||
"this conversation has outgrown the model's context window" — and a run that reaches it should be
|
||
started again as a fresh conversation rather than continued.
|
||
|
||
## 2026-08-25 — The terminal button is on the row `web 0.119.0`
|
||
|
||
The terminal added in the entry below was in the row's overflow menu, behind Test, Connections and
|
||
Edit. Two of us went looking for it on the Assets screen and neither found it, which is a placement
|
||
problem rather than an attention one.
|
||
|
||
**It is now the first icon on the row**, and it names the route it will take. A machine reachable
|
||
two ways shows one icon for its preferred route and keeps the other as a named entry in the menu —
|
||
two identical terminal icons side by side would stand for two different networks and two different
|
||
logins, and an icon cannot say which is which.
|
||
|
||
The row still carries three icons rather than four, so **Edit moves into the menu** on any machine
|
||
that has a route. Test and Connections keep their place: they are what you reach for when a route
|
||
does not answer, which is the same moment you were reaching for the terminal.
|
||
|
||
## 2026-08-25 — A terminal goes over a route you choose `api 0.91.0` `web 0.118.0`
|
||
|
||
**Opening a shell used to mean opening it through the Jarvis agent, from the Agents page.** A Linux
|
||
host commonly answers on OpenSSH _and_ through an enrolled agent, and those are different networks,
|
||
different credentials and different accounts at the other end — so which one your shell goes over is
|
||
a decision. Until now it was made for you by whichever was available.
|
||
|
||
You can now open a terminal from the **Assets** list as well, and pick the route by name. A machine
|
||
with one way in opens straight away; one with several offers an entry per route. The choice is made
|
||
before the shell opens, and it is recorded — on the session, in the audit trail, and in
|
||
**Settings → Organization → Terminal sessions**, which now shows which route each session used.
|
||
|
||
**SSH sessions are held to exactly the same rules as agent ones.** The same idle timeout, the same
|
||
four-hour ceiling, the same output limit, the same recording under `VAULT_MASTER_KEY` and the same
|
||
retention policy. A different route is not a different set of rules.
|
||
|
||
**What decides who may do this has not changed.** The per-machine "may open a terminal" permission is
|
||
the same one, and it still has no bearing on what the assistant may do on those machines. What it
|
||
now permits is a shell by any route that machine has, and the access editor says so where you grant
|
||
it.
|
||
|
||
### Fixed
|
||
|
||
- The agents list offered a terminal on a machine whose agent had no route recorded, which could
|
||
never open. It now says why instead.
|
||
|
||
## 2026-08-25 — Settled approval requests fold away `web 0.117.0`
|
||
|
||
**A conversation with several approvals behind it was a wall of text.** Each settled request kept its
|
||
full plan on screen — a summary, every numbered step and the machines it touched — so a thread with
|
||
six of them was a page you scrolled past rather than read. What you actually want from that list is
|
||
which plan it was, whether it went ahead, and who said so.
|
||
|
||
Decided requests now start folded and show exactly that. Open one to read its plan again; it is all
|
||
still there, unchanged.
|
||
|
||
**A request still awaiting your decision does not fold.** Its plan is the thing you are being asked
|
||
to agree to, and a row you have to open before you can see what you are approving is a row that
|
||
makes not looking the easy path. Those stay open, as they were.
|
||
|
||
One small thing on top: if somebody else decides a request while you have it open, it stays open
|
||
rather than folding up mid-sentence.
|
||
|
||
### Fixed
|
||
|
||
- On a phone, an approval's title was squeezed into the space three badges left over — often one
|
||
word per line, with the status badge across it. The title now takes a row of its own on narrow
|
||
screens.
|
||
|
||
## 2026-08-24 — The diagram full-screen button explains itself `web 0.116.0`
|
||
|
||
**If full screen has been missing from your diagram viewer, the button was hidden rather than
|
||
broken.** It hides itself when the browser says it will not grant full screen, which is right on
|
||
iOS — Safari does full screen for video and nothing else there — and wrong on a desktop whose
|
||
browser is centrally managed, where a policy can forbid it. The result was the same either way: the
|
||
control simply was not there, with nothing to say the browser had taken it away rather than us.
|
||
|
||
It is now shown and disabled on any pointer device, with a tooltip naming the likely reason. The
|
||
expand control beside it still fills the dialog, which is the best any browser can offer once full
|
||
screen is off the table.
|
||
|
||
**And when the browser refuses a full-screen request outright, it says so.** That rejection used to
|
||
be swallowed, so a click did nothing at all and no message anywhere explained why. The browser's own
|
||
sentence is what you see now.
|
||
|
||
### Fixed
|
||
|
||
- A diagram viewer could get stuck believing it was full screen when it was not — the button then
|
||
offered to leave a full screen that did not exist, and clicking it did nothing. The comparison
|
||
behind it treated "no full-screen element" and "no diagram attached" as the same thing.
|
||
|
||
## 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.
|