Sync the self-hosting stack
This commit is contained in:
@@ -9,6 +9,87 @@ one of them changed. An entry says which artefact and which version, so `web 0.1
|
|||||||
Every version published to a channel appears here. Nothing is written from memory afterwards: a build
|
Every version published to a channel appears here. Nothing is written from memory afterwards: a build
|
||||||
cannot be published without its entry.
|
cannot be published without its entry.
|
||||||
|
|
||||||
|
## 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`
|
## 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",
|
**If the assistant has been stopping mid-investigation with "the results above were not analysed",
|
||||||
|
|||||||
@@ -156,8 +156,7 @@ round has run, so replaying it re-generates intent and never re-runs an operatio
|
|||||||
each loop and lets it write out what it had streamed with a note saying why the transcript ends
|
each loop and lets it write out what it had streamed with a note saying why the transcript ends
|
||||||
there; the next process finishes that message, closes any tool call whose outcome is unknown saying
|
there; the next process finishes that message, closes any tool call whose outcome is unknown saying
|
||||||
in as many words that it is unknown, and resumes the run — instructed to read the current state
|
in as many words that it is unknown, and resumes the run — instructed to read the current state
|
||||||
before repeating anything that writes. This is why the api asks for a stop grace period, and part of
|
before repeating anything that writes. This is why the api asks for a stop grace period.
|
||||||
why it must run as a single replica.
|
|
||||||
|
|
||||||
**The agent dials out.** Nothing inbound is opened on a managed machine. It holds a websocket to
|
**The agent dials out.** Nothing inbound is opened on a managed machine. It holds a websocket to
|
||||||
your instance, signs each session with a key whose private half never leaves it, and reports its
|
your instance, signs each session with a key whose private half never leaves it, and reports its
|
||||||
@@ -398,14 +397,28 @@ These are deliberate and documented rather than surprises waiting to be found.
|
|||||||
password changed to match, so plan that one.
|
password changed to match, so plan that one.
|
||||||
- **SSH host keys are not verified.** Every SSH connection trusts whatever key answers. This is the
|
- **SSH host keys are not verified.** Every SSH connection trusts whatever key answers. This is the
|
||||||
one gap in the execution path with no compensating control.
|
one gap in the execution path with no compensating control.
|
||||||
- **The api must run as a single replica.** In-flight runs, pending approvals, presence and the
|
- **This compose file runs one api, and that is the shape it is written for.** It gives you one of
|
||||||
websocket of every enrolled agent live in one process's memory. The Redis in this stack does not
|
everything and generates the secrets onto a shared volume, which is right here and is exactly what
|
||||||
lift that limit: it fans outgoing events out to other replicas, but an incoming one is only ever
|
must not be done per replica anywhere else — each copy would mint its own vault key, and a
|
||||||
handled by the replica holding that connection. So a cancel or an approval answered on the wrong
|
credential one process sealed would read as corrupt to another with nothing logged at boot.
|
||||||
replica is silently dropped, and an agent tool call can land on a replica that does not hold the
|
|
||||||
target machine. Worse, a starting replica's recovery sweep claims every run it does not own — so a
|
The images themselves no longer require a single replica. The web container's upstream and DNS
|
||||||
second instance re-executes, against your real infrastructure, operations the first is still
|
resolver are configurable, there are separate liveness and readiness endpoints, both containers run
|
||||||
running.
|
unprivileged, and the schema is a separate entrypoint meant to run once rather than something every
|
||||||
|
container does at boot. Everything that used to do real damage across replicas is fixed: a starting
|
||||||
|
replica's recovery sweep once claimed every run it did not own, so a second instance re-executed —
|
||||||
|
against your real infrastructure — operations the first was still running. A run now holds its
|
||||||
|
conversation through a unique index in the database, a sweep claims only a run whose heartbeat has
|
||||||
|
gone cold, and a run that cannot write its heartbeat stops itself before anyone else may take over.
|
||||||
|
Agent tool calls, revocations, Stop, approvals and interactive terminals reach whichever process
|
||||||
|
holds the connection instead of failing where they land. A replica shutting down no longer marks
|
||||||
|
your whole fleet offline, and starting one no longer writes an ending into the record of a root
|
||||||
|
shell that is still open.
|
||||||
|
|
||||||
|
**Nobody has run an estate on more than one replica yet**, so if you are the first, treat it as
|
||||||
|
something to watch rather than something to assume. Two API processes have been run together
|
||||||
|
against one database and one Redis, and the manifests are published, but that is not the same
|
||||||
|
thing as your machines depending on it.
|
||||||
- **There is no rate limiting on sign-in.** No throttler, no account lockout, and authentication
|
- **There is no rate limiting on sign-in.** No throttler, no account lockout, and authentication
|
||||||
events are not audited. Credential stuffing is bounded only by the reverse proxy you put in front,
|
events are not audited. Credential stuffing is bounded only by the reverse proxy you put in front,
|
||||||
which this repository does not ship. If your proxy can rate-limit one route, make it that one.
|
which this repository does not ship. If your proxy can rate-limit one route, make it that one.
|
||||||
|
|||||||
+10
-1
@@ -206,6 +206,15 @@ services:
|
|||||||
AGENT_HEARTBEAT_INTERVAL_SEC: ${AGENT_HEARTBEAT_INTERVAL_SEC:-30}
|
AGENT_HEARTBEAT_INTERVAL_SEC: ${AGENT_HEARTBEAT_INTERVAL_SEC:-30}
|
||||||
RUN_SHUTDOWN_GRACE_SEC: ${RUN_SHUTDOWN_GRACE_SEC:-25}
|
RUN_SHUTDOWN_GRACE_SEC: ${RUN_SHUTDOWN_GRACE_SEC:-25}
|
||||||
|
|
||||||
|
# This container brings the database up to its own schema before serving. Left on, because on
|
||||||
|
# compose one container IS the deployment. Turn it off only where something else runs
|
||||||
|
# `migrate.sh` first — an orchestrator with a run-once Job, say.
|
||||||
|
JARVIS_SKIP_MIGRATIONS: ${JARVIS_SKIP_MIGRATIONS:-false}
|
||||||
|
# Let that phase DROP columns and tables. OFF, and it should stay off between deliberate acts:
|
||||||
|
# without it the schema sync REFUSES and prints what it would have destroyed, which is the
|
||||||
|
# answer you want from an upgrade that was not expecting to lose anything.
|
||||||
|
SCHEMA_ACCEPT_DATA_LOSS: ${SCHEMA_ACCEPT_DATA_LOSS:-false}
|
||||||
|
|
||||||
# Where the agent binaries live, if you have them. Leaving this unset is a supported state:
|
# Where the agent binaries live, if you have them. Leaving this unset is a supported state:
|
||||||
# everything except the agent installer works, and the installer answers 503 saying no build is
|
# everything except the agent installer works, and the installer answers 503 saying no build is
|
||||||
# published. See the README — a self-hosted instance has no way to produce these.
|
# published. See the README — a self-hosted instance has no way to produce these.
|
||||||
@@ -230,7 +239,7 @@ services:
|
|||||||
ports:
|
ports:
|
||||||
# Put your own TLS terminator in front of this. Jarvis speaks plain HTTP here on purpose and
|
# Put your own TLS terminator in front of this. Jarvis speaks plain HTTP here on purpose and
|
||||||
# reads X-Forwarded-Proto to know what the browser actually used.
|
# reads X-Forwarded-Proto to know what the browser actually used.
|
||||||
- "${JARVIS_PORT:-8080}:80"
|
- "${JARVIS_PORT:-8080}:8080"
|
||||||
|
|
||||||
volumes:
|
volumes:
|
||||||
postgres_data:
|
postgres_data:
|
||||||
|
|||||||
Reference in New Issue
Block a user