@loicngr/kobo 2.1.20 → 2.1.22

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (83) hide show
  1. package/AGENTS.md +37 -4
  2. package/CHANGELOG.md +9 -0
  3. package/CONFIGURATION.md +42 -5
  4. package/dist/server/services/agent/engines/claude-code/engine.js +5 -2
  5. package/dist/server/services/agent/engines/codex/engine.js +3 -0
  6. package/dist/server/services/agent/orchestrator.js +7 -0
  7. package/dist/server/services/agent/turn-liveness.js +1 -1
  8. package/dist/server/services/settings-service.js +30 -0
  9. package/docker-compose.vps.yml +9 -3
  10. package/docs/deployment-vps.md +153 -34
  11. package/docs/getting-started.md +16 -0
  12. package/package.json +1 -1
  13. package/scripts/kobo-install +35 -8
  14. package/scripts/kobo-update +37 -15
  15. package/src/client/dist/pwa/assets/{ActivityFeed-DiUxnmGJ.js → ActivityFeed-2wu6qXx1.js} +1 -1
  16. package/src/client/dist/pwa/assets/{AutoLoopFinalReviewSettings-f4VDR5dd.js → AutoLoopFinalReviewSettings-CwnNUWCh.js} +1 -1
  17. package/src/client/dist/pwa/assets/{ChangelogPage-dEWpjTlp.js → ChangelogPage-D9ke0TYz.js} +1 -1
  18. package/src/client/dist/pwa/assets/{ComparisonPanel-Bwbqgvr4.js → ComparisonPanel-ytDu1L7a.js} +1 -1
  19. package/src/client/dist/pwa/assets/{CreatePage-CCCCAhrx.js → CreatePage-YQ_jGilf.js} +1 -1
  20. package/src/client/dist/pwa/assets/{DashboardPage-3byhBITf.js → DashboardPage-C-PyArla.js} +1 -1
  21. package/src/client/dist/pwa/assets/{DiffViewer-Bj_NTfCF.js → DiffViewer-CPobwSwm.js} +3 -3
  22. package/src/client/dist/pwa/assets/{DocumentsPanel-xyLDV3T1.js → DocumentsPanel-BjZGNI-p.js} +1 -1
  23. package/src/client/dist/pwa/assets/{DrawerToggleButton-pwBSstQP.js → DrawerToggleButton-Dtd6fhYu.js} +1 -1
  24. package/src/client/dist/pwa/assets/{GitPanel-DvIdrkVw.js → GitPanel-BHJ3lpJZ.js} +3 -3
  25. package/src/client/dist/pwa/assets/{HealthPage-BSwhZQRo.js → HealthPage-Lv_IdF1l.js} +1 -1
  26. package/src/client/dist/pwa/assets/{MainLayout-DZJ9BvZw.js → MainLayout-DYumlWcP.js} +2 -2
  27. package/src/client/dist/pwa/assets/{MemoryPanel-ChR032zP.js → MemoryPanel-DS2F3x-S.js} +1 -1
  28. package/src/client/dist/pwa/assets/{MemoryProposalList-BRKSoS4K.js → MemoryProposalList-D_KDS3uX.js} +1 -1
  29. package/src/client/dist/pwa/assets/{QBtnDropdown-o3QH0Bb8.js → QBtnDropdown-XkBxmJO9.js} +1 -1
  30. package/src/client/dist/pwa/assets/{QExpansionItem-BhFHiyq9.js → QExpansionItem-B5vbHxJ2.js} +1 -1
  31. package/src/client/dist/pwa/assets/{QScrollArea-BsNY6slB.js → QScrollArea-BE_fAg20.js} +1 -1
  32. package/src/client/dist/pwa/assets/{QSelect-bEq3g_io.js → QSelect-BjZk4-r8.js} +1 -1
  33. package/src/client/dist/pwa/assets/{QTooltip-xYZ6KNu2.js → QTooltip-Dhn6VXWR.js} +1 -1
  34. package/src/client/dist/pwa/assets/{SchedulePanel-DIFv_FTY.js → SchedulePanel-BDhIt9pX.js} +1 -1
  35. package/src/client/dist/pwa/assets/{SearchPage-BqTBIt5E.js → SearchPage-CJrtIenZ.js} +1 -1
  36. package/src/client/dist/pwa/assets/SettingsPage-BSwN45zt.js +9 -0
  37. package/src/client/dist/pwa/assets/{SettingsPage-BRvUAZ5r.css → SettingsPage-CNIpxG-U.css} +1 -1
  38. package/src/client/dist/pwa/assets/{SplitWorkspacePage-D6r2pl5g.js → SplitWorkspacePage-BV62otQw.js} +1 -1
  39. package/src/client/dist/pwa/assets/{TerminalPanel-CladK9Vv.js → TerminalPanel-TQUHtZ-q.js} +1 -1
  40. package/src/client/dist/pwa/assets/{ToolCallItem-CdzEME4w.js → ToolCallItem-DxdYefuz.js} +1 -1
  41. package/src/client/dist/pwa/assets/{TouchPan-B4gQPwGN.js → TouchPan-CtwfZgkX.js} +1 -1
  42. package/src/client/dist/pwa/assets/{TourReplayButton-B0j6AZwU.js → TourReplayButton-DEC_Yt6E.js} +1 -1
  43. package/src/client/dist/pwa/assets/{WorkspaceAttentionLabels-CeVhoidC.js → WorkspaceAttentionLabels-BehGsWhI.js} +1 -1
  44. package/src/client/dist/pwa/assets/{WorkspacePage-w2bF8wTd.css → WorkspacePage-Bk-WWOYM.css} +1 -1
  45. package/src/client/dist/pwa/assets/{WorkspacePage-CK5XCuG_.js → WorkspacePage-CQz6QL5L.js} +3 -3
  46. package/src/client/dist/pwa/assets/{browser-B8tNScg1.js → browser-B3bGJv7H.js} +1 -1
  47. package/src/client/dist/pwa/assets/{build-path-tree-kmVAcvs_.js → build-path-tree-Bwwe_8vU.js} +1 -1
  48. package/src/client/dist/pwa/assets/{custom-sounds-CJwd-8TL.js → custom-sounds-1Rb1a5LA.js} +1 -1
  49. package/src/client/dist/pwa/assets/{de-BaO6kbdO.js → de-bfOiFUu1.js} +1 -1
  50. package/src/client/dist/pwa/assets/{editor.api-CgbPVW9s.js → editor.api-D2PTZoSF.js} +1 -1
  51. package/src/client/dist/pwa/assets/{efforts-y7Wf_Rgd.js → efforts-JkWc_mU5.js} +1 -1
  52. package/src/client/dist/pwa/assets/{es-DTvSsbD0.js → es-C5Sk8pec.js} +1 -1
  53. package/src/client/dist/pwa/assets/{expand-template-BFbbjj7X.js → expand-template-C-aGZz7k.js} +1 -1
  54. package/src/client/dist/pwa/assets/{format-BETfc4z9.js → format-BbXB3nKS.js} +1 -1
  55. package/src/client/dist/pwa/assets/{formatters-C22K9vlC.js → formatters-Bu3lC0Jk.js} +1 -1
  56. package/src/client/dist/pwa/assets/{fr-BH7V66Yz.js → fr-BNVJp67K.js} +1 -1
  57. package/src/client/dist/pwa/assets/{freemarker2-DiRVvp9O.js → freemarker2-qq6O820q.js} +1 -1
  58. package/src/client/dist/pwa/assets/{handlebars-BLYRroSQ.js → handlebars-nXATCa7t.js} +1 -1
  59. package/src/client/dist/pwa/assets/{html-DAW2vteY.js → html-ZSNyTQeY.js} +1 -1
  60. package/src/client/dist/pwa/assets/i18n-B_b02gtS.js +1 -0
  61. package/src/client/dist/pwa/assets/{index-CbyHfF8I.js → index-BZKHE14f.js} +3 -3
  62. package/src/client/dist/pwa/assets/{it-DZGbkkoy.js → it-B9g24VRZ.js} +1 -1
  63. package/src/client/dist/pwa/assets/{javascript-Bb5J8n0k.js → javascript-B3Elzrc0.js} +1 -1
  64. package/src/client/dist/pwa/assets/{liquid-Dqx3adnJ.js → liquid-CrMGh9mA.js} +1 -1
  65. package/src/client/dist/pwa/assets/{mdx-D_7igLV3.js → mdx-C6nxSGQ5.js} +1 -1
  66. package/src/client/dist/pwa/assets/{monaco-contributions-C-0CbrAy.js → monaco-contributions-WtuTHJJ8.js} +1 -1
  67. package/src/client/dist/pwa/assets/{monaco.contribution-c6JPBEZI.js → monaco.contribution--KeSH1BU.js} +2 -2
  68. package/src/client/dist/pwa/assets/{network-auth-BE4qbvWm.js → network-auth-DknPaiQS.js} +1 -1
  69. package/src/client/dist/pwa/assets/{notify-theme-BVFBBGrJ.js → notify-theme-DTaB0RsK.js} +1 -1
  70. package/src/client/dist/pwa/assets/{project-color-CHOR2jB5.js → project-color-bm_jpHka.js} +1 -1
  71. package/src/client/dist/pwa/assets/{python-Dqubw1F5.js → python-CQI_FwfK.js} +1 -1
  72. package/src/client/dist/pwa/assets/{razor-BQphc9Kt.js → razor-BT2wh4_7.js} +1 -1
  73. package/src/client/dist/pwa/assets/{typescript-Dep00l0X.js → typescript-D3ibItMf.js} +1 -1
  74. package/src/client/dist/pwa/assets/{use-search-index-status-CQHHvHwM.js → use-search-index-status-BnFR-_2l.js} +1 -1
  75. package/src/client/dist/pwa/assets/{use-tours-IVbXTmz2.js → use-tours-CsPhvrxf.js} +2 -2
  76. package/src/client/dist/pwa/assets/{use-worktree-restore-B3s5xDGR.js → use-worktree-restore-B5s0BuHk.js} +1 -1
  77. package/src/client/dist/pwa/assets/{workspace-templates-CyuTpztP.js → workspace-templates-GLO1mBlj.js} +1 -1
  78. package/src/client/dist/pwa/assets/{xml-BMS2dvaF.js → xml-EIEQlO3Y.js} +1 -1
  79. package/src/client/dist/pwa/assets/{yaml-CYw92aic.js → yaml-CJdHLr6G.js} +1 -1
  80. package/src/client/dist/pwa/index.html +1 -1
  81. package/src/client/dist/pwa/sw.js +1 -1
  82. package/src/client/dist/pwa/assets/SettingsPage-BLD0xWFB.js +0 -9
  83. package/src/client/dist/pwa/assets/i18n-DnuTmw3O.js +0 -1
