Sync the self-hosting stack (85835b7)
This commit is contained in:
+26
-12
@@ -48,8 +48,10 @@ JWT_REFRESH_SECRET=
|
||||
# Must decode to exactly 32 bytes:
|
||||
# openssl rand -base64 32
|
||||
#
|
||||
# Every credential in the vault — SSH keys, API secrets, Microsoft 365 client secrets, the outbound
|
||||
# mail password — is encrypted under this key with AES-256-GCM. It is not stored anywhere but here.
|
||||
# Every credential in the vault — SSH keys, API secrets, Microsoft 365 client secrets — is encrypted
|
||||
# under this key with AES-256-GCM, and so is everything Jarvis seals OUTSIDE the vault: the Microsoft
|
||||
# Graph client secret it sends its own mail with, this instance's licence identity key, every
|
||||
# authenticator-app secret, and every terminal recording. It is not stored anywhere but here.
|
||||
#
|
||||
# Change it or lose it and none of that data can be read again, by you or by anyone. A database backup
|
||||
# does not save you: the backup holds the ciphertext. Back this value up separately from the database,
|
||||
@@ -91,16 +93,28 @@ OPENAI_API_KEY=
|
||||
# Pinning makes an upgrade a decision instead of a side effect of pulling. The app reports its real
|
||||
# version either way — a channel tag is a second name on the same image, not a build that forgot
|
||||
# its number.
|
||||
#JARVIS_IMAGE_API=git.luxit.be/luxit/jarvis-api:0.59.1
|
||||
#JARVIS_IMAGE_WEB=git.luxit.be/luxit/jarvis-web:0.74.2
|
||||
#
|
||||
# THERE IS NO EXAMPLE NUMBER HERE ON PURPOSE. Pin the version you are ALREADY RUNNING, which the app
|
||||
# footer shows as `web … · api …`. Nothing in the publishing path bumps a number written into this
|
||||
# file, so any number printed here is one that went stale while nobody was looking — and moving the
|
||||
# api pin BACKWARDS runs an old build against a schema that has already been migrated forward.
|
||||
#JARVIS_IMAGE_API=git.luxit.be/luxit/jarvis-api:<the api version in your footer>
|
||||
#JARVIS_IMAGE_WEB=git.luxit.be/luxit/jarvis-web:<the web version in your footer>
|
||||
|
||||
# Only read when the agent overlay is enabled, just below. Its version is the AGENT's, and moves
|
||||
# independently of the two above — a Jarvis release usually does not change the agent at all.
|
||||
#
|
||||
# PIN THIS ONE FIRST if you pin only one. The two above change what your own server runs; this one
|
||||
# changes what runs on every machine you administer, and it applies itself without asking. An
|
||||
# enrolled agent also refuses any version that is not strictly newer, so moving this back stops a
|
||||
# rollout rather than reversing it on machines that already took the update.
|
||||
#JARVIS_IMAGE_AGENT=git.luxit.be/luxit/jarvis-agent-dist:0.20.0
|
||||
# changes what runs on every machine you administer. Pinning it decides which build your instance
|
||||
# publishes; a separate UPDATE POLICY decides when a machine takes it, and its default is "let the
|
||||
# agent decide" — which in practice means the next time its service starts. Set that policy
|
||||
# instance-wide under Settings → Platform → General, per organization under Settings → Organization →
|
||||
# Agent updates. An enrolled agent also refuses any version that is not strictly newer, so moving
|
||||
# this back stops a rollout rather than reversing it on machines that already took the update.
|
||||
#
|
||||
# The app footer does not carry this number. Each enrolled machine reports the build it runs, on the
|
||||
# Agents page — that is the one to pin.
|
||||
#JARVIS_IMAGE_AGENT=git.luxit.be/luxit/jarvis-agent-dist:<version>
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# The Jarvis agent (optional)
|
||||
@@ -127,12 +141,12 @@ OPENAI_API_KEY=
|
||||
# Licence (required to create anything)
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
# The key your provider gave you. REQUIRED to create anything new.
|
||||
# Your licence key. REQUIRED to create anything new. Ask antoine@luxit.be for one.
|
||||
#
|
||||
# Without it, an instance keeps running everything already set up and refuses to create new
|
||||
# organizations, users and agents — so an existing deployment does not stop working when this
|
||||
# reaches it, and a fresh install gets as far as its first administrator and then needs a key. An
|
||||
# unlicensed instance also contacts nobody at all. See the README for exactly what a licensed one
|
||||
# organizations, users, agents and assets — so an existing deployment does not stop working when
|
||||
# this reaches it, and a fresh install gets as far as its first administrator and then needs a key.
|
||||
# An unlicensed instance also contacts nobody at all. See the README for exactly what a licensed one
|
||||
# reports.
|
||||
#
|
||||
# This value SEEDS the database on first boot and does not govern it afterwards: a renewal arrives
|
||||
|
||||
@@ -6,3 +6,8 @@
|
||||
# the connection, or as a vault that cannot decrypt what it wrote yesterday, with nothing anywhere
|
||||
# naming a line ending as the cause.
|
||||
* text=auto eol=lf
|
||||
|
||||
# And the screenshots are bytes, not text. `text=auto` above already detects that correctly, but
|
||||
# saying so costs one line and removes the question before somebody adds a JPEG.
|
||||
*.png binary
|
||||
*.jpg binary
|
||||
|
||||
@@ -1,14 +1,161 @@
|
||||
# Self-hosting Jarvis
|
||||
<p align="center">
|
||||
<img src="jarvis.svg" width="76" alt="Jarvis">
|
||||
</p>
|
||||
|
||||
Jarvis is an AI-assisted infrastructure administration platform for MSPs. This repository runs it from
|
||||
published container images — no source, no build, no account with the project.
|
||||
<h1 align="center">Jarvis</h1>
|
||||
|
||||
<p align="center"><strong>AI-assisted infrastructure administration for MSPs.</strong></p>
|
||||
|
||||
An operator opens a conversation and asks for the work in words. Jarvis does it: a command over SSH
|
||||
or through an enrolled agent, a Proxmox guest powered on, a Microsoft 365 account blocked, a MikroTik
|
||||
firewall read back — on the machines of one client organization, with credentials it decrypts from
|
||||
the vault and shows nobody. Every call is judged before it runs, and the ones that cannot be undone
|
||||
stop and ask a human in the thread.
|
||||
|
||||
The difference from an assistant that writes commands for you to paste is that these run.
|
||||
|
||||
This repository runs Jarvis from published container images — no source, no build, no account with
|
||||
the project.
|
||||
|
||||
- [What it does](#what-it-does)
|
||||
- [What decides whether a tool call runs](#what-decides-whether-a-tool-call-runs)
|
||||
- [What it looks like](#what-it-looks-like)
|
||||
- [How it works](#how-it-works)
|
||||
- [What you need](#what-you-need)
|
||||
- [Install](#install)
|
||||
- [Your reverse proxy has two requirements](#your-reverse-proxy-has-two-requirements)
|
||||
- [Upgrading](#upgrading)
|
||||
- [The agent](#the-agent)
|
||||
- [Things worth knowing before you trust it with production](#things-worth-knowing-before-you-trust-it-with-production)
|
||||
- [When it does not come up](#when-it-does-not-come-up)
|
||||
- [Backups, and restoring one](#backups-and-restoring-one)
|
||||
- [Removing it](#removing-it)
|
||||
- [Licence keys, and what your instance reports](#licence-keys-and-what-your-instance-reports)
|
||||
- [Getting a licence, and getting help](#getting-a-licence-and-getting-help)
|
||||
|
||||
## What it does
|
||||
|
||||
**Five connectors reach a managed system.** Each one is a set of tools the assistant may call, and
|
||||
what it may do with them is decided per call — see the next section.
|
||||
|
||||
| Connector | Reaches |
|
||||
| --- | --- |
|
||||
| **SSH** | Anything with a shell — Linux, Windows, and CLI-driven network gear. An appliance that serves no SFTP is offered the command tool alone, rather than four that would fail at the handshake. |
|
||||
| **Jarvis agent** | A machine running the enrolled agent, which dials out — so it works behind NAT, on a dynamic address, with no inbound rule and no SSH exposed. Commands, files, services, processes, system facts. |
|
||||
| **Proxmox VE** | The cluster API. Read and power only — no create, clone, snapshot, backup or migrate. |
|
||||
| **MikroTik RouterOS** | The native binary API, or the RouterOS 7 REST API. One identical tool surface either way; the connection decides the transport. |
|
||||
| **Microsoft 365** | Graph, app-only, on any Microsoft cloud, authenticating with a client secret or a certificate. Named tools for identity, licences, groups, admin roles, Exchange Online, Intune and the audit logs, plus one that reaches every remaining Graph endpoint. |
|
||||
|
||||
**An asset is the managed thing; a connection is a way of reaching it.** A Linux host commonly
|
||||
answers on OpenSSH *and* through an enrolled agent; a Proxmox node has a cluster API, a shell and an
|
||||
agent. Each route carries its own address, its own credentials and its own health — so a vendor API
|
||||
that stops answering no longer makes a device unmanageable while its console is up.
|
||||
|
||||
**Documents.** A conversation accumulates what the assistant writes: reports and runbooks in
|
||||
Markdown, diagrams in Mermaid, tabular data as a workbook. What it writes is always a *source*, and
|
||||
Jarvis renders the file on download — PDF and Word from prose, Excel and CSV from a workbook, HTML
|
||||
from either. Keeping the source is what leaves a document revisable instead of a dead binary.
|
||||
|
||||
**And what an MSP has to administer about itself**: organizations and members under a role ceiling,
|
||||
invitations, an encrypted vault with its own folder tree and its own grants, an audit trail,
|
||||
passkeys and TOTP, a notification centre, and a console in English, French or German — a property of
|
||||
the account, not of the browser.
|
||||
|
||||
## What decides whether a tool call runs
|
||||
|
||||
Three things, resolved on every single call. The most restrictive wins.
|
||||
|
||||
**Risk is a property of the operation.** A tool declares a baseline and may escalate per invocation:
|
||||
a shell command is mutating in general and destructive for `rm -rf`, `mkfs`, `shutdown`,
|
||||
`iptables -F` and about two dozen other patterns. Escalation is one-way — a per-call assessment can
|
||||
raise the risk, never lower it.
|
||||
|
||||
**Autonomy is a property of the conversation**, chosen by the operator:
|
||||
|
||||
| | Safe | Mutating | Destructive |
|
||||
| --- | --- | --- | --- |
|
||||
| **Read-only** | run | refuse | refuse |
|
||||
| **Ask before every change** | run | ask | ask |
|
||||
| **Ask before destructive changes** (default) | run | run | ask |
|
||||
| **Full access** | run | run | run |
|
||||
|
||||
"Ask" raises an approval request in the conversation, and the run parks until a human answers. The
|
||||
level is re-read on every call, so lowering it takes effect on the very next tool call of a run
|
||||
already in flight. **Full access removes the last in-chat gate for every participant** — destructive
|
||||
operations then run immediately, with no prompt and no second pair of eyes. Make it a deliberate
|
||||
choice.
|
||||
|
||||
**Permission is a property of the person.** A grant says which slice of the asset tree somebody may
|
||||
operate, through which connectors, up to which risk, and whether the tools whose operation the
|
||||
assistant *composes* — a shell command, an arbitrary Graph request, the contents of a file — are
|
||||
admitted at all. Grants resolve by walking outward from the asset: the most specific level that says
|
||||
anything decides entirely, and if nothing has spoken by the root the answer is no. Absent means
|
||||
nothing, so a forgotten grant fails closed rather than open.
|
||||
|
||||
> The conversation's autonomy is a **floor the operator imposes on themselves**; the grant is the
|
||||
> **ceiling imposed on them**. What runs is whichever binds.
|
||||
|
||||
A conversation also carries a **scope** — any mix of assets and folders, a folder granting its whole
|
||||
subtree. Scope is checked *before* the asset is resolved, so a machine out of scope never has its
|
||||
vault secrets decrypted.
|
||||
|
||||
## What it looks like
|
||||
|
||||
<p align="center">
|
||||
<img src="docs/img/console.png" alt="The Jarvis console, on a freshly installed instance" width="900">
|
||||
</p>
|
||||
|
||||
The console on a fresh install. The footer names the two builds you are running, which is the first
|
||||
thing to quote when something is wrong.
|
||||
|
||||
<p align="center">
|
||||
<img src="docs/img/asset.png" alt="One asset, its two routes, its agent and its inventory" width="900">
|
||||
</p>
|
||||
|
||||
One asset, and the distinction the model rests on: **two routes to the same machine**, each with its
|
||||
own address, its own credentials and its own health. One is preferred and untested; the other is
|
||||
disabled without being deleted, so its settings and its history survive and no tool may use it.
|
||||
|
||||
## How it works
|
||||
|
||||
Four containers. **Only `web` publishes a port**: its nginx serves the console and reverse-proxies
|
||||
`/api` and the websocket to `api` over the internal network, so your TLS terminator has exactly one
|
||||
target and the API is never reachable from outside the compose network.
|
||||
|
||||
| Service | What it is |
|
||||
| --- | --- |
|
||||
| `web` | nginx serving the React console. The only published port. |
|
||||
| `api` | REST, auth, the vault, the connectors, the tool-calling loop, and the websocket. |
|
||||
| `postgres` | Everything except the vault master key. |
|
||||
| `redis` | Socket fan-out, nonces and rate-limit counters. |
|
||||
|
||||
The assistant streams a turn, executes the tool calls the model asked for **strictly after the
|
||||
stream is fully drained**, then feeds the results back — up to fifty rounds per user turn. That
|
||||
ordering is what makes retrying a broken stream safe: at the moment a stream fails, no tool of that
|
||||
round has run, so replaying it re-generates intent and never re-runs an operation.
|
||||
|
||||
**A run that was in flight when the api stopped is picked up when it comes back.** A shutdown aborts
|
||||
each loop and lets it write out what it had streamed with a note saying why the transcript ends
|
||||
there; the next process finishes that message, closes any tool call whose outcome is unknown saying
|
||||
in as many words that it is unknown, and resumes the run — instructed to read the current state
|
||||
before repeating anything that writes. This is why the api asks for a stop grace period, and part of
|
||||
why it must run as a single replica.
|
||||
|
||||
**The agent dials out.** Nothing inbound is opened on a managed machine. It holds a websocket to
|
||||
your instance, signs each session with a key whose private half never leaves it, and reports its
|
||||
inventory on every heartbeat.
|
||||
|
||||
## What you need
|
||||
|
||||
- Docker with Compose v2, on anything Linux.
|
||||
- A hostname and a TLS terminator in front of it. Jarvis speaks plain HTTP and reads
|
||||
`X-Forwarded-Proto`; it does not manage certificates.
|
||||
- An API key for an OpenAI-compatible endpoint.
|
||||
- Docker with Compose v2, on **x86-64 Linux**. The api and web images are published for
|
||||
`linux/amd64` only, so an arm64 host — a Pi, an Ampere, a Graviton — fails at `docker compose pull`
|
||||
with no matching manifest. The machines you *administer* have no such limit: the agent ships arm64
|
||||
builds for Linux, macOS and Windows.
|
||||
- A hostname and a TLS terminator in front of it. Jarvis speaks plain HTTP and does not manage
|
||||
certificates.
|
||||
- An API key for an OpenAI-compatible endpoint. The api will not start without one.
|
||||
- A licence key. See [Licence keys](#licence-keys-and-what-your-instance-reports) — without one an
|
||||
instance runs but creates nothing new.
|
||||
- Roughly 2 GB of RAM for the stack and room for Postgres to grow.
|
||||
|
||||
## Install
|
||||
@@ -25,28 +172,53 @@ docker compose up -d
|
||||
First boot syncs the database schema and runs its data backfills before the API listens, so give it
|
||||
about a minute. Then point your reverse proxy at `JARVIS_PORT` and open the app.
|
||||
|
||||
**Give Jarvis its public address with the right scheme.** `WEB_ORIGIN` is the CORS decision and it
|
||||
accepts exactly one origin; `PUBLIC_URL` is what gets baked into agent install commands, invitation
|
||||
links and share links. A wrong `WEB_ORIGIN` presents as a sign-in page that cannot sign in. A wrong
|
||||
`PUBLIC_URL` does not fail at deploy time — it fails weeks later, on somebody else's server.
|
||||
|
||||
**The first account created becomes super-admin.** Self-registration is open by default, so sign up
|
||||
immediately after the stack is up and then close registration under Settings → Platform → General.
|
||||
Leaving it open means anyone who reaches the sign-in page gets an account.
|
||||
immediately after the stack is up and then close registration under **Settings → Platform →
|
||||
General**. Leaving it open means anyone who reaches the sign-in page gets an account.
|
||||
|
||||
## Your reverse proxy has two requirements
|
||||
|
||||
Both are the kind that produce confusing symptoms rather than clean errors.
|
||||
|
||||
- **Forward the WebSocket upgrade.** Without it the chat cannot stream and no agent can connect.
|
||||
- **Forward the WebSocket upgrade.** Two separate sockets ride `JARVIS_PORT`: the chat, on
|
||||
`/socket.io/`, and enrolled agents, on `/api/agents/ws`. Neither falls back to plain HTTP. Without
|
||||
the upgrade the chat does not lose streaming — it never connects at all, and since the prompt
|
||||
itself travels over that socket, nothing sends. Presence and in-chat approvals go with it, and no
|
||||
agent can connect.
|
||||
- **Give it a long read timeout** — 300s or so. A reasoning model can go 90+ seconds without emitting
|
||||
a byte, and a 60s default cuts the response mid-stream. The client sees a connection reset rather
|
||||
than a timeout, which reads like a bug in Jarvis.
|
||||
|
||||
Forward `X-Forwarded-For` too, and set `TRUST_PROXY_HOPS` to match how many proxies rewrite it —
|
||||
that is what puts real client addresses in the audit trail and the session list instead of your
|
||||
proxy's.
|
||||
|
||||
## Upgrading
|
||||
|
||||
```sh
|
||||
docker compose pull && docker compose up -d
|
||||
```
|
||||
|
||||
That is the whole upgrade: every service tracks the **`stable`** channel by default. Schema changes
|
||||
apply themselves when the api starts, and the api and the web are versioned independently — their
|
||||
numbers are not meant to match, because usually only one side changed.
|
||||
That is the whole upgrade: the api, the web, and — with the agent overlay on — the agent release all
|
||||
track the **`stable`** channel by default. Postgres and Redis are not on a Jarvis channel; they
|
||||
follow their own upstream tags, `postgres:16-alpine` and `redis:7-alpine`. Schema changes apply
|
||||
themselves when the api starts, and the api and the web are versioned independently — their numbers
|
||||
are not meant to match, because usually only one side changed.
|
||||
|
||||
**A schema change is one-way.** Jarvis has no migration history: each boot force-syncs the database
|
||||
to the schema its image carries, adding what a release added and dropping what it removed, without
|
||||
prompting. Pulling an older api image does not undo that — unlike the image itself, the schema stays
|
||||
where the newer build left it. Restoring a dump is the only way back, which is what makes the one
|
||||
below a prerequisite rather than a precaution.
|
||||
|
||||
```sh
|
||||
docker compose exec -T postgres pg_dump -U jarvis jarvis | gzip > jarvis-$(date +%F).sql.gz
|
||||
```
|
||||
|
||||
### Channels
|
||||
|
||||
@@ -65,36 +237,33 @@ were tested, not a fresh build of the same source.
|
||||
**Channels do not mean the app forgets which build it is.** The version is stamped into the image
|
||||
when it is built, so the footer in the app, `/version.json` and the agent manifest keep reporting the
|
||||
real number whichever name you pulled it under. That is what lets you tell somebody which build you
|
||||
are on when something goes wrong.
|
||||
are on when something goes wrong. Set `JARVIS_CHANNEL` in `.env` and the footer names your channel
|
||||
beside those numbers; leave it empty if you pin, because then you follow no channel.
|
||||
|
||||
Once you are in production, consider pinning: set `JARVIS_IMAGE_API`, `JARVIS_IMAGE_WEB` and
|
||||
`JARVIS_IMAGE_AGENT` in `.env` to explicit version tags. It makes an upgrade a decision rather than a
|
||||
side effect of pulling.
|
||||
side effect of pulling. Pin the version you are *already running* — the footer shows it — rather than
|
||||
one copied from a document, and remember that moving the api pin backwards runs an old build against
|
||||
a schema that has already moved forward.
|
||||
|
||||
**Pin the agent one first if you pin only one.** The api and the web change what your own server
|
||||
runs; the agent changes what runs on every machine you administer, and it applies itself without
|
||||
asking. An enrolled agent also refuses any version that is not strictly newer, so moving that pin
|
||||
back stops a rollout rather than reversing it on machines that already took the update.
|
||||
runs; the agent changes what runs on every machine you administer. Pinning the image decides which
|
||||
build your instance publishes; a separate **update policy** decides when a machine takes it —
|
||||
*As soon as available*, *On a schedule*, *Manually only*, *Let the agent decide* (the default, which
|
||||
means on its next service start, and on a server that can be months) or *Never*. The instance-wide
|
||||
answer is on **Settings → Platform → General**; an organization overrides it under **Settings →
|
||||
Organization → Agent updates**, and a single machine overrides that. An enrolled agent also refuses
|
||||
any version that is not strictly newer, so moving that pin back stops a rollout rather than reversing
|
||||
it on machines that already took the update.
|
||||
|
||||
Take a database dump before an upgrade that moves the api's minor version:
|
||||
|
||||
```sh
|
||||
docker compose exec -T postgres pg_dump -U jarvis jarvis | gzip > jarvis-$(date +%F).sql.gz
|
||||
```
|
||||
|
||||
## What you get, and what you do not
|
||||
|
||||
Working: the assistant with its tool-calling loop, plan-level approvals for destructive operations,
|
||||
the encrypted vault, the asset registry, multi-tenant RBAC, real-time conversations, documents and
|
||||
exports, the audit trail, and the connectors — SSH, Proxmox, Microsoft 365 and MikroTik.
|
||||
## The agent
|
||||
|
||||
**The Jarvis agent is an optional overlay, off by default.** Enrolling a machine downloads a compiled
|
||||
binary that the api serves from `AGENT_RELEASE_DIR`, and a compose-only deployment has no way to
|
||||
produce one. `docker-compose.agent.yml` supplies it as a pullable image instead. Leaving it off is a
|
||||
supported state rather than a broken one: everything else works, the installer answers 503 saying no
|
||||
build is published, and the connectors above reach machines without it.
|
||||
|
||||
## The agent
|
||||
supported state rather than a broken one: everything else works, the manifest and download endpoints
|
||||
answer 503 saying no build is published, and the SSH, Proxmox, Microsoft 365 and MikroTik connectors
|
||||
all reach machines without it.
|
||||
|
||||
Turn it on by adding one line to `.env`, so that every later `docker compose` command picks up both
|
||||
files with no extra flags:
|
||||
@@ -104,63 +273,149 @@ COMPOSE_FILE=docker-compose.yml:docker-compose.agent.yml
|
||||
```
|
||||
|
||||
then `docker compose pull && docker compose up -d`. A one-shot `agent-releases` service copies the
|
||||
release into a volume the api reads, and exits. From there, Settings → Agents hands you the install
|
||||
one-liner for each platform.
|
||||
release into a volume the api reads, and exits. From there, the **Agents** page in the sidebar issues
|
||||
the install command: click **New install command**, pick Linux, macOS or Windows, and copy the one
|
||||
line. (Settings → Organization → Agent updates is a different screen — it schedules how
|
||||
already-enrolled agents take new builds.)
|
||||
|
||||
Three things worth knowing about it:
|
||||
Four things worth knowing about it:
|
||||
|
||||
- **Until that publisher exits cleanly, the api does not start.** That is deliberate — a release that
|
||||
failed to arrive should stop the deploy loudly rather than leave you handing 404s to every installer
|
||||
you run this week. The cost is that an unreachable registry blocks the whole stack. The comment in
|
||||
the file names the three lines to drop if you would rather it degraded quietly.
|
||||
- **Upgrading it does not restart anything.** The api computes each download's checksum from the bytes
|
||||
on disk on every request, so a new release in the volume is served immediately.
|
||||
- **Upgrading it does not restart anything.** The api re-checks the file on disk on every download
|
||||
request and re-hashes it whenever its size or timestamp has changed, so a new release in the volume
|
||||
is served immediately and the published checksum always describes the bytes actually being served.
|
||||
- **The agent version is its own number.** It moves independently of the api and the web, and a Jarvis
|
||||
release usually does not touch it at all. Pin it with `JARVIS_IMAGE_AGENT` when you pin the others.
|
||||
release usually does not touch it at all. The app footer does not carry it either — each enrolled
|
||||
machine reports the build it runs, on the Agents page. Pin it with `JARVIS_IMAGE_AGENT`.
|
||||
- **It carries an interactive shell.** Beyond what the assistant can do with it, a person gets a real
|
||||
terminal on an enrolled machine from the browser — Linux, macOS and Windows alike. Sessions are
|
||||
recorded by default, encrypted under `VAULT_MASTER_KEY` and deleted on a retention policy you set
|
||||
under **Settings → Organization → Terminal sessions**. Turning recording off stops the transcript,
|
||||
never the audit entry.
|
||||
|
||||
The agent runs as root on Linux and macOS and as LocalSystem on Windows, deliberately — its purpose is
|
||||
to administer the machine. What it may actually *do* is decided by Jarvis' autonomy policy and its
|
||||
approval gates, not by the account it runs under. Read that section of the main documentation before
|
||||
enrolling anything you care about.
|
||||
**What an enrolled agent can do.** Inventory the machine, run commands, read, write and fetch files,
|
||||
list and control services, list processes, update itself, and carry that shell. It runs as root on
|
||||
Linux and macOS and as LocalSystem on Windows, deliberately — its purpose is to administer the
|
||||
machine.
|
||||
|
||||
What the **assistant** does with that reach is bounded by the autonomy policy and the approval gates
|
||||
above. **The interactive shell is not.** There is no command to inspect before a shell opens, so the
|
||||
risk ceiling has nothing to weigh; it is gated instead by a permission and a per-machine switch on
|
||||
the grant. Decide who holds those before enrolling anything you care about.
|
||||
|
||||
## Things worth knowing before you trust it with production
|
||||
|
||||
These are deliberate and documented rather than surprises waiting to be found.
|
||||
|
||||
- **`VAULT_MASTER_KEY` has no recovery.** Read the note in `.env.example`. A database backup does not
|
||||
protect the vault — the backup holds ciphertext encrypted under that key.
|
||||
protect what it seals — the backup holds ciphertext encrypted under that key, and that covers more
|
||||
than the vault: the outbound-mail client secret, this instance's licence identity key, every TOTP
|
||||
secret and every terminal recording go with it.
|
||||
- **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.
|
||||
- **Access tokens cannot be revoked.** Revoking a session or changing a password invalidates refresh
|
||||
tokens; a stolen access token stays valid for up to 15 minutes.
|
||||
- **The api must run as a single replica.** In-flight runs, pending approvals and presence live in
|
||||
per-process memory. Scaling it out silently drops cancels and approvals answered on the wrong one.
|
||||
- **Both containers run as root**, and `/api/health` answers 200 with `status: "degraded"` when the
|
||||
database is unreachable, so the container healthcheck alone is not a liveness signal for the DB.
|
||||
- The assistant executes real operations on real infrastructure. Destructive ones require in-chat
|
||||
human approval; mutating ones do not. Decide your autonomy level per conversation accordingly.
|
||||
- **The api must run as a single replica.** In-flight runs, pending approvals, presence and the
|
||||
websocket of every enrolled agent live in one process's memory. The Redis in this stack does not
|
||||
lift that limit: it fans outgoing events out to other replicas, but an incoming one is only ever
|
||||
handled by the replica holding that connection. So a cancel or an approval answered on the wrong
|
||||
replica is silently dropped, and an agent tool call can land on a replica that does not hold the
|
||||
target machine. Worse, a starting replica's recovery sweep claims every run it does not own — so a
|
||||
second instance re-executes, against your real infrastructure, operations the first is still
|
||||
running.
|
||||
- **There is no rate limiting on sign-in.** No throttler, no account lockout, and authentication
|
||||
events are not audited. Credential stuffing is bounded only by the reverse proxy you put in front,
|
||||
which this repository does not ship. If your proxy can rate-limit one route, make it that one.
|
||||
- **An access token dies with its session, with one exception.** Every access token carries the id of
|
||||
the session that issued it and every request re-checks that the session is live, so revoking a
|
||||
session, signing other devices out or deactivating an account cuts that token off on its next
|
||||
request. The exception is changing your own password: it revokes every *other* session and keeps
|
||||
the one you are changing it from, so a token stolen from that session stays valid until it expires
|
||||
— 15 minutes by default (`JWT_ACCESS_TTL`). An admin-forced reset drops every session.
|
||||
- **The api container runs as root**, and so does nginx's master process in the web container,
|
||||
though its workers drop privileges. Neither image declares a `USER`.
|
||||
- **`/api/health` answers 200 with `status: "degraded"`** when the database is unreachable, so the
|
||||
container healthcheck alone is not a liveness signal for the database.
|
||||
- **Outbound mail is Microsoft Graph only.** There is no SMTP and no environment variable for any of
|
||||
it: an app registration with `Mail.Send` and a shared mailbox, set up on the **Mail** tab of
|
||||
**Settings → Platform → General**. Without it, invitations still work — the link comes back to the
|
||||
admin who created it instead of being emailed. Nothing else is emailed: there is no password reset
|
||||
and no address verification.
|
||||
- The assistant executes real operations on real infrastructure. What stops for a human is the
|
||||
conversation's autonomy level, and at *Full access* nothing does. See
|
||||
[What decides whether a tool call runs](#what-decides-whether-a-tool-call-runs).
|
||||
|
||||
## Backups
|
||||
## When it does not come up
|
||||
|
||||
Postgres holds everything except the vault key. Two volumes matter:
|
||||
```sh
|
||||
docker compose ps # who is running, and who is restarting
|
||||
docker compose logs -f api # the api says why it refused to start
|
||||
```
|
||||
|
||||
The api validates its configuration at boot and **fails fast rather than starting half-configured**,
|
||||
so a first-boot failure is almost always one line of `.env`:
|
||||
|
||||
| Symptom | Cause |
|
||||
| --- | --- |
|
||||
| `docker compose up` exits complaining about a variable | That variable is empty in `.env`. Compose refuses before any container starts. |
|
||||
| api restarts in a loop, logs `Invalid environment configuration` | A value is present but wrong. `VAULT_MASTER_KEY` must base64-decode to exactly 32 bytes. |
|
||||
| The sign-in page loads but cannot sign in | `WEB_ORIGIN` is not the address the browser used, scheme included. It accepts one origin, not a list. |
|
||||
| Chat never answers and nothing streams | The WebSocket upgrade is not being forwarded. |
|
||||
| An answer dies part-way through, every time | The proxy's read timeout is too short. |
|
||||
| Everything works but nothing can be created | No licence key. See below. |
|
||||
| An agent installer answers 503 | No agent build is published — the overlay is off. That is a supported state. |
|
||||
|
||||
The api takes about a minute on first boot, syncing the schema before it listens. `docker compose ps`
|
||||
showing `health: starting` for that long is expected, not a fault.
|
||||
|
||||
## Backups, and restoring one
|
||||
|
||||
Postgres holds everything except the vault key — avatars, documents, exports and terminal recordings
|
||||
are all rows, not files on disk. **Of the stack's volumes, only `postgres_data` is worth backing
|
||||
up.** `redis_data` carries nonces, rate-limit counters and the socket fan-out, all of which rebuild
|
||||
themselves; `agent_releases` is refilled by the next `docker compose pull`.
|
||||
|
||||
```sh
|
||||
docker compose exec -T postgres pg_dump -U jarvis jarvis | gzip > jarvis.sql.gz
|
||||
```
|
||||
|
||||
Plus `VAULT_MASTER_KEY`, stored somewhere that is not this host. A dump without the key is a database
|
||||
whose credentials cannot be read.
|
||||
whose credentials cannot be read — and an instance restored under a *different* key keeps **looking**
|
||||
configured, because nothing on a settings page decrypts anything. It fails on every reveal instead.
|
||||
|
||||
To restore, stop the api so nothing writes while you work, then load the dump into an empty database:
|
||||
|
||||
```sh
|
||||
docker compose stop api web
|
||||
docker compose exec -T postgres psql -U jarvis -d postgres \
|
||||
-c 'DROP DATABASE IF EXISTS jarvis;' -c 'CREATE DATABASE jarvis;'
|
||||
gunzip -c jarvis.sql.gz | docker compose exec -T postgres psql -U jarvis -d jarvis
|
||||
docker compose start api web
|
||||
```
|
||||
|
||||
Restore under the **same `VAULT_MASTER_KEY`** the dump was taken with. That value is not in the dump,
|
||||
and no part of the restore will warn you that it differs.
|
||||
|
||||
## Removing it
|
||||
|
||||
```sh
|
||||
docker compose down # stops everything, keeps the data
|
||||
docker compose down -v # also deletes the volumes — every conversation, asset and credential
|
||||
```
|
||||
|
||||
`down -v` is not recoverable from anything but a dump you already took.
|
||||
|
||||
## Licence keys, and what your instance reports
|
||||
|
||||
**Jarvis needs a licence key.** Ask your provider for one and put it in `.env` as
|
||||
`JARVIS_LICENSE_KEY`. The key is a signed token your instance verifies **offline** — it carries your
|
||||
term and your limits, and it needs no network to be checked.
|
||||
**Jarvis needs a licence key.** Ask for one (see [below](#getting-a-licence-and-getting-help)) and
|
||||
put it in `.env` as `JARVIS_LICENSE_KEY`. The key is a signed token your instance verifies
|
||||
**offline** — it carries your term and your limits, and it needs no network to be checked.
|
||||
|
||||
Without one, an instance keeps running everything already set up — every organization, every user,
|
||||
every agent, and the assistant itself — and refuses to create anything NEW. So an existing
|
||||
deployment does not stop working when this reaches it, and a fresh install gets as far as its first
|
||||
administrator account and then needs a key.
|
||||
every agent, every asset, and the assistant itself — and refuses only to create NEW ones: no new
|
||||
organization, user, agent or asset. So an existing deployment does not stop working when this reaches
|
||||
it, and a fresh install gets as far as its first administrator account and then needs a key.
|
||||
|
||||
**An unlicensed instance contacts nobody.** No check-in, no telemetry, nothing leaves your network at
|
||||
all. The reporting below starts only once a key is in place.
|
||||
@@ -169,16 +424,17 @@ A licensed instance then reports to the address written into that key, at an int
|
||||
sets — every ten minutes on the current arrangement, so that a renewal or a revocation reaches you
|
||||
promptly rather than tomorrow. This is everything it sends, in full:
|
||||
|
||||
| Field | What it is |
|
||||
| ----------------------- | ------------------------------------------------- |
|
||||
| Field | What it is |
|
||||
| ------------------------ | ------------------------------------------------- |
|
||||
| Product | The literal string `jarvis` |
|
||||
| Licence id | Which licence this is |
|
||||
| Instance id + public key | A key pair your instance generated, identifying it |
|
||||
| Version | Which Jarvis build you are running |
|
||||
| Contract version | Which set of limits this build understands — a checksum, not a document |
|
||||
| Counts | How many organizations, users, agents and assets |
|
||||
| Public URL | Your instance's address — always sent, see below |
|
||||
| Public URL | Your instance's address — always sent, see below |
|
||||
| Timestamps | When the process started, and when it reported |
|
||||
| Signature + nonce | Proof the message came from this instance, and a one-time value so an old one cannot be replayed. Carries nothing about you. |
|
||||
|
||||
**Counts, not contents.** No names, no email addresses, no conversation text, no asset inventory, no
|
||||
credentials, nothing about what you administer.
|
||||
@@ -186,21 +442,37 @@ credentials, nothing about what you administer.
|
||||
**The public URL is the one field that names your network rather than measuring something, and on a
|
||||
licensed instance it is sent.** It used to be a switch on the licence screen; it is not any more,
|
||||
because an installation the publisher can identify only by a fingerprint is one where "which of these
|
||||
is the customer calling about" has no answer. The screen now shows you the exact address that leaves,
|
||||
under Settings → Licence. If that is not acceptable for your deployment, the answer is to run
|
||||
unlicensed — which contacts nobody at all — or to take it up with your provider before installing.
|
||||
is the customer calling about" has no answer. The screen shows you the exact address that leaves,
|
||||
under **Settings → Platform → Licence**. If that is not acceptable for your deployment, the answer is
|
||||
to run unlicensed — which contacts nobody at all — or to take it up with us before installing.
|
||||
|
||||
The reply can carry a renewed key, which your instance adopts on its own — so a renewal reaches you
|
||||
without anybody re-pasting anything.
|
||||
|
||||
**Your platform does not stop working because of a licence.** Expiry gives you a grace period, then
|
||||
refuses only the creation of new organizations, users and agents — everything already set up keeps
|
||||
running, and so does the assistant. There is no state in which Jarvis disables, deletes or locks you
|
||||
out of something you are already using. If the check-in cannot reach the server, nothing changes at
|
||||
all: the key you hold is what governs, and it is checked without a network.
|
||||
**Your platform does not stop working because of a licence.** Expiry gives you a grace period — as
|
||||
long as your key says, which on the current plans is 30 days, and none at all on a trial. After it,
|
||||
Jarvis refuses only the creation of new organizations, users, agents and assets; everything already
|
||||
set up keeps running, and so does the assistant. There is no state in which Jarvis disables, deletes
|
||||
or locks you out of something you are already using. If the check-in cannot reach the server, nothing
|
||||
changes at all: the key you hold is what governs, and it is checked without a network.
|
||||
|
||||
## Getting a licence, and getting help
|
||||
|
||||
**Antoine Cavelier — <antoine@luxit.be>.** Licence keys, pricing, and anything wrong with the
|
||||
product.
|
||||
|
||||
When something is broken, the two facts worth putting in the first message are the build you are on
|
||||
and what the api said:
|
||||
|
||||
```sh
|
||||
curl -s https://your-jarvis.example.com/version.json # the web and api versions
|
||||
docker compose logs --tail=100 api
|
||||
```
|
||||
|
||||
`/version.json` is public on purpose, so you can quote it without signing in.
|
||||
|
||||
## Licence
|
||||
|
||||
The images are provided as-is with no warranty, no support and no commitment to future availability.
|
||||
The source is not public and no rights to it are granted. Ask the maintainer before deploying this
|
||||
commercially or for third parties.
|
||||
The source is not public and no rights to it are granted. Ask before deploying this commercially or
|
||||
for third parties.
|
||||
|
||||
+14
-6
@@ -10,15 +10,23 @@
|
||||
# websocket to the internal `api` service, so your own TLS terminator has exactly one target and the
|
||||
# API is never reachable from outside this compose network.
|
||||
#
|
||||
# UPGRADING: `docker compose pull && docker compose up -d`. Both services track `latest` by default,
|
||||
# so that is the whole upgrade. Schema changes apply themselves when the api starts.
|
||||
# UPGRADING: `docker compose pull && docker compose up -d`. Every Jarvis image here tracks `stable` by
|
||||
# default, so that is the whole upgrade. Postgres and Redis are not on a Jarvis channel — they follow
|
||||
# their own upstream tags. Schema changes apply themselves when the api starts, and they are ONE-WAY:
|
||||
# there is no migration history, so pulling an older api image does not put the schema back. Take a
|
||||
# dump first. See the README.
|
||||
#
|
||||
# `latest` does NOT mean the app forgets which build it is: the tag is a second name on the same image
|
||||
# as the version tag, and the version is stamped INTO the image at build time. The footer in the app and
|
||||
# /version.json keep reporting 0.51.0 or whatever you actually pulled.
|
||||
# A channel tag does NOT mean the app forgets which build it is: `stable` is a second name on the same
|
||||
# image as its version tag, and the version is stamped INTO the image when it is built. The footer in
|
||||
# the app and /version.json keep reporting the real number whichever name you pulled it under — which
|
||||
# is what lets you tell somebody which build you are on when something goes wrong.
|
||||
#
|
||||
# To pin instead — recommended once you are in production, because it makes an upgrade a decision rather
|
||||
# than a side effect of pulling — set JARVIS_IMAGE_API and JARVIS_IMAGE_WEB in .env to explicit tags.
|
||||
# than a side effect of pulling — set JARVIS_IMAGE_API and JARVIS_IMAGE_WEB (and JARVIS_IMAGE_AGENT, if
|
||||
# you run the agent overlay) in .env to explicit version tags.
|
||||
#
|
||||
# No version number is written in this comment on purpose. Nothing in the publishing path would ever
|
||||
# bump one, so a number here is a number that goes stale while nobody is looking.
|
||||
name: jarvis
|
||||
|
||||
services:
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 114 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 252 KiB |
+40
@@ -0,0 +1,40 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 96 96" width="96" height="96" role="img" aria-label="Jarvis">
|
||||
<!--
|
||||
The Jarvis identity mark. Two hexagonal shells OUT OF PHASE — a flat-top holding a pointy-top —
|
||||
around a core. That phase offset is the signature: a version that drops the inner ring is a
|
||||
hexagon, not this logo.
|
||||
|
||||
Colours are literals rather than CSS variables, because a README is rendered by a forge that
|
||||
does not load the app's stylesheet. The stroke reads on a white and on a dark ground alike,
|
||||
which is what a mark in a README has to survive.
|
||||
-->
|
||||
<defs>
|
||||
<linearGradient id="shell" gradientUnits="userSpaceOnUse" x1="48" y1="6" x2="48" y2="90">
|
||||
<stop offset="0" stop-color="#6e64d8" />
|
||||
<stop offset="1" stop-color="#6e64d8" stop-opacity=".76" />
|
||||
</linearGradient>
|
||||
<radialGradient id="halo" gradientUnits="userSpaceOnUse" cx="48" cy="48" r="30">
|
||||
<stop offset="0" stop-color="#a49cfc" />
|
||||
<stop offset=".55" stop-color="#a49cfc" stop-opacity=".32" />
|
||||
<stop offset="1" stop-color="#a49cfc" stop-opacity="0" />
|
||||
</radialGradient>
|
||||
</defs>
|
||||
|
||||
<circle cx="48" cy="48" r="30" fill="url(#halo)" opacity=".26" />
|
||||
<path
|
||||
d="M 48 23 L 26.35 35.5 L 26.35 60.5 L 48 73 L 69.65 60.5 L 69.65 35.5 Z"
|
||||
fill="none"
|
||||
stroke="#6e64d8"
|
||||
stroke-width="4"
|
||||
stroke-linejoin="round"
|
||||
opacity=".4"
|
||||
/>
|
||||
<path
|
||||
d="M 90 48 L 69 11.63 L 27 11.63 L 6 48 L 27 84.37 L 69 84.37 Z"
|
||||
fill="none"
|
||||
stroke="url(#shell)"
|
||||
stroke-width="6"
|
||||
stroke-linejoin="round"
|
||||
/>
|
||||
<circle cx="48" cy="48" r="8" fill="#a49cfc" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.6 KiB |
Reference in New Issue
Block a user