Every Jarvis release, newest first: what changed, why, what was found on the way, how it was tested and what access it needed. The plan and progress log is Jarvis: goal, plan and progress log; technical reference Jarvis; how to use it User Guide; how it was built Jarvis: how it was built. This page is generated from
docs/CHANGELOG.mdin the container (/opt/jarvis/docs) at each release.
Last updated 2026-10-14 (bridge v0.12). The Jarvis Console (web page) is at v0.11; the speech servers and Home Assistant wiring date from v0.10.
| Version | Date | Headline |
|---|---|---|
| v0.12 | 2026-10-14 (night) | The British butler voice, a clearer storage report |
| v0.12 | 2026-10-13 (late evening, 22:00 local) | A read-only Storj check, a payout watch, Storj problems in the watch |
| v0.11 | 2026-10-13 (evening) | Commands that run without asking, a midnight warm-up, a quiet web page, the Jarvis Console, talking in the browser |
| v0.10 | 2026-10-13 | Tailscale login, voice through Home Assistant, housekeeping |
| v0.9 | 2026-10-11 | A phone web page, a voice endpoint, OpenProject |
| v0.7 | 2026-10-10 (evening) | Proxmox actions with approval |
| v0.6 | 2026-10-10 | Read-only Proxmox, Ceph, backups, storage, IPs and temperatures; Home Assistant; wiki search; /problems; opt-in /watch; /mute |
| v0.1-v0.5 | 2026-10-09 | LXC 130 and the local model (bake-off), ntfy chat, notebook, weather, monitors, cameras, calculator, reminders, daily briefing, guards |
(v0.8, the loopback API and the first web layer, was folded into the v0.9 entry.)
bm_daniel (British male) at 1.05x (Cody picked it by ear from nine samples: "closest"; and asked for it about 1.1x faster than the 0.95x of the samples, so 0.95 x 1.1 = 1.05). One speech service now answers on tailnet:10200 for everything: jarvis-tts-kokoro.service (voice/wyoming_kokoro.py in its own venv voice/venv-kokoro; the Piper unit jarvis-tts is disabled but installed). Home Assistant's Wyoming text-to-speech entry was reloaded and the Jarvis Assist pipeline's voice set to bm_daniel (ops/ha_voice_set_tts.py, token on stdin); the Console reads answers aloud with it (TTS_VOICE in a jarvis-web drop-in). Measured: about 10x real time on the CPU, a sentence in 0.3-0.4 s; the end-to-end test through Home Assistant (ops/ha_voice_test.py with TEST_VOICE=bm_daniel) passes: text -> Jarvis -> voice, and spoken question -> Whisper -> Jarvis -> voice. Switch script ops/switch_voice_v12.sh [voice] [speed] (re-runnable), rollback ops/rollback_voice_v12.sh (back to Piper en_GB-alan-medium). Change the speed: set KOKORO_SPEED in the unit, systemctl daemon-reload && systemctl restart jarvis-tts-kokoro; no change in Home Assistant needed.jarvis_lab.pve_storage_report): every percentage now says "used" ("54% used", "Plenty of room (percent USED): ... boot-disks 2% used"), container disks say "84% full". New checks in test_lab_offline.py (143) require the word on every figure. Real figures that day: boot-disks 2.1% used, cephfs 15.3%, local-lvm 53.8%.ops/deploy_v12.sh (idempotent).Cody asked after the Storj audit (docs/STORJ.md): "add a read only check like this to jarvis", "add some sort of watch too for when I get paid out", and to keep ntfy for the Jarvis watch and other important things but not for web chat.
bridge/jarvis_storj.py, new). /storj (health), /storj money (earned / held / paid by month, the hold-back schedule by the node's age, what a month earns at today's size), /storj forecast (when the allowance and the drive fill, what a month pays at 5 / 8 / 12 / 16 TB, the likely level-off), /storj payouts (what reached the wallet). The model has a tool for it (get_storj_status, T0, policy-gated, audited) and a plain-English guard: a question that names Storj fetches the live numbers BEFORE the model answers (it picks status / money / forecast / payouts from the words). A Storj group in the Console menu. Everything is text built by code from numbers; nothing the node or the explorer writes (names, symbols, memos) can reach a message.http://192.168.86.211:14002, a fixed list of paths; the address in the policy must be a private http address; redirects refused; answers capped at 1.5 MB) and, for the payout watch, to one public explorer page. A test scans the source for anything that could write.0x303EDC...b44B): address-poisoning dust and look-alike tokens, which do land on this wallet, never count. The first pass after switching on only learns (no replay of old payments); new ones give one ntfy message each ("Storj paid you 0.79 USDC (about $0.79)...", title "Storj payout", tag moneybag), also in the chat log and the audit. payout_watch.chain: false = no outside lookup at all, only the satellites' own paid-out total. Live first pass 2026-10-14 02:03 UTC: learned the 2 existing payments, said nothing./problems and /watch. jarvis_lab.collect_problems asks the module: disqualified, suspended, audit < 99%, suspension < 95%, online < 95%, software not up to date, QUIC not OK, no satellite contact for 2 h, allowance 95% full, dashboard not answering. Disqualified / suspended alerts are high priority; /mute storj silences them like any known item.storj block and the lab suite points POLICY_FILE nowhere (tests must never touch a real system); (2) the new policy interval was falling back to the default instead of clamping (found by the first run of the new suite); (3) the source scan for "read-only" tripped on the word POST in a docstring, so the docstring was reworded; (4) four gaps in the new tests were found before the sabotage run (look-alike private ranges such as 172.32.x.x, a payout change under a cent, a shrinking node, a boolean accepted as a number).test_storj_offline.py 212: policy parsing, the network layer against a real local fake node and fake explorer, recorded real numbers, the reports, each problem, every rule for what counts as a payment, the watch including outages and local mode, the bridge wiring and the web-silence/payout interaction); sabotage: tests/mutate_storj.py 85 of 85 broken copies caught (plus 243 earlier). Deploy/rollback: ops/deploy_v12.sh, ops/rollback_v12.sh (state before: backup-v11/); the staged copy of run_all_suites.sh is used as the gate.storj and tool get_storj_status (merged into the live policy, nothing else touched); Jarvis now reaches LXC 114 port 14002 (LAN) and eth.blockscout.com:443 (the wallet address only). No new secrets. The first message after the deploy took 36 s (the tool list changed, so the prompt was re-read once).ops/storj-log-rotation.sh (container recreated 2026-10-14 00:58 UTC; identity and data untouched); Tailscale key expiry for jarvis is off (Cody); the OpenProject token stays the built-in admin's by Cody's decision.en_GB-alan-medium voice "not what I had in mind" and wants a British butler as close to the films' Jarvis as possible. He approved the downloads; Kokoro-82M (British male voices) and Piper en_GB-vctk-medium are installed beside the old voice (Kokoro in its own venv, with a tested Wyoming wrapper and a disabled unit); nine audition samples were sent to him; the switch waits for his pick (steps in VOICE.md).docs/EXEC-ALLOWLIST-PROPOSAL.md and said "enable the command allow list to all machines"). A command runs without asking only if, after collapsing whitespace, it EQUALS one of 27 reviewed entries or fits a template (systemctl status {unit} --no-pager, systemctl is-active {unit}, journalctl -u {unit} -n {n} --no-pager, journalctl -p err -n {n} --no-pager, docker logs --tail {n} {name}; {unit}/{name} are a few safe characters and never start with -, {n} is 1-200). The other 22 are fixed commands: uptime, date, hostname, timedatectl, cat /proc/loadavg, free -h, df -h, df -i, du -xh --max-depth=1 /, uname -a, cat /etc/os-release, ip -br addr, ip route, ss -tlnp, ps aux --sort=-%mem, ps aux --sort=-%cpu, top -bn1, apt list --upgradable, systemctl --failed --no-pager, docker ps, docker ps -a, docker stats --no-stream. "All machines" means every running container; it never applies to the Proxmox hosts (commands never run there), to ntfy (127) or Jarvis (130), or to a spoken request (the voice door may still propose a command, which then waits for a yes). The matcher (jarvis_actions.allow_match) refuses any entry with shell syntax at policy-read time, never rewrites an untidy entry (it drops it), rejects anything multi-line (df\n-h would otherwise collapse into a listed command), and what is stored and run is the normalised string that matched. The row is created already approved with decided_by = policy:allowlist, is re-checked when it executes (so switching the allow-list off stops a row created a moment earlier), counts against the hourly limit for approved actions, and is audited. /run storj df -h answers "Running #N now (a read-only command on the allow-list, no yes needed)" and later "#N done" with the output; any other command still shows the exact text and waits for his yes. Policy: actions.exec_allowlist = {enabled, scope: "all" | [ids], patterns: [...] (optional, replaces the built-in list)}; enabled: false switches it off at once. Live check: uptime in network ran in 0.4 s; ls /root asked and was denied.Today is <weekday date>.; when the date changes, Gemma's sliding-window attention cannot reuse the memory it saved after that point (llama.cpp keeps only a few snapshots, all taken after the changed words), so the first message of the new day re-reads the whole 3.5-6.5k-token prompt at CPU speed (about 100-140 tokens/s = 28-66 s), while every later message reads only its own ~20 words (1 s). Measured with the real prompt: date unchanged -> 3,457 of 3,470 tokens reused, 1 s; only the date changed -> 0 reused, 28 s. warm_cache() sends the REAL system prompt (with the new date), the remembered conversation, a throw-away last user message and the same tool list with max_tokens: 1 at 00:05 local (policy warmup: {enabled, time}; 6-hour catch-up window if the bridge was down; skipped while paused), so the morning's first message finds the memory warm. Nothing is added to the conversation, the chat log or ntfy; an audit row warmup records the seconds and how many tokens were read. Cost: one prompt read a day. Live smoke test: 0.7 s today (3,418 of 3,456 tokens were already cached); the first real midnight run is 2026-10-14 00:05 (check /audit)./watch alerts, answers to messages typed in ntfy) still goes to ntfy and also appears on the page. Done with a per-thread door flag (CHAN.ch == "web") that the approval thread and the "Working on it" timer inherit explicitly. A conversation summary notification was NOT built (Cody: "not needed").publish() or when the handler returns); proven with a deliberately slow publish.https://jarvis.tailbacb1d.ts.net:8766). Phone layout unchanged (a column with the quick chips, a Commands button and a bottom sheet). New desktop layout (from 900 px wide): the window is used in full: a 380 px sidebar (brand and status, waiting approvals, search, the command menus) and the chat taking the rest, with wider bubbles. Command menus: nine groups by what the commands do (is something wrong, Proxmox and storage, look inside a container, change things, projects, home, notes and reminders, alerts, Jarvis itself), each item with a plain description and a badge (small change / asks first / no yes needed). Items with arguments open a small form with live dropdowns (containers, running containers only for checks, projects, the open tasks of the chosen project, notes, reminders, the 27 allow-listed checks with blanks to fill in for templates), a Send button and "Edit first" (puts the text in the message box). Two panels start from the thing instead of the command: Pick a container... (details, backups, address; the quick checks; restart/start/shut down, which ask first) and Pick a project... (status, finish, add a task, every open task with a one-tap Done). Every button just sends the same text typing it would, through the normal door, so the usual rules apply. /reset and /pause need a second tap. Data comes from GET /api/commands and GET /api/choices?kind=guests|projects|tasks|notes|reminders|checks (the web service passes only kind, project and running=1 to the bridge's loopback /v1/commands and /v1/choices; the catalog lives in jarvis_commands.py). A "speak" link under every Jarvis message reads it aloud; a speaker toggle reads every new answer aloud.jarvis.tailbacb1d.ts.net obtained with tailscale cert (note: Tailscale certificates are listed in public certificate-transparency logs, so that host name is publicly visible; no data is). Same handler, same device list, same Host rule; TLS 1.2+; handshakes happen in the request's thread so a slow or hostile client cannot stall the listener. The certificate (90 days) is renewed by jarvis-cert.timer (weekly; ops/renew-cert.sh restarts the page only if the certificate changed). The microphone button records in the page, converts to 16 kHz 16-bit mono, and POST /api/voice sends it to Whisper (a small stdlib Wyoming client in jarvis_web.py), the text goes through the voice door (it can never approve or run anything unasked), and with the speaker on Piper reads the answer back. Limits: 30 s per clip, one spoken request at a time. The exchange is shown on the page only (the voice door does not write to the shared chat log). Audio is never stored. The plain-HTTP port 8765 stays (Home Assistant's token door and a fallback; its page tells you where the secure address is). The nft rule for the speech servers now also admits this node's own tailnet address (the web page is a client of them). CSP gained media-src 'self' blob: data: and the Permissions-Policy microphone=(self).lenovo-laptop, my-personal-pc, ipad157 were added to /etc/jarvis/web_access.json (with iphone-14, personal-pc).hanna-properties-backend (LXC 123, stopped): its 64 GB root moved from local-lvm to boot-disks (Ceph RBD, 29 GB used, 1,206,982 files) with pct move-volume 123 rootfs boot-disks --delete 0, verified identical by rsync -c (no differences at all), then the old volume removed: local-lvm 93.8% -> 53.8% (with the Minecraft move earlier today). Storj: the log rotation for the 18 GB Docker log needs a change to the node's compose file; the harness refused me that edit as a change to a shared service, so it is prepared as /root/storj-log-rotation.sh on SuperPC for Cody to run (it changes only the logging: block, recreates only that container, and compares image/ports/mounts/environment keys before and after; the identity and /storage are bind mounts and untouched).mutate_actions.py and two of mutate_doors.py had pointed at code that moved in v0.9 and had silently stopped testing anything (the script said "anchor found 0x"); retargeted, so the claim is true again; two further entries were retargeted for v0.11 and the two obsolete mirror entries removed. (2) The first sabotage run reported everything "caught" because the test files were missing from the scratch copy: always read the failure text, not the count. (3) A strict-match test showed my policy reader quietly rewrote an untidy entry (ls\nid -> ls id); entries are now accepted as written or dropped. (4) Two sabotage entries slipped through (a split-line command that would collapse into a listed one; the start-before-announce race) and were closed with stronger tests. (5) The deploy gate cannot see a file I forgot to copy into staging: the HTTPS listener silently did not start after the first deploy; found by looking at the listening ports after the deploy; the deploy script should compare checksums of staged and working files (done by hand now). (6) The OpenProject suite replaces time.sleep with a no-op for the whole test run, which made a "slow" test double instant.mutate_actions.py 64, mutate_doors.py 93, mutate_v11.py 45 (allow-list, quiet web, warm-up, catalog), mutate_tailnet.py 41 (device list, Host rule, Ollama door, push-to-talk, read-aloud, HTTPS): 243 of 243 sabotaged copies caught. Live: the allow-listed command, the unlisted one asking, nothing in the ntfy topic, the menus' data, strict TLS validation, push-to-talk through the real secure page (a synthesised question heard word for word, answered, and spoken back in 2.7 s), the real warm-up call.actions.exec_allowlist and warmup; the web unit has WEB_TLS_PORT=8766 and WEB_HTTPS_URL; /etc/jarvis/tls/ (certificate and key, group jarvis-web); units jarvis-cert.service + .timer; the nft rule's source set; the three extra devices. Nothing changed on ntfy, Ceph, Proxmox permissions, Home Assistant or OpenProject. Deploy/rollback: ops/deploy_v11.sh / rollback_v11.sh (state before: /opt/jarvis/backup-v10).jarvis (100.111.230.69; tailnet tailbacb1d.ts.net; Tailscale 1.104.1 from the official apt repo). /dev/net/tun is passed through with the same two lines the other containers use (lxc.cgroup2.devices.allow: c 10:200 rwm, lxc.mount.entry: /dev/net dev/net none bind,create=dir; the old config is /root/130.conf.bak-20261013-tailscale on SuperPC). Cody did the login. Joined with --accept-dns=false (the container's resolver is untouched). Remember to disable key expiry for jarvis in the Tailscale admin console, or the node drops off the tailnet when the key expires (the page and voice then go quiet; Proxmox, ntfy and the chat are unaffected).jarvis_web.py v0.10 with WEB_AUTH=tailscale (set in the unit): it listens on the tailnet address only (the LAN port 8765 is closed; it refuses to start on any other address) and the connecting Tailscale device is the login. A request is accepted only if (1) it comes from a tailnet address, (2) tailscaled itself (LocalAPI whois, read over its unix socket by the unprivileged jarvis-web user) says the device belongs to an allowed login, and (3) the device's name is on the allowed list. (3) exists because the tailnet also holds service containers (ghost, nginx-proxy, arr-stack, n8n, mail ...): one of those being broken into must not be able to open Jarvis and approve a restart/run command. The lists are /etc/jarvis/web_access.json (logins, nodes, hosts), re-read every few seconds (add a device without a restart); a missing or broken file allows nobody. hosts are the names the page may be opened under; any other Host header gets 421 (DNS-rebinding defence: without it a hostile website could point its own name at the tailnet address and be "Cody" from the network's point of view). CSRF rules (custom header + JSON + Origin) are unchanged; /login and /logout do not exist in this mode. Allowed today: iphone-14, personal-pc under codyhanna8@gmail.com. The v0.9 password mode is still in the code (WEB_AUTH unset) and ops/rollback_v10.sh restores the v0.9 files.voice_token, never approves, auto read as ask) also answers GET /api/tags, GET /api/version and POST /api/chat in Ollama's dialect, so Home Assistant's own Ollama integration can use Jarvis as a conversation agent. Only the last user message of a chat request is used (Jarvis keeps its own memory; HA's prompt, history and tool list are ignored). Machine routes skip the device and Host rules (the token is the credential; ten wrong tokens lock an address for five minutes). /api/ask is unchanged; its body limit stays 8 KB, /api/chat accepts 256 KB (HA sends its whole conversation).jarvis-stt (faster-whisper small.en) and jarvis-tts (Piper en_GB-alan-medium) are enabled and listen on the tailnet address (/etc/jarvis/voice.env: WYOMING_BIND=100.111.230.69). The Wyoming protocol has no login, so a firewall rule (/etc/jarvis/jarvis-fw.nft, unit jarvis-fw, its own nft table inet jarvis_fw) lets only loopback and Home Assistant (tailnet 100.75.206.53) reach ports 10200/10300; verified: Home Assistant connects, the Proxmox host is dropped (the rule's counter shows the dropped packets).ops/ha_voice_setup.py (run inside the HA container; token and voice token on stdin, nothing printed): Wyoming integration twice (faster-whisper, piper at the tailnet address), the Ollama integration pointing at http://100.111.230.69:8765 with the voice token as API key and a conversation agent Jarvis (conversation.jarvis, model jarvis:latest), and the Assist pipeline Jarvis (stt.faster_whisper -> conversation.jarvis -> tts.piper, voice en_GB-alan-medium, prefer_local_intents on so Home Assistant's own devices stay with Home Assistant). It is the preferred pipeline; the previous one ("Home Assistant", id 01kxbaywcgkyqk3wxp8d273c82) is untouched. Ids: Wyoming 01M4VPVF26T4KHQPVSZB1PQQ6K and 01M4VPVF2RRTPNR2C7YQJPJ020, Ollama 01M4VPVF5255R6E777FP5RT140, pipeline 01m4vpvm251jvd71hzg2ra398q. Undo: ops/ha_voice_undo.py (dry run by default; --do-it removes exactly those and restores the preferred pipeline). Tested through HA's own pipeline (ops/ha_voice_test.py): text -> Jarvis -> MP3 5.2 s; a spoken question (synthesised with Piper, resampled to 16 kHz) -> Whisper 0.9 s, heard word for word -> Jarvis -> MP3 15.6 s end to end (multi-tool answer; a one-tool question is ~5 s); "turn on the kitchen light" is not matched locally (no such device yet) so it reaches Jarvis, which says it cannot control devices.openproject_token and jarvis_projects.py selftest passes all 10 steps against the real instance (create, add task, show, check off, mark finished, add and delete a task, delete the project; also settles the open question whether the finished project status exists: it does). The token belongs to the built-in admin user, not a dedicated jarvis user: changes show up in OpenProject as "OpenProject Admin" and Jarvis' reach is bounded only by its own code. Recommended: create the user jarvis and replace the token (steps in BUILD-GUIDE 13.1). The first selftest run timed out creating a project because the host was saturated by a volume copy (I/O pressure 70%); the create had succeeded, I deleted that leftover project (id 24) by hand. Consider raising the 15 s default timeout of _call for creates.LoadCredential=openproject_token (the v0.9 note "nothing to restart" was wrong; it could not be tested before a token existed). Added the line (copy: ops/jarvis-bridge.service.v10; the unit before this change is ops/jarvis-bridge.service.v08), restarted the bridge; /projects through the live bridge now lists the 15 projects. After changing a token: systemctl restart jarvis-bridge.Today is <weekday date>. and Gemma's sliding-window cache cannot reuse anything after a changed token (the three saved checkpoints all lie after the change), so the first message of each new day, and any message after the brain restarts, is a full prefill of the whole prompt (3.5k tokens = 28 s in a clean experiment; 6.5k tokens with a real history at ~103 tok/s = 66 s). Proved with the real prompt and tools against the live brain: date unchanged -> 99.6% reused, 1 s; only the date changed -> 0 tokens reused, 28 s. A keep-warm ping would NOT have helped. Options (not built, Cody's call): one silent warm-up request just after local midnight (about one prefill a day), or drop the date line from the prompt (the model has get_current_time, but weekday arithmetic for reminders would suffer)."nodes": "iphone-14") was being read as single characters (fixed, now ignored); a node that has a name but no login was rejected only downstream (now at the source, pinned by a test). systemd IPAddressAllow= is not enforced inside an unprivileged container (no cgroup BPF): the v0.9 web unit's LAN-only rule never did anything, the bind address was the real control; use a firewall (nft) inside the container. The unit keeps the lines but they are decoration.docker logs file, /var/lib/docker/containers/<id>/<id>-json.log, growing ~0.4 GB a day since 2026-08-29; the larger disk only buys ~12 days, the log needs rotation or truncation). local-lvm on SuperPC 93.8% -> 78.2%: the Minecraft world (KrustyKraft01, 13.8 GB in 9,515 files) mounted into the AMP container (LXC 124, stopped) moved from local-lvm to boot-disks (Ceph RBD) with pct move-volume 124 mp0 boot-disks --delete 0, verified identical by checksum (rsync -c dry run, only lost+found differed), then the old LV removed; mp0 is now boot-disks:vm-124-disk-1. No Ceph pool, rule or layout was changed.test_web_tailnet_offline.py 86: address rule, access list, whois against a fake tailscaled socket, who may use the page, Host rule, changing the list live, the Ollama endpoints, startup refusals, page files), mutate_tailnet.py 25 of 25 sabotaged copies caught (device list, login, tailnet address rule, tags, Host rule, CSRF, bearer check, lockout, last-vs-first message, voice vs web channel, body limits, LAN bind refusal, fail-closed file handling, caching). Live checks: allowed device auth true; a tailnet container not on the list auth false and 401; Host: evil.example 421; the old LAN address closed; Ollama chat through the real brain; Home Assistant's container reaches 10200/10300, the host does not.tun lines in its config), secret voice_token set (40 random characters, never printed), files /etc/jarvis/{web_access.json,voice.env,jarvis-fw.nft}, units jarvis-fw, drop-ins jarvis-stt|tts.service.d/10-tailnet.conf, jarvis-web.service v0.10, Home Assistant objects above. Nothing changed on ntfy, Ceph, Proxmox permissions or the bridge code.jarvis_web.py, service jarvis-web, http://192.168.86.226:8765, LAN address only). A separate unprivileged, sandboxed service with no Proxmox/HA/ntfy secrets; it talks to the bridge's loopback API (127.0.0.1:8091, key web_api_key). Login with a password Cody chooses (set-web-password: hidden prompt, scrypt hash only), random hashed session tokens, HttpOnly + SameSite=Strict cookie, 5 wrong passwords lock an address for 5 minutes (60 attempts/hour overall), CSRF header + Origin check on every change, strict CSP, a fixed list of files. The page shows the same conversation as ntfy (chatlog.jsonl, mode 0600), sends text, lists waiting approvals with Approve (second tap required) / Deny buttons that send exactly yes N / no N; add-to-home-screen on the iPhone. A web message is handled like an ntfy one, answered silently and mirrored into the ntfy topic; approvals record cody (web).POST /api/ask, bearer voice_token; does not exist while the token file is empty): a spoken request can never approve or cancel anything, policy auto is read as ask, approval requests go to the chat while the speaker hears "I've sent that to your chat for approval", replies are speakable text (<= ~420 characters; the rest is in the chat log). Speech services installed as units jarvis-stt (10300) and jarvis-tts (10200), disabled, loopback only. Loopback demo of the whole chain (voice/loopback_demo.py): about 4-5 s from the end of a spoken question to the first spoken word of the answer when the prompt cache is warm (speech-to-text 0.5 s, Jarvis 3.5-4.4 s, first audio 0.1 s); the first request after a restart is a cold read of about 20 s; one of three questions was misheard by Whisper.jarvis_projects.py; tools get_projects T0 and project_request T1; deletions are T2 approval rows executed by jarvis_actions): create a project, add a task, check a task off, mark a project finished, delete a project or task (after yes, after re-checking the name). Commands /projects /project /newproject /addtask /done /finish /rmproject /rmtask; plain English is parsed by code first; the model's first move for unrecognised wording is a required project_request call; reports are built from OpenProject's own answer and kept out of the model's memory; a claim of an unperformed change is replaced by an honest message. python3 -I jarvis_projects.py selftest walks the lifecycle against the real OpenProject once a token exists. Not yet run against the real OpenProject (no token): tested against a fake and with the live model.test_projects_offline.py 259, test_web_offline.py 118, the doors and OpenProject sections of test_actions_offline.py), mutate_doors.py (95 sabotages, all caught; plus the 64 of mutate_actions.py), tool_reliability9.py (93 live scenarios incl. 19 for OpenProject: the 74 older scenarios stayed at 100% with an empty and with a 20-message history after the prompt and tool changes; the 19 OpenProject scenarios are 19 of 19 at four runs each in both histories after the fixes the long-history runs forced), web_e2e9.py (live over the LAN: 50 of 50 checks over the real LAN service (login, approvals on a test container, the voice door, ports not reachable from the LAN, no secret in any log), everything restored afterwards).jarvis-web and jarvis-voice; secrets web_api_key, voice_token (empty), later web_password_hash and openproject_token; LAN port 8765. Nothing changed on Proxmox, ntfy, Home Assistant or Ceph.BUILD-GUIDE.md (a replication guide; wiki projects/jarvis-build-guide), EXEC-ALLOWLIST-PROPOSAL.md (read-only commands that could run without asking; not enabled, awaiting Cody's review), ROADMAP/ARCHITECTURE/OPERATIONS/VOICE refreshed.propose_action tool, plain-English requests ("restart jellyfin", "add crm to the backup job") and slash commands (/start /stop /kill /restart /addbackup /rmbackup /run) only record a proposal. Cody replies yes / no (or yes 7, yes all, no 7); only then does code execute it, once, after re-checking live state and policy. /pending lists what waits; requests expire after 15 min (commands 10).jarvis@pve!actions with a custom role JarvisOps (VM.PowerMgmt only). It has no access at all to guest 127 (ntfy) and 130 (Jarvis). The read-only token is unchanged./usr/local/sbin/jarvis-exec (5 verbs, validated arguments, output and time limits, never container 130, never the hosts, verifies its own job edits and restores on mismatch)./jobs (backup jobs and the guests in none), /pending and /tasks [failed|jarvis] (Proxmox's own task history: who did what and how it ended). /audit shows proposals, approvals and results (command output is never logged, only its size and hash).crm (101) and "the wiki container" resolve; a casual "ok" never approves a command.test_actions_offline.py (406 checks incl. the jarvis-exec script and the whole flow), a mutation test (every safety property sabotaged on purpose and caught), 23 new live scenarios, and bridge_e2e7.py (real ntfy, real Proxmox, real ssh on test guest 128).JarvisOps, token jarvis@pve!actions, ACLs on /vms (+ NoAccess on /vms/127, /vms/130), one authorized_keys line (backup /root/authorized_keys.bak-20261010-jarvis-exec), jarvis-exec + /etc/jarvis-exec/ on the three nodes.JarvisOps on /vms to the user made the read-only token blind to every guest (a more specific ACL replaces inherited roles). Fixed by also granting PVEAuditor on /vms.status/current).unavailable in Home Assistant now) and, under the long history, one of three "take storj out of the weekly backups" replies asking "Should I remove it?" instead of proposing (backup wording is now recognised as a change request)./opt/jarvis/voice and measured. See VOICE.md.docs/wiki/publish_wiki.sh, which asks for the wiki password at a hidden prompt; check with python3 wiki_push_v07.py --preview, it says unchanged when done): the stale NAS address (.100 is the Fios extender; the NAS is .103) corrected on the five pages that listed it, new page projects/jarvis (this log and the plan), refreshed Jarvis pages and lessons.Read-only Proxmox / Ceph / backups / storage / IPs / temperatures, Home Assistant (presence, battery, plugs, shopping list), wiki search, /problems roll-up, opt-in /watch, /mute, ~20 new commands, add_to_shopping_list (T1). LXC 130 added to the Sunday vzdump job (notebook excluded). Prompt-cache fixes (writes 22-40 s -> 3-5 s), empty-reply retry, write first-move guards. 669 offline checks, live sweeps all perfect, 97-check e2e.
LXC 130 created (the one approved new LXC). llama.cpp built; Gemma 4 26B-A4B chosen after a bake-off; 42-minute soak (553 requests, no errors). ntfy topic jarvis + user jarvis-bot; request limit raised. Bridge: policy gate, audit log, git-versioned notebook, weather, Kuma, cameras, calculator, reminders, daily briefing; claim verifier and guards added after measuring that the model invents times and claims saves it never made.