@@ -132,11 +132,34 @@ docker build -t kobo:local source
132
132
 
133
133
  Save [`docker-compose.vps.yml`](../docker-compose.vps.yml) as `/opt/kobo/compose.yaml`
134
134
  and [`scripts/kobo-update`](../scripts/kobo-update) as `/opt/kobo/kobo-update`.
135
+ Also copy [`scripts/kobo-vps-entrypoint`](../scripts/kobo-vps-entrypoint) beside the updater as `/opt/kobo/kobo-vps-entrypoint`; the updater includes it in its compatibility image layer.
136
+
135
137
  These files may be newer than the example release above: obtain them from the
136
138
  checkout containing this guide, not an older tag that predates them.
137
139
 
138
140
  ```bash
139
- chmod 755 /opt/kobo/kobo-update
141
+ chmod 755 /opt/kobo/kobo-update /opt/kobo/kobo-vps-entrypoint
142
+ ```
143
+
144
+ For an older release image without the launcher, build this small compatibility
145
+ layer before starting the current Compose template. Its build context contains
146
+ only the launcher, never the deployment `.env`, projects or credentials:
147
+
148
+ ```bash
149
+ mkdir -p runtime
150
+ cp kobo-vps-entrypoint runtime/
151
+ docker tag kobo:local kobo:release-base
152
+ cat > runtime/Dockerfile <<'DOCKERFILE'
153
+ FROM kobo:release-base
154
+ USER root
155
+ RUN apt-get update && apt-get install -y --no-install-recommends \
156
+ util-linux netcat-openbsd docker-compose-plugin docker-buildx-plugin \
157
+ && rm -rf /var/lib/apt/lists/*
158
+ COPY --chmod=755 kobo-vps-entrypoint /usr/local/bin/kobo-vps-entrypoint
159
+ RUN chmod a+r /app/package.json /app/package-lock.json /app/CHANGELOG.md \
160
+ && chmod -R a+rX /app/dist /app/src/client/dist
161
+ DOCKERFILE
162
+ docker build -t kobo:local runtime
140
163
  ```
141
164
 
142
165
  Create `/opt/kobo/.env` with your deployment values:
@@ -147,8 +170,12 @@ KOBO_PROJECTS_DIR=/opt/kobo/projects
147
170
  TRAEFIK_NETWORK=web
