@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.
- package/AGENTS.md +37 -4
- package/CHANGELOG.md +9 -0
- package/CONFIGURATION.md +42 -5
- package/dist/server/services/agent/engines/claude-code/engine.js +5 -2
- package/dist/server/services/agent/engines/codex/engine.js +3 -0
- package/dist/server/services/agent/orchestrator.js +7 -0
- package/dist/server/services/agent/turn-liveness.js +1 -1
- package/dist/server/services/settings-service.js +30 -0
- package/docker-compose.vps.yml +9 -3
- package/docs/deployment-vps.md +153 -34
- package/docs/getting-started.md +16 -0
- package/package.json +1 -1
- package/scripts/kobo-install +35 -8
- package/scripts/kobo-update +37 -15
- package/src/client/dist/pwa/assets/{ActivityFeed-DiUxnmGJ.js → ActivityFeed-2wu6qXx1.js} +1 -1
- package/src/client/dist/pwa/assets/{AutoLoopFinalReviewSettings-f4VDR5dd.js → AutoLoopFinalReviewSettings-CwnNUWCh.js} +1 -1
- package/src/client/dist/pwa/assets/{ChangelogPage-dEWpjTlp.js → ChangelogPage-D9ke0TYz.js} +1 -1
- package/src/client/dist/pwa/assets/{ComparisonPanel-Bwbqgvr4.js → ComparisonPanel-ytDu1L7a.js} +1 -1
- package/src/client/dist/pwa/assets/{CreatePage-CCCCAhrx.js → CreatePage-YQ_jGilf.js} +1 -1
- package/src/client/dist/pwa/assets/{DashboardPage-3byhBITf.js → DashboardPage-C-PyArla.js} +1 -1
- package/src/client/dist/pwa/assets/{DiffViewer-Bj_NTfCF.js → DiffViewer-CPobwSwm.js} +3 -3
- package/src/client/dist/pwa/assets/{DocumentsPanel-xyLDV3T1.js → DocumentsPanel-BjZGNI-p.js} +1 -1
- package/src/client/dist/pwa/assets/{DrawerToggleButton-pwBSstQP.js → DrawerToggleButton-Dtd6fhYu.js} +1 -1
- package/src/client/dist/pwa/assets/{GitPanel-DvIdrkVw.js → GitPanel-BHJ3lpJZ.js} +3 -3
- package/src/client/dist/pwa/assets/{HealthPage-BSwhZQRo.js → HealthPage-Lv_IdF1l.js} +1 -1
- package/src/client/dist/pwa/assets/{MainLayout-DZJ9BvZw.js → MainLayout-DYumlWcP.js} +2 -2
- package/src/client/dist/pwa/assets/{MemoryPanel-ChR032zP.js → MemoryPanel-DS2F3x-S.js} +1 -1
- package/src/client/dist/pwa/assets/{MemoryProposalList-BRKSoS4K.js → MemoryProposalList-D_KDS3uX.js} +1 -1
- package/src/client/dist/pwa/assets/{QBtnDropdown-o3QH0Bb8.js → QBtnDropdown-XkBxmJO9.js} +1 -1
- package/src/client/dist/pwa/assets/{QExpansionItem-BhFHiyq9.js → QExpansionItem-B5vbHxJ2.js} +1 -1
- package/src/client/dist/pwa/assets/{QScrollArea-BsNY6slB.js → QScrollArea-BE_fAg20.js} +1 -1
- package/src/client/dist/pwa/assets/{QSelect-bEq3g_io.js → QSelect-BjZk4-r8.js} +1 -1
- package/src/client/dist/pwa/assets/{QTooltip-xYZ6KNu2.js → QTooltip-Dhn6VXWR.js} +1 -1
- package/src/client/dist/pwa/assets/{SchedulePanel-DIFv_FTY.js → SchedulePanel-BDhIt9pX.js} +1 -1
- package/src/client/dist/pwa/assets/{SearchPage-BqTBIt5E.js → SearchPage-CJrtIenZ.js} +1 -1
- package/src/client/dist/pwa/assets/SettingsPage-BSwN45zt.js +9 -0
- package/src/client/dist/pwa/assets/{SettingsPage-BRvUAZ5r.css → SettingsPage-CNIpxG-U.css} +1 -1
- package/src/client/dist/pwa/assets/{SplitWorkspacePage-D6r2pl5g.js → SplitWorkspacePage-BV62otQw.js} +1 -1
- package/src/client/dist/pwa/assets/{TerminalPanel-CladK9Vv.js → TerminalPanel-TQUHtZ-q.js} +1 -1
- package/src/client/dist/pwa/assets/{ToolCallItem-CdzEME4w.js → ToolCallItem-DxdYefuz.js} +1 -1
- package/src/client/dist/pwa/assets/{TouchPan-B4gQPwGN.js → TouchPan-CtwfZgkX.js} +1 -1
- package/src/client/dist/pwa/assets/{TourReplayButton-B0j6AZwU.js → TourReplayButton-DEC_Yt6E.js} +1 -1
- package/src/client/dist/pwa/assets/{WorkspaceAttentionLabels-CeVhoidC.js → WorkspaceAttentionLabels-BehGsWhI.js} +1 -1
- package/src/client/dist/pwa/assets/{WorkspacePage-w2bF8wTd.css → WorkspacePage-Bk-WWOYM.css} +1 -1
- package/src/client/dist/pwa/assets/{WorkspacePage-CK5XCuG_.js → WorkspacePage-CQz6QL5L.js} +3 -3
- package/src/client/dist/pwa/assets/{browser-B8tNScg1.js → browser-B3bGJv7H.js} +1 -1
- package/src/client/dist/pwa/assets/{build-path-tree-kmVAcvs_.js → build-path-tree-Bwwe_8vU.js} +1 -1
- package/src/client/dist/pwa/assets/{custom-sounds-CJwd-8TL.js → custom-sounds-1Rb1a5LA.js} +1 -1
- package/src/client/dist/pwa/assets/{de-BaO6kbdO.js → de-bfOiFUu1.js} +1 -1
- package/src/client/dist/pwa/assets/{editor.api-CgbPVW9s.js → editor.api-D2PTZoSF.js} +1 -1
- package/src/client/dist/pwa/assets/{efforts-y7Wf_Rgd.js → efforts-JkWc_mU5.js} +1 -1
- package/src/client/dist/pwa/assets/{es-DTvSsbD0.js → es-C5Sk8pec.js} +1 -1
- package/src/client/dist/pwa/assets/{expand-template-BFbbjj7X.js → expand-template-C-aGZz7k.js} +1 -1
- package/src/client/dist/pwa/assets/{format-BETfc4z9.js → format-BbXB3nKS.js} +1 -1
- package/src/client/dist/pwa/assets/{formatters-C22K9vlC.js → formatters-Bu3lC0Jk.js} +1 -1
- package/src/client/dist/pwa/assets/{fr-BH7V66Yz.js → fr-BNVJp67K.js} +1 -1
- package/src/client/dist/pwa/assets/{freemarker2-DiRVvp9O.js → freemarker2-qq6O820q.js} +1 -1
- package/src/client/dist/pwa/assets/{handlebars-BLYRroSQ.js → handlebars-nXATCa7t.js} +1 -1
- package/src/client/dist/pwa/assets/{html-DAW2vteY.js → html-ZSNyTQeY.js} +1 -1
- package/src/client/dist/pwa/assets/i18n-B_b02gtS.js +1 -0
- package/src/client/dist/pwa/assets/{index-CbyHfF8I.js → index-BZKHE14f.js} +3 -3
- package/src/client/dist/pwa/assets/{it-DZGbkkoy.js → it-B9g24VRZ.js} +1 -1
- package/src/client/dist/pwa/assets/{javascript-Bb5J8n0k.js → javascript-B3Elzrc0.js} +1 -1
- package/src/client/dist/pwa/assets/{liquid-Dqx3adnJ.js → liquid-CrMGh9mA.js} +1 -1
- package/src/client/dist/pwa/assets/{mdx-D_7igLV3.js → mdx-C6nxSGQ5.js} +1 -1
- package/src/client/dist/pwa/assets/{monaco-contributions-C-0CbrAy.js → monaco-contributions-WtuTHJJ8.js} +1 -1
- package/src/client/dist/pwa/assets/{monaco.contribution-c6JPBEZI.js → monaco.contribution--KeSH1BU.js} +2 -2
- package/src/client/dist/pwa/assets/{network-auth-BE4qbvWm.js → network-auth-DknPaiQS.js} +1 -1
- package/src/client/dist/pwa/assets/{notify-theme-BVFBBGrJ.js → notify-theme-DTaB0RsK.js} +1 -1
- package/src/client/dist/pwa/assets/{project-color-CHOR2jB5.js → project-color-bm_jpHka.js} +1 -1
- package/src/client/dist/pwa/assets/{python-Dqubw1F5.js → python-CQI_FwfK.js} +1 -1
- package/src/client/dist/pwa/assets/{razor-BQphc9Kt.js → razor-BT2wh4_7.js} +1 -1
- package/src/client/dist/pwa/assets/{typescript-Dep00l0X.js → typescript-D3ibItMf.js} +1 -1
- package/src/client/dist/pwa/assets/{use-search-index-status-CQHHvHwM.js → use-search-index-status-BnFR-_2l.js} +1 -1
- package/src/client/dist/pwa/assets/{use-tours-IVbXTmz2.js → use-tours-CsPhvrxf.js} +2 -2
- package/src/client/dist/pwa/assets/{use-worktree-restore-B3s5xDGR.js → use-worktree-restore-B5s0BuHk.js} +1 -1
- package/src/client/dist/pwa/assets/{workspace-templates-CyuTpztP.js → workspace-templates-GLO1mBlj.js} +1 -1
- package/src/client/dist/pwa/assets/{xml-BMS2dvaF.js → xml-EIEQlO3Y.js} +1 -1
- package/src/client/dist/pwa/assets/{yaml-CYw92aic.js → yaml-CJdHLr6G.js} +1 -1
- package/src/client/dist/pwa/index.html +1 -1
- package/src/client/dist/pwa/sw.js +1 -1
- package/src/client/dist/pwa/assets/SettingsPage-BLD0xWFB.js +0 -9
- package/src/client/dist/pwa/assets/i18n-DnuTmw3O.js +0 -1
package/docs/deployment-vps.md
CHANGED
|
@@ -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
|
|
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
|
|
237
|
-
`docker compose exec kobo docker compose version`; an older release image may
|
|
238
|
-
need
|
|
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
|
|
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
|
|
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
|
|
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
|
-
`
|
|
477
|
-
The archive contains
|
|
478
|
-
|
|
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
|
-
|
|
490
|
-
|
|
491
|
-
|
|
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
|
|
500
|
-
|
|
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
|
package/docs/getting-started.md
CHANGED
|
@@ -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.
|
|
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",
|
package/scripts/kobo-install
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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':
|
package/scripts/kobo-update
CHANGED
|
@@ -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}\
|
|
267
|
-
'
|
|
268
|
-
'
|
|
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 / '
|
|
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 / "
|
|
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 `
|
|
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
|
-
|
|
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
|
|
465
|
-
into a NEW
|
|
466
|
-
|
|
467
|
-
|
|
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
|
|