Everyday usage: Jarvis — User Guide. This page is the technical reference.
A private, locally hosted AI assistant. You chat with it from your phone through one ntfy topic; a local language model (llama.cpp, Gemma 4 26B-A4B, CPU-only) answers; a small bridge service enforces a permission policy, runs a set of safe tools, schedules reminders and a daily briefing, and keeps a git-versioned markdown notebook. No hosted-model API, no cloud AI, no API keys.
Abilities (bridge v0.12; v0.9 added a phone web page, a voice endpoint and OpenProject projects and tasks; v0.10 Tailscale login and voice through Home Assistant; v0.11 the Jarvis Console, read-only commands that run without asking and a midnight warm-up): clock, own health, weather at home, homelab monitor health (Uptime Kuma), security cameras (Frigate status and events), exact calculator, reminders, daily briefing, notebook, and read-only views of Proxmox (nodes, containers, storage, Ceph, backups, live IPs, host temperatures), Home Assistant (presence, battery, plugs, shopping list) and the wiki (keyword search). Directly it can change only its notebook, its reminders and the Home Assistant shopping list. On Proxmox it can ask for approval to start, shut down, force-stop or restart a container/VM, to add or remove a guest in the weekly backup job, and to run one command inside a container; nothing happens until Cody replies yes (section Approval-gated actions). Opt-in proactive alerts (/watch). The project plan and progress log are on Jarvis: goal, plan and progress log.
Nothing depends on it. Stopping the LXC (pct stop 130) is the master kill switch and affects no other service. n8n's monitoring agent uses the separate Ollama in LXC 126, not this.
| Field | Value |
|---|---|
| Hostname | jarvis |
| VMID / Type | LXC 130, unprivileged, nesting=1,keyctl=1 |
| Proxmox Node | SuperPC |
| IP Address | 192.168.86.226 |
| Tailscale IP | 100.111.230.69 (jarvis.tailbacb1d.ts.net; disable key expiry in the admin console) |
| OS | Debian 13 |
| CPU / RAM | 12 cores (cpuunits 50, so Frigate and Ceph win under contention) / 28 GB, swap 0 |
| Storage | rootfs 30 GB on boot-disks (Ceph RBD) · mp0 /srv/models ← CephFS /mnt/pve/cephfs/jarvis (models, backup=0) · mp1 /srv/notes ← CephFS /mnt/pve/cephfs/jarvis-notes (notebook, backup=0) |
| Start on boot | No (onboot: 0) — start with pct start 130 |
Sizing rationale: the brain needs ~15 GB of real RAM; 28 GB leaves headroom for page cache. Do not raise it without a measured reason.
192.168.86.202:8080) — the chat channel and reminder delivery. Without it Jarvis is mute but healthy.192.168.86.215:9090) — Uptime Kuma monitor states (monitor_status). Read-only.192.168.86.212:5000) — the internal, unauthenticated API: /api/stats, /api/events. Read-only.https://192.168.86.102:8006, falling back to .101 and .103) — read-only: /cluster/resources, /cluster/status, /cluster/ceph/status, /cluster/backup, /cluster/backup-info/not-backed-up, vzdump task history and logs, and each container's config (for its IP). TLS is verified against the cluster CA (/etc/jarvis/pve-root-ca.pem, public).POST /nodes/{node}/{lxc|qemu}/{id}/status/{start|shutdown|stop|reboot} with the second API token (the code has a regex allow-list; nothing else can be POSTed), and ssh as root to a node with a key that authorized_keys restricts to /usr/local/sbin/jarvis-exec.192.168.86.217:8123) — GET /api/states; POST /api/services/todo/get_items (a read); POST /api/services/todo/add_item pinned to todo.shopping_list (the single write).192.168.86.216:3000) — the public GraphQL page list and the public rendered pages (guests cannot read page text through the API, so Jarvis indexes the rendered HTML).192.168.86.214:8080, v17.5) — REST API v3 with an API token (HTTP Basic, user apikey) belonging to a dedicated jarvis user, sent with Host: project.codycodez.com and X-Forwarded-Proto: https (OpenProject is configured for that host name and redirects plain http). Read: projects, work packages, statuses, types. Write (T1): create a project, add a work package, move one to a closed status, set a project's status to finished. Delete (T2, after approval): a project or a work package. Only fixed endpoints; no path or URL comes from a message./sys/class/hwmon and /proc/pressure as visible inside the container (CPU, NVMe, iGPU temperatures; I/O pressure).api.open-meteo.com, internet) — keyless weather. The only call that leaves the LAN; it sends rounded coordinates for Catonsville, MD.All endpoints are hard-coded in jarvis_tools.py / jarvis_lab.py; no tool accepts a user-supplied URL.
| Port | Protocol | Purpose | Exposed to |
|---|---|---|---|
| 8080 | HTTP | jarvis-brain OpenAI-compatible API |
loopback only |
| 8081 | HTTP | jarvis-fast (only when started) |
loopback only |
| 8091 | HTTP | the bridge's loopback API (chat log, messages and menu data for the web page, voice door), needs the shared key | loopback only |
| 8765 | HTTP | jarvis-web (the Jarvis Console, plain): the page, /api/ask, and Ollama's /api/tags /api/version /api/chat for Home Assistant (bearer token) |
the tailnet address only (100.111.230.69), device list enforced |
| 8766 | HTTPS | the same Console with a real certificate (the microphone needs a secure page) | the tailnet address only |
| 10300 / 10200 | Wyoming | speech-to-text / text-to-speech (jarvis-stt, jarvis-tts-kokoro: Kokoro bm_daniel since 2026-10-14; Piper's jarvis-tts is a disabled fallback) |
the tailnet address; no login, so an nft table admits only Home Assistant (100.75.206.53), loopback and this node's own address |
| — | — | Outbound only: ntfy :8080, Prometheus :9090, Frigate :5000, Proxmox :8006, Proxmox nodes :22 (ssh, approved or allow-listed actions only), Home Assistant :8123, OpenProject :8080, Wiki.js :3000, Open-Meteo :443, Tailscale |
— |
Nothing listens on the LAN address except the container's own sshd on :22 (Debian template default). The bridge's API and the model API are loopback-only; the Console and the speech servers answer on the tailnet address only. Note: systemd's IPAddressAllow= is not enforced inside this unprivileged container, so real network rules are bind addresses and the nft table inet jarvis_fw. Outbound since v0.12 (read-only): the Storj node's dashboard (LXC 114, http://192.168.86.211:14002, GET only) and, for the payout watch only, https://eth.blockscout.com (the node's wallet address, which is public chain data; switch off with storj.payout_watch.chain: false).
| Item | Location |
|---|---|
| Services | jarvis-brain.service, jarvis-bridge.service, jarvis-web.service, jarvis-stt.service, jarvis-tts-kokoro.service (the voice; jarvis-tts.service = Piper, disabled fallback), jarvis-fw.service (the nft rule for the speech ports), tailscaled.service, jarvis-cert.timer (weekly certificate renewal) (all enabled); jarvis-fast.service (not enabled) |
| llama.cpp (built from source, CPU, AVX-512) | /opt/src/llama.cpp — binaries in build/bin |
| Models (GGUF) | /srv/models/gguf: gemma-4-26B-A4B-it-Q4_0 (default brain), gpt-oss-20b-MXFP4, Qwen3.5-4B-Q4_K_M (fast lane) |
| Bridge code | /opt/jarvis/bridge/ntfy_bridge.py (chat, policy, guards, commands, scheduler, watch), jarvis_tools.py (weather, monitors, cameras, calculator, reminders), jarvis_lab.py (read-only Proxmox, Home Assistant, wiki index, host sensors, roll-ups), jarvis_notes.py (notebook), jarvis_actions.py (approval-gated changes) — root-owned |
| Permission policy | /etc/jarvis/policy.json — re-read on every message |
| Bridge state / audit log | /var/lib/jarvis-bridge/: state.json (chat history, daily-briefing time, muted list, /watch state), reminders.json, audit.db (table audit_log) |
Secrets (root-only, handed to the bridge by systemd LoadCredential) |
/etc/jarvis/secrets/: ntfy_bot_password, pve_token (read-only), pve_actions_token (power only), ha_token, exec_key (ssh key). Store with /opt/jarvis/bin/set-secret <name> (hidden prompt). The Proxmox CA /etc/jarvis/pve-root-ca.pem is public. /etc/jarvis/known_hosts holds the three nodes' ssh host keys (root-owned, so the bridge cannot change which hosts it trusts). |
| Node-side script | /usr/local/sbin/jarvis-exec on SuperPC, M910q and NAS (copy: /opt/jarvis/ops/jarvis-exec); optional deny list /etc/jarvis-exec/deny; log: journalctl -t jarvis-exec on the node |
| Rollback copies | /opt/jarvis/backup-v06/ (before v0.7), /opt/jarvis/backup-v05/ |
| Notebook | /srv/notes (git repo, one commit per change) |
| Helper scripts / tests / results / docs | /opt/jarvis/bin, /opt/jarvis/tests, /opt/jarvis/eval, /opt/jarvis/soak, /opt/jarvis/docs (ROADMAP, ARCHITECTURE, OPERATIONS, CHANGELOG, VOICE; wiki sources in docs/wiki), /opt/jarvis/voice (speech groundwork) |
| In-container working notes | /opt/jarvis/README.md |
Brain flags (in the unit file): -t 8 -c 16384 -np 2 --jinja -fa on --reasoning off --cache-reuse 256 --cache-ram 1024 --ctx-checkpoints 3, bound to 127.0.0.1:8080. --cache-ram 1024 matters: llama-server's default 8 GB prompt cache would blow the RAM budget. 8 threads beat 12 for prompt speed.
Permission tiers (policy.json): a tool runs only if listed as tier T0 (observe) or T1 (reversible) with mode auto, or as the T2 proposal tool propose_action with mode ask (it only records a proposal; see Approval-gated actions). T3 (never) has no tool at all.
| Tool | Tier | Notes |
|---|---|---|
get_current_time, get_jarvis_status |
T0 | |
get_weather |
T0 | Open-Meteo, 10-minute cache |
get_service_health |
T0 | Uptime Kuma via Prometheus, 20-second cache; optional name filter; honours /mute |
get_problems |
T0 | roll-up: Kuma monitors + Proxmox nodes/quorum + Ceph + storage + container root disks (≥90%) + backups (failed or >9 days) + cameras; names every check it could not run |
get_cluster_status(what, name) |
T0 | what = nodes, guests, storage, ceph, backups; guests/IPs read live from Proxmox |
get_home_status(query) |
T0 | Home Assistant summary, or entity search |
get_shopping_list |
T0 | HA todo.get_items (a read) |
search_wiki(query) |
T0 | keyword index over the wiki's rendered pages (BM25-style, table-row aware), rebuilt every 15 min |
get_frigate_status, get_frigate_events |
T0 | cameras online/fps/detector; detections by camera/label/window (max 72 h) |
calculate |
T0 | AST allow-list evaluator (numbers, + - * / // % **, a few math functions); no names, attributes or arbitrary calls |
list_notes, search_notes, read_note, list_reminders |
T0 | |
save_note (new notes only), append_note |
T1 | 20 writes/hour; never overwrite or delete |
set_reminder, cancel_reminder |
T1 | 30 changes/hour, 50 pending max, up to one year ahead |
add_to_shopping_list(item) |
T1 | 20 adds/hour; HA todo.add_item, pinned in code to todo.shopping_list; skips duplicates |
propose_action(action, target, command, job, reason) |
T2, mode ask |
records a proposal and nothing else; action is one of start, shutdown, stop (force), restart, backup_add, backup_remove, run_command |
Read-only guarantees, enforced in three places: (1) the read-only Proxmox client (jarvis_lab.py) has no function that sends anything but GET, and its token has only the built-in PVEAuditor role (a stop/start/config attempt returns HTTP 403, tested); (2) the Home Assistant client can call only two allow-listed services, one pinned to the shopping list; (3) no tool exists to start, stop, restart or change anything: the only path to a change is an approved proposal, executed by jarvis_actions.py. The offline suite includes deliberate sabotage checks for each (widening the allow-list, switching to POST, dropping the entity pin all make tests fail).
Cody asked on 2026-10-10 for Jarvis to start, stop and restart containers/VMs, add them to backup jobs and, with his permission, run commands. All of it works as propose -> Cody replies yes -> code executes once:
start, shutdown (graceful, 60 s), stop (FORCE, only if Cody says force/kill), restart, backup_add / backup_remove (the weekly job; only jobs with an explicit guest list, never empties one), run_command (one shell command inside a running container; VMs are not supported).restart jellyfin, please shut down crm and obsidian, add crm to the backup job) is turned into a proposal by code without the model; vaguer wording (jellyfin is stuck, bounce it) makes the model's first move a required propose_action call; commands /start /stop /kill /restart /addbackup /rmbackup /run <guest> <command>; /pending lists open requests, /jobs shows the backup jobs and the guests in none, /tasks [failed|jarvis] shows Proxmox's own task history (who did what, how it ended).yes / no approve or cancel the request Jarvis' last message asked about; yes 7, no 7, yes all name requests explicitly and work for anything still pending. A casual ok / sure is enough for start/shutdown/restart but never for commands, force-stops or backup edits. Requests expire after 15 minutes (commands 10).from-jarvis, one whose id Jarvis published, or one older than 15 minutes never approves anything; talking about something else closes the "bare yes" window.policy.json says.policy.json, block actions): per-action mode (ask / off; auto is honoured only for start, shutdown and restart and is not used), protected_guests, exec_denied_guests, TTLs, command length (500) and timeout (60 s, max 120), hourly limits.jarvis-exec (forced command): verbs ping, exec <vmid> <seconds> <base64>, backup-show|add|remove <job> <vmid>; the command arrives base64-encoded and is passed to pct exec as ONE argument (never through a host shell); output is capped at 60 KB; inside the container timeout stops runaway commands (verified: a 100-second sleep was stopped at 60 s and left nothing running). Backup edits change only the vmid list, are checked afterwards and put back if anything else differs./audit and the approvals table list every proposal, decision and result. For commands the audit keeps the command, exit code, output size and a hash of the output, never the output. Proxmox's own task log shows the work as user jarvis@pve with token actions.status/current), not the cluster-wide list, which Proxmox refreshes only every ~10 s and which still said "running" just after a successful shutdown (found by reading the first live run's log). The same live status is used for the "is it already running?" checks.Other limits: 60 chats/hour, 8 tool calls per message, 2,000 input characters, 20 shopping adds/hour.
Commands (about 40 names with aliases, answered by code with no model call; data commands never write): /problems /cluster /guests /ceph /storage /backups /temps /ip /lab /cams /home /ha /shopping /wiki /weather /audit /status /help read; /add /remind /mute /unmute /watch /brief /briefing /cancel /reset (also /clear /new /forget) /pause /resume act. /add and /remind call the same dispatch() as the model's tools, so the policy file, hourly budgets and audit log apply identically.
Watch (/watch on, a thread, off by default): every 5 minutes it runs the /problems check; a problem seen on 2 consecutive checks and not yet announced is sent (priority 4 for Ceph/offline/failed/quorum/OSD, 3 otherwise, 1 during Home Assistant quiet hours), a cleared one is sent as resolved (priority 2); a source it could not read never counts as resolved, and after 3 blind checks it says so once; at most 6 alerts/hour, then one "holding back N" note; muting a known problem removes it silently; the told/seen state is persisted so a restart repeats nothing.
Scheduler (a thread in the bridge, 5-second tick): delivers due reminders at ntfy priority 4 with the alarm_clock tag (a reminder is marked fired before it is sent, so a crash can never double-send; one that was due while the bridge was down arrives late with a "was due" note) and sends the daily briefing when configured (/briefing HH:MM; not sent if more than 6 hours late). The briefing is built by plain code from the tools, not by the model.
Safeguards in the bridge (each exists because of a measured failure — see Known Issues):
save_note or append_note is accepted), the model's first move is a required tool call (and only a call to an expected tool is accepted). Questions such as "do you remember my locker code?" are never forced. Measured under a 20-message history, such requests had averaged 14-19 s (up to 50 s) because the claim verifier had to correct the model, and one append went to the wrong tool; they now take about 3-4 s. A reminder with no time is not forced, so the model can ask "when?"... or symlinks, size limits, git commit per write.from-jarvis and ignores them, remembers message ids across restarts, and skips messages older than 15 minutes.jarvis_web.py listens on the tailnet address only. A request is accepted only if it comes from a tailnet address, tailscaled itself (LocalAPI whois over its unix socket) says the device belongs to an allowed login, and the device's name is on the allowed list (/etc/jarvis/web_access.json: logins, nodes, hosts; live-reloaded, a missing or broken file allows nobody). The name list exists because the tailnet also holds internet-facing containers. Any other Host header gets HTTP 421 (DNS-rebinding defence). CSRF rules (a custom header, the right content type, a matching Origin) apply to every change. Allowed today: iphone-14, personal-pc, lenovo-laptop, my-personal-pc, ipad157./api/commands, /api/choices; catalog in jarvis_commands.py), "Pick a container..." and "Pick a project..." panels, push-to-talk (/api/voice: browser audio -> Whisper -> the voice door -> Piper) and read-aloud (/api/speak). Layout: a column on a phone with a bottom sheet for the menus; from 900 px wide a sidebar plus the chat using the whole window.tailscale cert for jarvis.tailbacb1d.ts.net (public in certificate-transparency logs; renewed by jarvis-cert.timer).auto is read as ask, never uses the allow-list, approval requests go to the chat. Endpoints: POST /api/ask, and Ollama's dialect GET /api/tags, GET /api/version, POST /api/chat (bearer voice_token; only the last user message is used).systemctl stop jarvis-web (the chat in ntfy keeps working); add a device = add its name to nodes; deploy and roll back with ops/deploy_v11.sh / rollback_v11.sh.Module bridge/jarvis_storj.py, policy block storj, notes and numbers in docs/STORJ.md (wiki: Storj Node).
/storj, /storj money, /storj forecast, /storj payouts; model tool get_storj_status (T0/auto in the policy); a question that names Storj fetches the numbers before the model answers; a Storj group in the Console menu./api/sno/, /api/sno/satellites, /api/sno/estimated-payout, /api/heldamount/paystubs/<from>/<to>, /api/heldamount/held-history), the node address must be a private http address, text is built from numbers only. A test scans the source for anything that could write./problems and the watch: disqualified or suspended on any satellite (high priority), audit < 99%, suspension < 95%, online < 95%, software not up to date, QUIC not OK, no satellite contact for 2 hours, allowance 95% full, dashboard not answering. /mute storj silences them.every_s (default 7200) in its own thread: Blockscout's incoming ERC-20 transfers for the node's wallet; a payment is a transfer of the STORJ or USDC contract from a sender on payout_watch.senders; first pass silent, then one ntfy message per payment ("Storj payout", tag moneybag), also in the chat log and the audit; state storj in /var/lib/jarvis-bridge/state.json. chain: false = only the satellites' own paid-out total. It reaches ntfy even while a web message is being answered (the web flag is per thread; tested).storj.enabled, url, rates, alerts, disk_tb, disk_overhead, payout_watch.{enabled,chain,every_s,senders}; all re-read on each call.test_storj_offline.py 212 checks, mutate_storj.py 85 sabotaged copies. Deploy/rollback ops/deploy_v12.sh / rollback_v12.sh.Policy actions.exec_allowlist (enabled, scope, optional patterns). A command runs without a yes only if it equals one of 27 reviewed read-only entries or fits a template ({unit}, {n} 1-200, {name}), in a running container that is not ntfy (127) or Jarvis (130), and never for a spoken request. The stored row is decided_by policy:allowlist, is re-checked when it runs, counts against the hourly limit and is audited. Anything else still waits for a yes.
Policy warmup (enabled, time 00:05): one prompt read with the new day's date so the first message of the day is fast. Why: the system prompt ends with the date, and a changed word near the start of a prompt makes the model's saved working memory useless for everything after it (measured: 0 of 3,470 tokens reused, 28 s; unchanged: 99.6%, 1 s).
jarvis_projects.py. Tools: get_projects (T0) and project_request (T1; the delete operations only create an approval row). Commands: /projects, /project <name>, /newproject, /addtask <project>: <task>, /done [<project>:] <task>, /finish <project>, /rmproject, /rmtask. Plain English ("create a project called Garden", "add a task buy mulch to the Garden project", "check off buy mulch", "delete the Garden project") is parsed by code first; the model handles other wording and its first move is a required project_request call.jarvis and its API token in /etc/jarvis/secrets/openproject_token (set-secret; read on every call). Creating projects needs the global permission Create project; deleting projects needs an administrator (an OpenProject rule); without it OpenProject refuses and Jarvis says so. Check everything with python3 -I /opt/jarvis/bridge/jarvis_projects.py selftest (creates, checks off and deletes a throw-away project).finished exists (/api/v3/project_statuses/finished), the closed-status filter operator is c, project deletion is administrator-only, and both the older /projects/{id}/work_packages and the newer /workspaces/{id}/work_packages routes exist (the code uses whichever answers).jarvis-bot (read-write on topic jarvis only). Its password is in /etc/jarvis/secrets/ntfy_bot_password (root-only, mode 600) and is handed to the bridge through a systemd credential — the bridge user cannot read the secrets directory.pct enter 130, then /opt/jarvis/bin/set-secret <name>.cody and jarvis-bot have read-write on jarvis; anonymous access is denied (deny-all default). The ntfy request limit is raised to burst 150 / one per 2 s (see Change Log).jarvis@pve (no password, cannot log in) with API token bridge (privilege separation on), ACL PVEAuditor on / for both the user and the token. List: pveum user token list jarvis@pve; revoke: pveum user token remove jarvis@pve bridge.jarvis@pve has a second token actions (privilege separation on) with the custom role JarvisOps = VM.PowerMgmt on /vms, and NoAccess on /vms/127 (ntfy) and /vms/130 (Jarvis). The read-only token also needs PVEAuditor on /vms (see Known Issues: a more specific ACL replaces inherited roles). Check: pveum user permissions 'jarvis@pve!actions' --path /vms/125. Revoke: pveum user token remove jarvis@pve actions./etc/jarvis/secrets/exec_key, never leaves it); the public half is one line in /etc/pve/priv/authorized_keys (cluster-wide): restrict,from="192.168.86.226",command="/usr/local/sbin/jarvis-exec" ssh-ed25519 ... jarvis-exec@130. Backup of the file before the change: /root/authorized_keys.bak-20261010-jarvis-exec. Revoke: delete that line.ha_token. It belongs to the owner/admin account (Cody); Cody confirmed on 2026-10-10 that an admin token is fine. The code allows only GET states, todo.get_items and todo.add_item pinned to the shopping list.web_api_key) and the voice token (voice_token) are delivered to jarvis-web as systemd credentials; the certificate key is readable by the jarvis-web group only. The service can read nothing else.jarvis user in openproject_token; revoke by deleting it in OpenProject or removing the file.backup-0343c483-22ec, 06:00 Sunday, zstd, to backup-storage). A first manual backup succeeded: 1.7 GB of root disk, 635 MB archive, 3 min 47 s throttled, brain unaffected apart from the brief snapshot freeze./mnt/pve/cephfs/jarvis-notes) is deliberately not backed up (Cody's choice; it has its own git history but no off-box copy); models are re-downloadable (/opt/jarvis/bin/fetch-models.sh; sources: ggml-org/gemma-4-26B-A4B-it-GGUF, ggml-org/gpt-oss-20b-GGUF, unsloth/Qwen3.5-4B-GGUF on Hugging Face)./etc/jarvis/secrets, including the two Proxmox tokens, the ssh key, the web password hash, the loopback key and the OpenProject token) are in the rootfs, so they are in the backup archive. Treat backup-storage accordingly.# llama.cpp (rebuild, then restart the brain)
pct exec 130 -- bash -c 'cd /opt/src/llama.cpp && git pull && cmake -B build -DCMAKE_BUILD_TYPE=Release -DGGML_NATIVE=ON -DLLAMA_CURL=ON -DGGML_CCACHE=ON && nice -n 10 cmake --build build -j 10 --target llama-server llama-bench llama-cli'
pct exec 130 -- systemctl restart jarvis-brain # cold model load can take ~2 minutes
# bridge: copy the new files into /opt/jarvis/bridge (root-owned), then
pct exec 130 -- systemctl restart jarvis-bridge
After ANY change to the system prompt, tools, models or llama.cpp, re-run the tests — prompt rules interact (one added rule fixed recall 21/30 → 30/30 but dropped append first-try accuracy 42/50 → 17/50 until a second sentence fixed it):
# offline, seconds, no model needed
pct exec 130 -- python3 -I /opt/jarvis/tests/test_bridge_offline.py /opt/jarvis/bridge/ntfy_bridge.py # 72 checks: policy gate, notebook, verifier
pct exec 130 -- python3 -I /opt/jarvis/tests/test_bridge_v05_offline.py /opt/jarvis/bridge/ntfy_bridge.py /opt/jarvis/tests/policy.json # 88: guards, reminders, scheduler, briefing, escalation, math guard
pct exec 130 -- python3 -I /opt/jarvis/tests/test_bridge_v06_offline.py /opt/jarvis/bridge/ntfy_bridge.py /etc/jarvis/policy.json # 305: new tools, guard routing, shopping verifier, first-move guards, commands, mute, watch, empty replies, cache behaviour, real-use fixes
pct exec 130 -- python3 -I /opt/jarvis/tests/test_lab_offline.py /opt/jarvis/bridge/jarvis_lab.py /opt/jarvis/tests/fixtures # 131: Proxmox/HA/wiki against REAL captured responses with a fake transport
pct exec 130 -- python3 -I /opt/jarvis/tests/test_tools_offline.py /opt/jarvis/bridge/jarvis_tools.py # 34
pct exec 130 -- python3 -I /opt/jarvis/tests/test_notes_offline.py /opt/jarvis/bridge/jarvis_notes.py # 39
pct exec 130 -- python3 -I /opt/jarvis/tests/test_actions_offline.py /opt/jarvis/bridge /etc/jarvis/policy.json /opt/jarvis/ops/jarvis-exec # 406: approvals, execution, POST allow-list, the ssh channel, jarvis-exec, the whole flow, secrets
pct exec 130 -- python3 -I /opt/jarvis/tests/mutate_actions.py /opt/jarvis/staging-vNN # 64 deliberately sabotaged copies; every one must make the tests fail
pct exec 130 -- python3 -I /opt/jarvis/tests/test_projects_offline.py /opt/jarvis/bridge # 259: the OpenProject client against a fake OpenProject, the plain-English parser
pct exec 130 -- python3 -I /opt/jarvis/tests/test_web_offline.py /opt/jarvis/bridge /opt/jarvis/web # 118: chat log, loopback API, web service over real HTTP
pct exec 130 -- python3 -I /opt/jarvis/tests/mutate_doors.py /opt/jarvis/staging-vNN # 95 sabotaged copies of the web/voice/OpenProject code; every one must fail a test
pct exec 130 -- bash /opt/jarvis/ops/run_all_suites.sh /opt/jarvis/bridge /etc/jarvis/policy.json /opt/jarvis/web # every offline suite in one go
# live, with the real brain and data sources (real model calls, so these take a while); BRIDGE_DIR defaults to /opt/jarvis/bridge
pct exec 130 -- python3 -I /opt/jarvis/eval/tool_reliability9.py 6 # 93 scenarios (74 + 19 OpenProject against a fake OpenProject; --proj runs only those) (add --new, --old or --act to run a part); Proxmox/ssh calls are stubbed to fail if attempted
pct exec 130 -- env LONG=1 python3 -I /opt/jarvis/eval/tool_reliability7.py 4 # the same under a realistic 20-message conversation (this found real failures the empty-history run hid)
pct exec 130 -- python3 /opt/jarvis/eval/bridge_e2e6.py # full suite over real ntfy incl. reminders that must arrive and a secret-leak scan (~15 min; briefly stops the brain)
# python3 /opt/jarvis/eval/web_e2e9.py (v0.9 password mode only; not used since v0.10)
python3 /opt/jarvis/eval/bridge_e2e7.py # run on the Proxmox HOST: real ntfy + real Proxmox + real ssh on test guest 128, verified on the host (~5 min)
v0.9: deploy_v09.sh backs up v0.7 once to /opt/jarvis/backup-v07/, installs the bridge, the web service and the units, re-runs every offline suite on the deployed files and only then restarts; rollback_v09.sh restores v0.7. Earlier releases: deploy/rollback scripts keep the previous version: deploy_v07.sh backs up v0.6 once to /opt/jarvis/backup-v06/, re-runs every offline suite on the deployed files and only then restarts; rollback_v07.sh restores it. (Older: /opt/jarvis/backup-v05/.) The tool list is at the start of the prompt, so the first message after a deploy re-reads everything once (~15 s).
Not in Uptime Kuma yet. Health check today: send /status in the jarvis topic, or pct exec 130 -- systemctl is-active jarvis-brain jarvis-bridge. Every tool call, refusal, guard action and fired reminder is in audit.db:
pct exec 130 -- sqlite3 -readonly /var/lib/jarvis-bridge/audit.db "select datetime(ts,'unixepoch','localtime'),tier,action,reasoning,substr(result,1,80) from audit_log order by id desc limit 20"
jarvis in the admin console: the Console and voice then stop (the ntfy chat does not).warmup.jarvis user may be a non-admin, in which case deletions are refused by OpenProject with a clear message.eval/chat_latency.py on a growing conversation: 22-40 s and 3-5k tokens re-processed per write message). Forced calls therefore send the same 21-tool list as normal calls and only the expected tool's call is accepted; chat history is trimmed in one 40% step instead of sliding one exchange at a time. After the fix an 18-message conversation averages 5.2 s (median 4.5 s, p90 9.9 s; before: mean 10.5 s, p90 32.6 s). The first message after a restart or a long idle still pays a cold read (~15 s).infrastructure/servers/nas, infrastructure/servers, services/agent-hub, services/wiki-js) listed the NAS as 192.168.86.100; Proxmox and the IP plan say .103 (.100 is the Verizon Fios extender; confirmed by MAC address and by the NAS's own interface). Corrected on 2026-10-10 (also reference/ports, which still said .100-.102). For container/node IPs Jarvis still asks Proxmox first and trusts it over the wiki.binary_sensor.svc_pi_hole, homelab_offline_count) reports Pi-hole offline because input_text.pihole_ip is empty; Pi-hole itself answers DNS. The smart plugs are not fully set up in Home Assistant yet (Cody, 2026-10-10: expected): earlier their watt readings were implausibly low (SuperPC 3.2 W) and now all four report unavailable. Jarvis reports what HA says.crm 101, obsidian 102, AI-Recipe-Book 112, hanna-properties-backend 123, terraria-ai 128; all stopped). crm and AI-Recipe-Book were still in the 2026-10-04 run. /backups lists them.pct start 130.ntfy.sh for iOS push; heavy test traffic made ntfy.sh answer HTTP 429 (harmless for chat, it only delays iOS instant notifications).safe.directory exception and files edited as root need chgrp -R 100987 . && chmod -R g+rwX . (see the User Guide).JarvisOps on /vms to jarvis@pve alone removed VM.Audit below /vms, so the read-only token (whose rights are capped by the user's) saw zero guests for about two minutes. Fix: also grant PVEAuditor on /vms. Anything that grants roles to jarvis@pve must be re-checked with pveum user permissions 'jarvis@pve!bridge' --path /vms/125.av>=14,<16.| Symptom | Check | Fix |
|---|---|---|
| No reply to chat | /status; pct status 130; paused? |
pct start 130; send /resume |
| "My brain isn't answering" | systemctl status jarvis-brain |
wait for the model to load; systemctl restart jarvis-brain |
| Reminder did not arrive | /reminders; journalctl -u jarvis-bridge; audit_log rows reminder_fired |
check the LXC was running at the due time; check ntfy app settings for priority-4 messages |
| "unavailable" for weather, lab or cameras | curl the source from the LXC: Open-Meteo, 192.168.86.215:9090, 192.168.86.212:5000 |
fix the source service; Jarvis reports the outage instead of guessing |
| "Proxmox is unreachable" / HTTP 401 / 403 from Proxmox | pveum user token list jarvis@pve; pveum acl list (PVEAuditor on / for the user AND the token) |
recreate the token; set-secret pve_token |
| "Home Assistant answered HTTP 401" | token revoked | new long-lived token → set-secret ha_token → restart bridge |
| Wiki answers missing or stale | index rebuilds every 15 min; curl the wiki page from the LXC |
restart the bridge to force a rebuild |
| "couldn't ssh to ..." / "unexpected answer from ..." on an approved command or backup edit | from the LXC: ssh -F /dev/null -i /etc/jarvis/secrets/exec_key -o UserKnownHostsFile=/etc/jarvis/known_hosts root@NODE ping; check the authorized_keys line, from=, the host key in known_hosts |
fix that; the approval stays recorded as failed, ask again |
| Approved power action says HTTP 403 | pveum user permissions 'jarvis@pve!actions' --path /vms/NNN must list VM.PowerMgmt (127 and 130 deliberately show nothing) |
restore the ACL |
| "... isn't waiting any more" | the request expired (15 min; commands 10) or was already answered: /pending, /audit |
ask again |
| Chat works but notes don't save | audit_log rows with refused/denied; ls -ld /srv/notes; git -C /srv/notes status |
check group jarvis-notes and the mount; write limit is 20/hour |
HTTP 429 in journalctl -u jarvis-bridge |
ntfy per-IP request limit | wait, or raise the limit (Change Log) |
fatal: dubious ownership on the host |
host git ownership check | use the safe.directory exception (User Guide) |
| Slow replies | /status (tok/s), uptime, host PSI |
stop other heavy jobs; normal range is 3–15 s |
pct stop 130 ; pct start 130 # kill switch / start
pct enter 130
journalctl -u jarvis-bridge -f ; journalctl -u jarvis-brain -f
/opt/jarvis/bin/set-secret <name> # store a secret without echo
/opt/jarvis/bin/ntfy-selftest.sh # 13 ACL checks against the ntfy server
The plan, phase status and progress log live on Jarvis: goal, plan and progress log; how it was built, step by step, on Jarvis: how it was built. Next, in order: Cody's own steps (Storj log rotation, Tailscale key expiry, an OpenProject jarvis user), a voice satellite and wake word, more read-only integrations, Home Assistant control once the lights exist.
| Date | Change |
|---|---|
| 2026-10-13 | v0.12: the read-only Storj check (/storj, tool, menu, Storj in /problems and the watch) and the payout watch (public explorer, token contract + sender list); policy block storj; 2,054 offline checks, 85 new sabotage copies. Web chat stays off ntfy (verified), watch/payout/reminders do not. |
| 2026-10-13 | v0.11: read-only command allow-list ON for every running container (27 entries, actions.exec_allowlist); midnight warm-up (warmup); the web conversation stays off ntfy; the Jarvis Console (desktop layout, command menus with live dropdowns, container/project panels, push-to-talk and read-aloud, HTTPS listener with a tailscale cert certificate and jarvis-cert.timer); an execution gate so "Running" always precedes "done"; hanna-properties-backend rootfs moved to Ceph. 1,842 offline checks; 243 sabotaged copies, all caught. |
| 2026-10-13 | v0.10: Tailscale login for the web page (device list, Host rule), Ollama-compatible voice door, speech servers on the tailnet behind an nft rule, Home Assistant wired (Wyoming x2, Ollama, pipeline Jarvis), OpenProject connected (the bridge unit now loads its token). |
| 2026-10-11 | Bridge v0.9: jarvis-web (phone chat page with approve buttons and the voice endpoint; separate unprivileged service, loopback API with a key, scrypt login, throttling, CSRF checks, strict CSP); chat log; the voice door (never approves, auto read as ask); speech services as units (loopback, off); OpenProject (jarvis_projects.py: read, create/check off, deletions through the approval flow, plain-English parser, self-test); approvals record the door (cody (web)); new suites (test_web_offline.py, test_projects_offline.py), mutate_doors.py (95 sabotages), tool_reliability9.py (93 scenarios), web_e2e9.py. Access changes: users jarvis-web, jarvis-voice in the container; secrets web_api_key, voice_token (empty), later web_password_hash and openproject_token; port 8765 on the container's LAN address. |
| 2026-10-09 | LXC 130 created. llama.cpp built; Gemma 4 26B-A4B, gpt-oss-20b and Qwen3.5-4B downloaded and benchmarked. 42-minute soak test (553 requests, no errors/restarts/OOM, Ceph and Frigate unaffected). |
| 2026-10-09 | ntfy (LXC 127): user jarvis-bot and topic jarvis added; request limit raised from burst 60 / 1 per 5 s to burst 150 / 1 per 2 s (NTFY_VISITOR_REQUEST_LIMIT_BURST/REPLENISH in /opt/agent-hub/ntfy/docker-compose.yml; backup docker-compose.yml.bak-20261009-ratelimit). |
| 2026-10-09 | ntfy chat bridge (v0.4.1), policy gate, audit log and git-versioned notebook deployed; safeguards added after measuring fabricated actions. |
| 2026-10-09 | Bridge v0.5: weather (Open-Meteo), homelab monitor health (Prometheus/Uptime Kuma), cameras (Frigate status + events), calculator, reminders with a scheduler, daily briefing (/brief, /briefing), clock-time parsing in code, reminder claims added to the verifier. |
| 2026-10-10 | LXC 130 added to the Sunday vzdump job (notebook excluded by choice); first manual backup verified. |
| 2026-10-10 | Proxmox: user jarvis@pve + API token bridge, role PVEAuditor on / (read-only; writes verified refused with HTTP 403). Home Assistant long-lived token stored as ha_token. |
| 2026-10-10 | Fixes from Cody's own use of v0.5 (audit log): a pasted metrics table no longer triggers the math guard (it had taken 56 s); /clear now works; "save a note with the current Ceph status" / "save metrics from Proxmox" now has the data to write; imperative note requests get the first-move guard. |
| 2026-10-10 | Bridge v0.6 follow-ups, found by live testing: retries empty model replies; write first-move guard (reminders with a time, shopping adds, appends to a named note); forced calls reuse the prompt cache (write requests 22-40 s -> 3-5 s in a real conversation). |
| 2026-10-10 | Bridge v0.7: approval-gated Proxmox actions (jarvis_actions.py): start / shut down / force-stop / restart, backup-job edits, commands inside containers; token jarvis@pve!actions (role JarvisOps, no access to 127/130); ssh forced command jarvis-exec on the three nodes; commands /start /stop /kill /restart /addbackup /rmbackup /run /pending /jobs; verified live on test container 128 (start, tricky-quoting command, hung command stopped at 60 s, restart, backup-job add/remove with jobs.cfg byte-identical, forged approvals ignored, no secret in any log). Voice groundwork. Wiki NAS address corrected. |
| 2026-10-10 | Bridge v0.6: new module jarvis_lab.py — Proxmox/Ceph/backups/storage/IPs, Home Assistant, wiki search, host sensors, get_problems roll-up; add_to_shopping_list (T1); about 20 new commands incl. /watch, /mute, /audit, /add, /remind, /ip; briefing includes cluster and home; empty-reply retry; uptime monitors for the externally disabled domains muted. |