148
171
  TRAEFIK_ENTRYPOINT=websecure
149
172
  TRAEFIK_CERTRESOLVER=letsencrypt
173
+ KOBO_UID=1000
174
+ KOBO_GID=1000
150
175
  ```
151
176
 
177
+ Replace `KOBO_UID` and `KOBO_GID` with `id -u` and `id -g` from the host user who owns your projects. Both must be nonzero. The guided installer collects them automatically; `--uid` and `--gid` override them.
178
+
152
179
  The network, entrypoint and resolver names must match **your existing Traefik**;
153
180
  `web`, `websecure` and `letsencrypt` are examples. If Traefik uses a preloaded
154
181
  certificate rather than an ACME resolver, remove the `tls.certresolver` label.
@@ -173,8 +200,7 @@ remembers the token; treat both that URL and startup logs as credentials.
173
200
  the public hostname. Both network access and behind-proxy mode are enabled.
174
201
 
175
202
  This example publishes no HTTP or SSH ports. Use SSH to the VPS followed by
176
- `docker compose exec kobo bash` for a container shell. The Compose entrypoint
177
- starts Node directly, so it does not start the image's optional SSH daemon.
203
+ `docker compose exec kobo kobo-vps-entrypoint --exec bash` for a container shell. The VPS launcher prepares the home permissions, then drops root before starting Node. It does not start the image's optional SSH daemon. `--exec` runs administrative shell/CLI commands as the same non-root user without repeating home migration.
178
204
 
179
205
  Check routing before starting a mission:
180
206
 
@@ -188,13 +214,85 @@ without a token. `/api/health` is intentionally public. Ordinary authenticated
188
214
  REST requests use `X-Kobo-Token`; the external MCP endpoint also supports Bearer
189
215
  authentication. No additional Traefik route is needed for WebSocket upgrades.
190
216
 
217
+ Behind-proxy mode also protects loopback requests. Kōbō supplies the current
218
+ network token to its internal workspace MCP process when launching Claude or
219
+ Codex, so memory, progress bars and other backend calls can authenticate without
220
+ manual MCP configuration. Memory and progress calls still require their separate
221
+ per-launch capabilities. After rotating the network token, stop and resume active
222
+ agent sessions so their MCP processes receive the new token.
223
+
191
224
  ## 3. Authenticate and choose a project
192
225
 
226
+ ### Runtime user and existing installations
227
+
228
+ The VPS Compose example runs Kōbō and its agents as `KOBO_UID:KOBO_GID`, not
229
+ root. Claude's bypass mode requires a non-root process; select that permission
230
+ mode explicitly in Kōbō. The launcher's brief root bootstrap creates a matching
231
+ user/group and assigns the dedicated home volume to them. Project directories
232
+ are never recursively chowned. They must already be writable by the chosen UID.
233
+ When the Docker socket is mounted, its numeric group is added to the agent's
234
+ supplementary groups. That access still grants control over the host daemon.
235
+
236
+ `HOME=/root` and `KOBO_HOME=/root/.config/kobo` deliberately remain unchanged:
237
+ the directory name does not mean the process runs as root. Keeping it avoids
238
+ losing existing sessions, logins, plugin paths and updater backups. Do not put
239
+ additional bind mounts below `/root` in this standalone layout.
240
+
241
+ Verify the effective identity, then use the same launcher for setup commands:
242
+
243
+ ```bash
244
+ docker compose exec kobo kobo-vps-entrypoint --exec id
245
+ docker compose exec kobo kobo-vps-entrypoint --exec bash
246
+ ```
247
+
248
+ A plain `docker compose exec kobo ...` still defaults to the container's root
249
+ bootstrap user. Installing skills or authenticating that way can create files
250
+ the agent cannot update. Use `kobo-vps-entrypoint --exec` throughout this guide;
251
+ reserve `--user 0` for deliberate OS maintenance. Do not add `user:` to the VPS
252
+ Compose file: that would skip the permission bootstrap of an existing root home.
253
+
254
+ For an **existing root deployment**, schedule an idle maintenance window and
255
+ back up the full home volume plus the Compose file first. Build an image with
256
+ the new launcher (the updated updater's `--prepare` does this), select your host
257
+ UID/GID in `.env`, and update the service settings below (merge the environment
258
+ with existing settings). `.env` alone does not pass variables into a container:
259
+
260
+ ```yaml
261
+ entrypoint: ["kobo-vps-entrypoint"]
262
+ command: ["node", "dist/server/index.js"]
263
+ environment:
264
+ KOBO_UID: ${KOBO_UID:-1000}
265
+ KOBO_GID: ${KOBO_GID:-1000}
266
+ HOME: /root
267
+ NODE_ENV: development
268
+ NPM_CONFIG_INCLUDE: dev
269
+ ```
270
+
271
+ Keep the same named home volume and project mount paths. Recreate the idle
272
+ service, verify `id`, authenticated API access and one agent launch. The initial
273
+ ownership scan takes longer for a large home. Read-only App keys/configuration
274
+ must also be readable by the selected UID; changing the home owner cannot repair
275
+ their host permissions.
276
+
277
+ The updater preserves existing Compose settings: updating an image alone does
278
+ **not** convert a legacy `entrypoint: ["node"]` deployment to non-root. The
279
+ installer does not migrate completed managed stacks or rewrite their recorded
280
+ fingerprints. Retain their manifest as history and manage an intentional Compose
281
+ migration with the updater/manual procedure; do not rerun the installer to adopt
282
+ the altered files. The other Compose examples retain their historical entrypoint.
283
+
284
+ The current VPS template needs an image containing `kobo-vps-entrypoint`. For a
285
+ fresh manual installation of an older release, build the compatibility layer in
286
+ step 1. For an existing installation, the updated updater's `--prepare` includes
287
+ it. Do not combine the new template with an unmodified old image.
288
+
289
+ ### Engine and Git login
290
+
193
291
  Authenticate **inside the container**, using the engine you intend to run:
194
292
 
195
293
  ```bash
196
- docker compose exec kobo claude auth login
197
- docker compose exec kobo codex login --device-auth
294
+ docker compose exec kobo kobo-vps-entrypoint --exec claude auth login
295
+ docker compose exec kobo kobo-vps-entrypoint --exec codex login --device-auth
198
296
  ```
199
297
 
200
298
  Follow the browser URL/code printed by your installed CLI. Provider availability
@@ -206,10 +304,10 @@ For a personal GitHub account, complete the CLI's browser login flow from your P
206
304
  then connect Git HTTPS credentials to it:
207
305
 
208
306
  ```bash
209
- docker compose exec kobo gh auth login --hostname github.com --git-protocol https --web
210
- docker compose exec kobo gh auth setup-git --hostname github.com
211
- docker compose exec kobo git config --global user.name "Your Name"
212
- docker compose exec kobo git config --global user.email "you@example.com"
307
+ docker compose exec kobo kobo-vps-entrypoint --exec gh auth login --hostname github.com --git-protocol https --web
308
+ docker compose exec kobo kobo-vps-entrypoint --exec gh auth setup-git --hostname github.com
309
+ docker compose exec kobo kobo-vps-entrypoint --exec git config --global user.name "Your Name"
310
+ docker compose exec kobo kobo-vps-entrypoint --exec git config --global user.email "you@example.com"
213
311
  ```
214
312
 
215
313
  Git commit identity and forge authentication are separate settings. For a GitHub
@@ -219,7 +317,7 @@ instead of an interactive account login.
219
317
  Clone your repository in the mounted project directory (replace the repository):
220
318
 
221
319
  ```bash
222
- docker compose exec kobo git clone https://github.com/YOUR-ORG/YOUR-REPO.git /opt/kobo/projects/my-project
320
+ docker compose exec kobo kobo-vps-entrypoint --exec git clone https://github.com/YOUR-ORG/YOUR-REPO.git /opt/kobo/projects/my-project
223
321
  ```
224
322
 
225
323
  In the first-run form, **Local project directory** means
@@ -233,9 +331,9 @@ not automatically available inside a container. See [Docker volume persistence](
233
331
 
234
332
  ## Optional project Docker environments
235
333
 
236
- The image includes the Docker CLI and Compose plugin. Verify with
237
- `docker compose exec kobo docker compose version`; an older release image may
238
- need the plugin installed in a derived image. The updater below adds it when missing.
334
+ The image includes the Docker CLI, Compose and Buildx plugins, and netcat (`nc`). Verify with
335
+ `docker compose exec kobo kobo-vps-entrypoint --exec docker compose version`; an older release image may
336
+ need these tools installed in a derived image. The updater below includes them in its compatibility layer.
239
337
 
240
338
  To let agents manage project containers, explicitly enable the commented
241
339
  `/var/run/docker.sock` mount in `compose.yaml` and recreate the service when idle.
@@ -253,6 +351,10 @@ An `npm` development process does not require the Docker socket. Development
253
351
  server URLs are separate from Kōbō's dashboard URL; configure their own reachable
254
352
  ports or proxy routes. See the [dev-server contract](../CONFIGURATION.md#dev-server).
255
353
 
354
+ For project dependencies, private registry login, SSH tunnels, stop/cleanup commands,
355
+ file-watcher troubleshooting and installing Claude/Codex extensions, follow
356
+ [Projects, development servers and skills on a VPS](./vps-projects-and-skills.md).
357
+
256
358
  ## GitHub App bot authentication
257
359
 
258
360
  A GitHub App installation token expires after one hour. Putting one token in
@@ -348,12 +450,12 @@ Recreate the idle service to apply the mounts, then configure Git inside it:
348
450
  ```bash
349
451
  docker compose config --quiet
350
452
  docker compose up -d --wait --wait-timeout 180 kobo
351
- docker compose exec kobo kobo-github-app-token --check
352
- docker compose exec kobo git config --global --replace-all credential.https://github.com.helper ''
353
- docker compose exec kobo git config --global --add credential.https://github.com.helper '!/usr/local/bin/gh auth git-credential'
354
- docker compose exec kobo bash -lc 'command -v gh'
355
- docker compose exec kobo gh repo view YOUR-ORG/YOUR-REPO --json nameWithOwner
356
- docker compose exec kobo git -C /opt/kobo/projects/my-project ls-remote origin HEAD
453
+ docker compose exec kobo kobo-vps-entrypoint --exec kobo-github-app-token --check
454
+ docker compose exec kobo kobo-vps-entrypoint --exec git config --global --replace-all credential.https://github.com.helper ''
455
+ docker compose exec kobo kobo-vps-entrypoint --exec git config --global --add credential.https://github.com.helper '!/usr/local/bin/gh auth git-credential'
456
+ docker compose exec kobo kobo-vps-entrypoint --exec bash -lc 'command -v gh'
457
+ docker compose exec kobo kobo-vps-entrypoint --exec gh repo view YOUR-ORG/YOUR-REPO --json nameWithOwner
458
+ docker compose exec kobo kobo-vps-entrypoint --exec git -C /opt/kobo/projects/my-project ls-remote origin HEAD
357
459
  ```
358
460
 
359
461
  The empty helper entry resets inherited helpers **only for this HTTPS host**.
@@ -373,7 +475,7 @@ renewals with a bounded lock, and invalidates cached tokens when the configurati
373
475
  or key changes. Failed renewal never falls back to an expired token. HTTPS calls
374
476
  use the fixed GitHub API endpoint and do not follow redirects.
375
477
 
376
- Use `docker compose exec kobo kobo-github-app-token --check` to validate signing,
478
+ Use `docker compose exec kobo kobo-vps-entrypoint --exec kobo-github-app-token --check` to validate signing,
377
479
  GitHub authentication and the cache without exposing a token. It prints a JSON
378
480
  status and expiration time. On failure, check file permissions, the key and IDs,
379
481
  the App installation's repository access, server clock and outbound HTTPS. Do not
@@ -385,8 +487,8 @@ commits. Configure commit identity separately, for example using your App's bot
385
487
  login and its actual GitHub bot-user email:
386
488
 
387
489
  ```bash
388
- docker compose exec kobo git config --global user.name 'YOUR-APP[bot]'
389
- docker compose exec kobo git config --global user.email 'BOT-USER-ID+YOUR-APP[bot]@users.noreply.github.com'
490
+ docker compose exec kobo kobo-vps-entrypoint --exec git config --global user.name 'YOUR-APP[bot]'
491
+ docker compose exec kobo kobo-vps-entrypoint --exec git config --global user.email 'BOT-USER-ID+YOUR-APP[bot]@users.noreply.github.com'
390
492
  ```
391
493
 
392
494
  `BOT-USER-ID` is the bot account's user ID, not the App ID or installation ID.
@@ -427,9 +529,11 @@ assuming a particular server. It uses the local Docker daemon; do not point it
427
529
  at a remote Docker context. It requires network access to npm, GitHub and image
428
530
  build dependencies. It does not update Traefik or install a scheduled auto-updater.
429
531
 
532
+ Keep `kobo-vps-entrypoint` beside `kobo-update` when updating the operator scripts (the guided installer places both in `bin/`). This is required even when building an older release.
533
+
430
534
  Both operator scripts explicitly read the deployment directory's `.env` for
431
535
  Compose. Exported `KOBO_DOMAIN`, `KOBO_PROJECTS_DIR`, `TRAEFIK_NETWORK`,
432
- `TRAEFIK_ENTRYPOINT`, `TRAEFIK_CERTRESOLVER` and `COMPOSE_*` variables in the caller's
536
+ `TRAEFIK_ENTRYPOINT`, `TRAEFIK_CERTRESOLVER`, `KOBO_UID`, `KOBO_GID` and `COMPOSE_*` variables in the caller's
433
537
  shell cannot override these commands. This applies to validation, start, stop and
434
538
  recovery. Docker context selection and forge authentication remain available to
435
539
  child processes; other commands such as image builds retain their environment.
@@ -457,11 +561,11 @@ original deadline and prompt stay unchanged. Already stale, never-attempted
457
561
  wakeups are not revived.
458
562
  It retrieves npm's `latest` version (or the explicit
459
563
  stable version), verifies the Git tag's commit and package version, builds a uniquely
460
- tagged image, ensures Compose is installed and checks native modules. The old
564
+ tagged image, ensures Compose, Buildx, netcat and the non-root launcher are installed and checks native modules. The old
461
565
  service stays available during the build. Plan for roughly 5–15 minutes without
462
- build cache; backup duration depends on the volume size.
566
+ build cache; backup duration depends only on the size of Kōbō's data directory.
463
567
 
464
- Once idle, it stops Kōbō, archives the complete `/root` volume, tests SQLite
568
+ Once idle, it stops Kōbō, archives only `/root/.config/kobo`, tests SQLite
465
569
  migrations/integrity on a copy, pins the new image directly in `compose.yaml`, and
466
570
  starts the service. It checks container health, the authenticated internal API,
467
571
  the version served through HTTPS, and public API rejection without a token. It
@@ -473,9 +577,12 @@ repository when you want to update the updater.
473
577
  ## Backups and failed updates
474
578
 
475
579
  Each update records its private log, `state.json`, previous Compose file,
476
- `home.tar.gz` and recovery instructions in `<deployment>/updates/<timestamp>-<id>/`.
477
- The archive contains credentials as well as application data; keep it private.
478
- A prepared-only build does not create a home backup. No old images, backups or
580
+ `kobo.tar.gz` and recovery instructions in `<deployment>/updates/<timestamp>-<id>/`.
581
+ The archive contains the whole `/root/.config/kobo` directory: SQLite database
582
+ and WAL/SHM sidecars when present, settings, templates, attachments and other Kōbō
583
+ data. It retains paths relative to `/root`. Settings can contain credentials;
584
+ keep the archive private. `state.json` records the backup scope and archive name.
585
+ A prepared-only build does not create a backup. No old images, backups or
479
586
  volumes are automatically removed; monitor disk space and retain off-host copies.
480
587
 
481
588
  An error before shutdown releases the reservation, including a lost acquisition
@@ -486,9 +593,12 @@ the reservation deliberately has no expiry that could reopen the race. Follow
486
593
  container and confirm its exit before restarting it. Restart clears the in-memory
487
594
  reservation. Never release it while a Docker stop could still be in progress.
488
595
 
489
- Repositories/worktrees stored outside `/root`, bind-mounted App keys/scripts and
490
- project database volumes are **not included** in `home.tar.gz`. Back them up
491
- separately along with `.env` and other deployment files. Never copy a live SQLite
596
+ Claude/Codex configuration and credentials, skills/plugins, caches, browser
597
+ downloads, repositories/worktrees, bind-mounted App keys/scripts and project
598
+ database volumes are **not included** in `kobo.tar.gz`. Files outside the Kōbō
599
+ directory remain in the existing volume; the updater neither archives nor deletes
600
+ them. Back up important files separately, along with `.env` and deployment files.
601
+ Never copy a live SQLite
492
602
  file as your only database backup; the updater stops Kōbō before archiving it.
493
603
 
494
604
  A build failure leaves the current service running. A backup or migration-test
@@ -496,8 +606,17 @@ failure restarts the previous version before new code touches production data.
496
606
  After a new-version launch, an error leaves data intact for diagnosis: an older
497
607
  image must not be blindly started against a migrated database, and a snapshot
498
608
  restore could discard messages or work resumed since launch. Read the generated
499
- `RECOVERY.md`; restore to a **new volume**, retaining the current one, if a full
500
- rollback is necessary. An interrupted update blocks subsequent updates until its
609
+ `RECOVERY.md`: for a rollback, stop Kōbō and clone the current volume into a **new
610
+ volume** so engine credentials and skills are retained. In the new volume only,
611
+ replace `.config/kobo` completely from `kobo.tar.gz`, preserving numeric ownership
612
+ and permissions, then use that volume with the previous image. Do not overlay an
613
+ old database onto newer WAL/SHM files. Keep the original volume. This restores
614
+ Kōbō data only, not engine state or project changes.
615
+
616
+ Older updater versions created `home.tar.gz` containing all of `/root`; use each
617
+ backup's own `RECOVERY.md`. Updating the Kōbō image does not update the operator
618
+ script: install the newer `scripts/kobo-update` to use the smaller backups.
619
+ An interrupted update blocks subsequent updates until its
501
620
  recorded state has been reviewed. Inspect logs locally; they may contain secrets.
502
621
 
503
622
  For troubleshooting, start with `docker compose ps`, the private update log and
@@ -12,6 +12,11 @@ No skill plugin, forge CLI, Docker, Notion, Sentry or voice software is needed f
12
12
 
13
13
  For a remote installation, follow the [VPS and Docker guide](./deployment-vps.md) first: `kobo-install` guides a fresh installation behind existing Traefik and can configure a GitHub App bot. The same guide documents manual setup. The project picker browses the server/container filesystem: select a repository cloned there, not a folder on the PC running your browser. Authenticate the agent and forge tools inside the container too.
14
14
 
15
+ The [VPS projects and skills guide](./vps-projects-and-skills.md) explains how to
16
+ prepare environment files/dependencies, open a remote dev server through SSH,
17
+ and install Claude/Codex skills as the non-root agent user. Host/PC installations
18
+ are not automatically inherited by container agents.
19
+
15
20
  ## Start
16
21
 
17
22
  ```bash
@@ -51,6 +56,17 @@ Configure integrations only when you need them, in their Settings tabs. Notion a
51
56
 
52
57
  ## Existing installations
53
58
 
59
+ The **Show progress bars** switch in **Settings → Agents** is enabled by default.
60
+ Turn it off and save to hide activity indicators without interrupting their work
61
+ or tracking. Re-enabling it preserves individual dismissals and expiry times.
62
+
63
+ The **agent inactivity watchdogs** switch is at the top of **Settings → Agents**.
64
+ It is off by default, including after upgrading an existing installation, so a
65
+ long silent task is not stopped by Kōbō's engine inactivity timers. Enable it if
66
+ you want stalled sessions closed automatically. Changes apply on the next agent
67
+ start or resume. The Settings tour explains this option; see
68
+ [session watchdogs](../CONFIGURATION.md#session-watchdogs) for the deadlines.
69
+
54
70
  Upgrades preserve customized prompts, plugin selection, integration toggles, permission modes and resource limits. Existing installations retain legacy automatic Git workflow preferences; review them in Settings if you prefer manual authorization. Existing workspaces receive a snapshot of that historical policy, while new workspaces snapshot the effective global/project/create policy. Changing a global policy does not rewrite existing missions.
55
71
 
56
72
  Known shipped prompt text and retired sound selections are migrated; custom prompt text is preserved. Audio enablement and volume are retained. Take a complete backup before an upgrade; see [Troubleshooting](./troubleshooting.md).
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@loicngr/kobo",
3
- "version": "2.1.20",
3
+ "version": "2.1.22",
4
4
  "description": "Kōbō — local multi-workspace orchestrator for Claude Code and OpenAI Codex, with isolated Git worktrees, live review and optional automation.",
5
5
  "type": "module",
6
6
  "license": "GPL-3.0-or-later",
@@ -20,7 +20,7 @@ spec = importlib.util.spec_from_loader(loader.name, loader)
20
20
  updater = importlib.util.module_from_spec(spec)
21
21
  loader.exec_module(updater)
22
22
  InstallError = updater.UpdateError
23
- TOOLS = ('kobo-install', 'kobo-update', 'kobo-github-app-token', 'kobo-gh')
23
+ TOOLS = ('kobo-install', 'kobo-update', 'kobo-github-app-token', 'kobo-gh', 'kobo-vps-entrypoint')
24
24
  ENV_KEYS = updater.COMPOSE_ENV_KEYS
25
25
 
26
26
 
@@ -32,6 +32,10 @@ def safe_path(value):
32
32
 
33
33
 
34
34
  def validate_config(config):
35
+ for name in ('uid', 'gid'):
36
+ value = str(config.get(name, 1000))
37
+ if not re.fullmatch(r'[1-9][0-9]{0,9}', value) or int(value) > 2147483647:
38
+ raise InstallError('UID and GID must be positive non-root numeric IDs.')
35
39
  domain = config['domain']
36
40
  if (len(domain) > 253 or '.' not in domain or
37
41
  not all(re.fullmatch(r'[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?', label)
@@ -49,6 +53,9 @@ def validate_config(config):
49
53
  raise InstallError('Projects must be outside container system paths, /app and /root (for example /opt/kobo/projects).')
50
54
  if projects.exists() and not projects.is_dir():
51
55
  raise InstallError('The projects path must be a directory.')
56
+ if not projects.exists() and 'uid' in config and os.getuid() not in (0, int(config['uid'])):
57
+ raise InstallError('A new projects directory requires --uid to match the installer user, or root. '
58
+ 'Otherwise prepare a directory writable by that UID first.')
52
59
  app = [config.get(k) for k in ('github_app_id', 'github_installation_id', 'github_private_key')]
53
60
  if any(app):
54
61
  if not all(app):
@@ -58,6 +65,10 @@ def validate_config(config):
58
65
  key = safe_path(app[2])
59
66
  if not key.is_file() or not os.access(key, os.R_OK) or key.stat().st_mode & 0o077:
60
67
  raise InstallError('The GitHub private key must be a readable regular file with private permissions (0600).')
68
+ if 'uid' in config and key.stat().st_uid != int(config['uid']):
69
+ raise InstallError('The GitHub private key owner must match --uid so the non-root agent can read it.')
70
+ if 'uid' in config and os.getuid() not in (0, int(config['uid'])):
71
+ raise InstallError('Run the installer as --uid (or root) to create readable private GitHub configuration.')
61
72
  if config['version'] != 'latest':
62
73
  updater.normalize_version(config['version'])
63
74
 
@@ -81,7 +92,7 @@ def render(config, stack):
81
92
  ' bind:\n create_host_path: false\n')
82
93
  text = text.replace(' networks:\n', mounts + ' networks:\n', 1)
83
94
  values = (config['domain'], config['projects_directory'], config['network'],
84
- config['entrypoint'], config['certresolver'])
95
+ config['entrypoint'], config['certresolver'], str(config.get('uid', 1000)), str(config.get('gid', 1000)))
85
96
  env = ''.join(f"{key}='{value}'\n" for key, value in zip(ENV_KEYS, values))
86
97
  return text, env
87
98
 
@@ -180,7 +191,13 @@ class Installer(updater.Updater):
180
191
  path = self.stack / name
181
192
  path.parent.mkdir(mode=0o700, parents=True, exist_ok=True)
182
193
  updater.atomic_write(path, data, 0o755 if name.startswith('bin/') else 0o600)
183
- Path(config['projects_directory']).mkdir(parents=True, exist_ok=True)
194
+ if name == 'auth/github-app.json' and os.getuid() == 0:
195
+ os.chown(path, int(config.get('uid', 1000)), int(config.get('gid', 1000)))
196
+ projects = Path(config['projects_directory'])
197
+ projects_existed = projects.exists()
198
+ projects.mkdir(parents=True, exist_ok=True)
199
+ if not projects_existed and os.getuid() == 0:
200
+ os.chown(projects, int(config.get('uid', 1000)), int(config.get('gid', 1000)))
184
201
  self.save(phase='configured')
185
202
 
186
203
  def verify_files(self):
@@ -198,16 +215,19 @@ class Installer(updater.Updater):
198
215
  # No files or directories are needed for --check. All substitutions are supplied explicitly.
199
216
  text, _ = render(config, self.stack)
200
217
  for key, value in zip(ENV_KEYS, (config['domain'], config['projects_directory'], config['network'],
201
- config['entrypoint'], config['certresolver'])):
218
+ config['entrypoint'], config['certresolver'],
219
+ str(config.get('uid', 1000)), str(config.get('gid', 1000)))):
202
220
  text = re.sub(r'\$\{' + key + r'(?::[^}]*)?\}', lambda _: value, text)
203
221
  self.run(['docker', 'compose', '--env-file', '/dev/null', '-p', 'kobo', '-f', '-', 'config', '--quiet'],
204
222
  data=text.encode())
205
223
 
206
224
  def configure_github(self):
207
- self.run(self.compose('exec', '-T', 'kobo', '/usr/local/bin/kobo-github-app-token', '--check'))
225
+ # Legacy prepared installations retain their original root execution.
226
+ runtime = ['kobo-vps-entrypoint', '--exec'] if 'uid' in self.state.get('config', {}) else []
227
+ self.run(self.compose('exec', '-T', 'kobo', *runtime, '/usr/local/bin/kobo-github-app-token', '--check'))
208
228
  helper = 'credential.https://github.com.helper'
209
- self.run(self.compose('exec', '-T', 'kobo', 'git', 'config', '--global', '--replace-all', helper, ''))
210
- self.run(self.compose('exec', '-T', 'kobo', 'git', 'config', '--global', '--add', helper,
229
+ self.run(self.compose('exec', '-T', 'kobo', *runtime, 'git', 'config', '--global', '--replace-all', helper, ''))
230
+ self.run(self.compose('exec', '-T', 'kobo', *runtime, 'git', 'config', '--global', '--add', helper,
211
231
  '!/usr/local/bin/gh auth git-credential'))
212
232
 
213
233
  def assert_started(self):
@@ -297,6 +317,8 @@ class Installer(updater.Updater):
297
317
  def arguments(argv=None):
298
318
  parser = argparse.ArgumentParser(description=__doc__)
299
319
  parser.add_argument('--directory', type=Path, required=True)
320
+ parser.add_argument('--uid', help='Non-root runtime UID (defaults to the invoking user).')
321
+ parser.add_argument('--gid', help='Runtime GID (defaults to the invoking user).')
300
322
  for name in ('domain', 'projects-directory', 'network', 'entrypoint', 'certresolver', 'version',
301
323
  'github-app-id', 'github-installation-id', 'github-private-key'):
302
324
  parser.add_argument('--' + name)
@@ -311,7 +333,7 @@ def arguments(argv=None):
311
333
 
312
334
  def configure(args, installer):
313
335
  names = ('domain', 'projects_directory', 'network', 'entrypoint', 'certresolver', 'version',
314
- 'github_app_id', 'github_installation_id', 'github_private_key', 'docker_socket')
336
+ 'github_app_id', 'github_installation_id', 'github_private_key', 'docker_socket', 'uid', 'gid')
315
337
  config = installer.state.get('config', {}).copy()
316
338
  config.update({name: getattr(args, name) for name in names if getattr(args, name) is not None})
317
339
  interactive = sys.stdin.isatty() and not args.yes
@@ -339,6 +361,10 @@ def configure(args, installer):
339
361
  ask('entrypoint', 'Traefik HTTPS entrypoint')
340
362
  ask('certresolver', 'Traefik certificate resolver (empty for preloaded certificate)', '')
341
363
  ask('version', 'Stable release version', 'latest')
364
+ # Do not silently change an already managed deployment's recorded config.
365
+ if not installer.state:
366
+ ask('uid', 'Host user ID for Kōbō and agents', str(os.getuid() or 1000))
367
+ ask('gid', 'Host group ID for Kōbō and agents', str(os.getgid() or 1000))
342
368
  if config.get('docker_socket') is None:
343
369
  config['docker_socket'] = interactive and input('Give agents Docker access (root-equivalent host control)? [y/N]: ').lower() == 'y'
344
370
  if not any(config.get(name) for name in ('github_app_id', 'github_installation_id', 'github_private_key')):
@@ -365,6 +391,7 @@ def main(argv=None):
365
391
  return 0
366
392
  print(f'Install directory: {installer.stack}\nURL: https://{config["domain"]}\n'
367
393
  f'Projects: {config["projects_directory"]}\nDocker socket: {config["docker_socket"]}\n'
394
+ f'Runtime UID:GID: {config.get("uid", "legacy")}:{config.get("gid", "legacy")}\n'
368
395
  f'GitHub App: {bool(config["github_app_id"])}')
369
396
  if not args.yes and not installer.state:
370
397
  if not sys.stdin.isatty() or input('Create this installation? [y/N]: ').lower() != 'y':
@@ -21,7 +21,7 @@ from datetime import datetime, timezone
21
21
  REPO = 'https://github.com/loicngr/kobo.git'
22
22
  VERSION = re.compile(r'\d+\.\d+\.\d+')
23
23
  COMPOSE_ENV_KEYS = ('KOBO_DOMAIN', 'KOBO_PROJECTS_DIR', 'TRAEFIK_NETWORK',
24
- 'TRAEFIK_ENTRYPOINT', 'TRAEFIK_CERTRESOLVER')
24
+ 'TRAEFIK_ENTRYPOINT', 'TRAEFIK_CERTRESOLVER', 'KOBO_UID', 'KOBO_GID')
25
25
  BUSY_JS = r"""
26
26
  const db = new (require('better-sqlite3'))('/root/.config/kobo/kobo.db', {readonly:true});
27
27
  const sessions = db.prepare("SELECT count(*) n FROM agent_sessions WHERE status = 'running'").get().n;
@@ -217,7 +217,7 @@ class Updater:
217
217
  'node', '-p', 'require("./package.json").version']))
218
218
  self.idle()
219
219
  self.run(['docker', 'compose', 'version'])
220
- used_kib = int(self.run(['docker', 'exec', self.container, 'du', '-sk', '/root']).split()[0])
220
+ used_kib = int(self.run(['docker', 'exec', self.container, 'du', '-sk', '/root/.config/kobo']).split()[0])
221
221
  # Sources/build layers plus archive + migration copy; compression is not assumed.
222
222
  required = 6 * 1024**3 + used_kib * 1024 * 2
223
223
  docker_root = self.run(['docker', 'info', '--format', '{{.DockerRootDir}}'])
@@ -262,24 +262,38 @@ class Updater:
262
262
  self.logged(['docker', 'build', '--progress=plain', '-t', base, str(source)])
263
263
  overlay = self.job / 'overlay'
264
264
  overlay.mkdir()
265
+ launcher = Path(__file__).resolve().with_name('kobo-vps-entrypoint')
266
+ if not launcher.is_file():
267
+ raise UpdateError('Install kobo-vps-entrypoint beside kobo-update before building an image.')
268
+ shutil.copyfile(launcher, overlay / 'kobo-vps-entrypoint')
265
269
  (overlay / 'Dockerfile').write_text(
266
- f'FROM {base}\nRUN docker compose version >/dev/null 2>&1 || '
267
- '(apt-get update && apt-get install -y --no-install-recommends docker-compose-plugin '
268
- '&& rm -rf /var/lib/apt/lists/*)\n')
270
+ f'FROM {base}\nUSER root\n'
271
+ 'RUN apt-get update && apt-get install -y --no-install-recommends '
272
+ 'docker-compose-plugin docker-buildx-plugin netcat-openbsd util-linux '
273
+ '&& rm -rf /var/lib/apt/lists/*\n'
274
+ 'COPY --chmod=755 kobo-vps-entrypoint /usr/local/bin/kobo-vps-entrypoint\n'
275
+ 'RUN chmod a+r /app/package.json /app/package-lock.json /app/CHANGELOG.md '
276
+ '&& chmod -R a+rX /app/dist /app/src/client/dist\n')
269
277
  self.logged(['docker', 'build', '--progress=plain', '-t', image, str(overlay)])
270
278
  self.run(['docker', 'run', '--rm', '--network', 'none', '--entrypoint', 'docker',
271
279
  image, 'compose', 'version'])
272
280
  self.run(['docker', 'run', '--rm', '--network', 'none', '--entrypoint', 'node', image,
273
281
  '-e', "require('better-sqlite3')(':memory:').close();require('node-pty')"])
282
+ self.run(['docker', 'run', '--rm', '--network', 'none', '--entrypoint', 'kobo-vps-entrypoint',
283
+ image, 'node', '-e', "if(process.getuid()===0)process.exit(1);"
284
+ "const fs=require('fs');const dir='/app/src/client/dist/';"
285
+ "const mode=['pwa','spa'].find(m=>fs.existsSync(dir+m+'/index.html'));"
286
+ "if(!mode)throw Error('Missing built UI');fs.accessSync(dir+mode+'/index.html',fs.constants.R_OK);"
287
+ "require('better-sqlite3')(':memory:').close();require('node-pty')"])
274
288
  return image
275
289
 
276
290
  def backup(self):
277
- archive = self.job / 'home.tar.gz'
291
+ archive = self.job / 'kobo.tar.gz'
278
292
  with archive.open('xb') as f, self.log.open('ab') as err:
279
293
  try:
280
294
  subprocess.run(['docker', 'run', '--rm', '--network', 'none', '--read-only',
281
295
  '--mount', f'type=volume,src={self.volume},dst=/data,readonly',
282
- '--entrypoint', 'tar', self.old_image, '-C', '/data', '-czf', '-', '.'],
296
+ '--entrypoint', 'tar', self.old_image, '-C', '/data', '-czf', '-', './.config/kobo'],
283
297
  stdout=f, stderr=err, check=True, timeout=3600)
284
298
  f.flush()
285
299
  os.fsync(f.fileno())
@@ -346,6 +360,7 @@ class Updater:
346
360
  self.record('building', previous_image=old_tag, previous_digest=self.old_image,
347
361
  volume=self.volume, version=version, sha=sha,
348
362
  maintenance_owner=self.maintenance_owner,
363
+ backup_archive='kobo.tar.gz', backup_scope='/root/.config/kobo',
349
364
  backup_complete=False, new_version_start_attempted=False)
350
365
  self.recovery_notes()
351
366
  stopped = False
@@ -372,7 +387,7 @@ class Updater:
372
387
  self.maintenance('POST')
373
388
  self.record('stopping')
374
389
  stopped = True # Also recover if the stop command itself fails/interruption arrives.
375
- say('Stopping Kōbō, backing up /root and testing migrations on a database copy…')
390
+ say('Stopping Kōbō, backing up only /root/.config/kobo and testing migrations on a database copy…')
376
391
  self.run(self.compose('stop', '-t', '60', 'kobo'))
377
392
  info = json.loads(self.run(['docker', 'inspect', self.container]))[0]
378
393
  if info['State']['Running']:
@@ -390,7 +405,7 @@ class Updater:
390
405
  self.logged(self.compose('up', '-d', '--no-deps', '--wait', '--wait-timeout', '180', 'kobo'), timeout=240)
391
406
  self.verify(version)
392
407
  self.record('complete')
393
- say(f'Kōbō {version} is up to date. Backup: {self.job / "home.tar.gz"}')
408
+ say(f'Kōbō {version} is up to date. Backup: {self.job / "kobo.tar.gz"}')
394
409
  except BaseException:
395
410
  if stopped and not launching:
396
411
  # Only safe before any process from the new version has used production data.
@@ -432,9 +447,11 @@ Private log: `{self.job / 'update.log'}`. Durable phase: `state.json`.
432
447
  Previous configuration: `compose.before.yaml`.
433
448
  Previous immutable image: `{self.old_image}`.
434
449
  Original volume: `{self.volume}`.
435
- The archive `home.tar.gz` is validated only when backup_complete=true.
450
+ The archive `kobo.tar.gz` is validated only when backup_complete=true.
451
+ It contains ONLY /root/.config/kobo, with paths relative to /root.
436
452
  new_version_start_attempted indicates whether a new-version launch was attempted.
437
- Projects outside /root are NOT included in this archive; back them up separately.
453
+ Claude/Codex credentials, skills/plugins, caches and projects are NOT included;
454
+ back up those files separately if needed. They remain in the original volume.
438
455
  Old images, volumes and backups are never automatically deleted.
439
456
 
440
457
  ## Interrupted admission reservation
@@ -461,10 +478,15 @@ Do not run an older image against a potentially migrated database.
461
478
  Inspect the private log and Compose logs (which may contain credentials).
462
479
  A proxy error can leave a working backend: check for new messages and resumed
463
480
  work before considering a restore.
464
- To roll back fully, stop Kōbō, back up the current volume too, extract home.tar.gz
465
- into a NEW empty Docker volume and adapt the previous Compose configuration to
466
- use that restored volume and the previous immutable image above. Keep the current
467
- volume. Database rollback does not undo external agent or project changes.
481
+ To roll back Kōbō data, stop Kōbō and preserve the current volume. Clone that
482
+ stopped volume into a NEW Docker volume to retain current engine credentials,
483
+ skills and other files outside .config/kobo. In the NEW volume only, replace
484
+ .config/kobo completely with the contents of kobo.tar.gz (extract at the volume
485
+ root, preserving numeric owners and permissions). Do not merge the old database
486
+ with newer WAL/SHM files or other new-version Kōbō files. Adapt the previous
487
+ Compose configuration to use that restored volume and the previous immutable
488
+ image above. Keep the original volume. This archive cannot roll back files
489
+ outside .config/kobo, external agent actions or project changes.
468
490
  After recovery and health verification, set state.json phase to complete or aborted.
469
491
  ''')
470
492