@loicngr/kobo 2.1.20 → 2.1.21

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 CHANGED
@@ -282,6 +282,21 @@ Traefik network. It persists `/root`, mounts project paths identically on host a
282
282
  container, and leaves the Docker socket opt-in. `docs/deployment-vps.md` owns the
283
283
  operator walkthrough; `CONFIGURATION.md` compares all Compose topologies.
284
284
 
285
+ The VPS launcher `scripts/kobo-vps-entrypoint` bootstraps the dedicated home and
286
+ executes Kōbō as positive `KOBO_UID:KOBO_GID`, adding the socket's numeric group
287
+ when mounted. Keep `HOME=/root` and the existing volume for compatibility. Do not
288
+ chown existing project mounts, follow home symlinks, or resolve root bootstrap
289
+ commands through user-writable skill/tool paths. `--exec` runs operator commands
290
+ as the agent without rescanning ownership. Legacy root Compose entrypoints are
291
+ preserved by the updater and need the documented explicit migration. New installer
292
+ identity fields must not silently reconfigure old completed manifests.
293
+
294
+ The updater needs the launcher beside its script to build its compatibility layer
295
+ (including netcat/Buildx). `docs/vps-projects-and-skills.md` covers dependency/env
296
+ setup, private registry auth, SSH dev-server access, scoped cleanup, native
297
+ Claude/Codex skills and persistence. Project-specific OS packages and host inotify
298
+ changes are operator-managed; the updater does not preserve arbitrary image layers.
299
+
285
300
  `scripts/kobo-install` guides a fresh installation; it does not adopt existing
286
301
  unmanaged stacks. Preserve its read-only `--check`, explicit Docker-socket opt-in,
287
302
  managed-file fingerprints, shared updater lock, and durable start phases. Completed
package/CHANGELOG.md CHANGED
@@ -4,6 +4,10 @@ All notable changes to Kōbō are documented here. The format is based on
4
4
  [Keep a Changelog](https://keepachangelog.com/). Each release is an `## <version>`
5
5
  section — the in-app "What's new" dialog reads this file.
6
6
 
7
+ ## 2.1.21
8
+
9
+ - feat(vps): introduce non-root runtime launcher and UID/GID support
10
+
7
11
  ## 2.1.20
8
12
 
9
13
  - fix(progress): clarify activity updates and completion
package/CONFIGURATION.md CHANGED
@@ -1183,17 +1183,20 @@ Traefik already exists. Build the image from a source checkout before starting.
1183
1183
  All four use the same underlying image (`docker build -t kobo:local .` from the
1184
1184
  repo root). The original three mount provider/Git configuration from the host;
1185
1185
  the VPS example keeps independent credentials in a persistent container home.
1186
- The VPS example overrides the entrypoint to start Node directly; the original
1186
+ The VPS example uses `kobo-vps-entrypoint`: it prepares the dedicated home volume
1187
+ then drops to `KOBO_UID:KOBO_GID` before starting Node, without `sshd`. The original
1187
1188
  files use the image entrypoint, which starts `sshd` even when port 22 is not published.
1189
+ See the [VPS runtime user and migration procedure](./docs/deployment-vps.md#runtime-user-and-existing-installations)
1190
+ and [project/skills operations guide](./docs/vps-projects-and-skills.md).
1188
1191
 
1189
1192
  ### The image
1190
1193
 
1191
1194
  Multi-stage `Dockerfile`, `node:24.19.0-slim` base:
1192
1195
 
1193
1196
  - **Build stage**: installs root, client, and PWA deps, then runs `npm run build` (client via Quasar, server via `tsc`).
1194
- - **Runtime stage**: production deps only, plus OS tooling the agents and forge CLIs need — `git`, `openssh-client` + `openssh-server`, `ripgrep`, `fd-find`, `jq`, `curl`, Python + build tools, the Docker CLI and Compose plugin (`docker compose`, talks to a mounted host socket, see [Volumes](#volumes)), `gh`, `glab`, `tmux`, `vim`/`nano`, and more for interactive shell use.
1197
+ - **Runtime stage**: production deps only, plus OS tooling the agents and forge CLIs need — `git`, `openssh-client` + `openssh-server`, `ripgrep`, `fd-find`, `jq`, `curl`, Python + build tools, the Docker CLI, Compose and Buildx plugins (talk to a mounted host socket, see [Volumes](#volumes)), netcat (`nc`), `gh`, `glab`, `tmux`, `vim`/`nano`, and more for interactive shell use.
1195
1198
  - **Global CLIs**: `@anthropic-ai/claude-code` and `@openai/codex` are installed globally for interactive login (`claude /login`, `codex login` over SSH) — separate from Kōbō's own bundled SDK/subprocess usage, which needs no global install.
1196
- - **`git config --system --add safe.directory '*'`**: the container runs as root, but bind-mounted project repos keep the *host* user's UID on disk. Git's ownership check (CVE-2022-24765 mitigation) refuses to touch a repo it doesn't "own" unless told otherwise. This line pre-trusts every mounted path — fine for a single-user dev container, not something you'd do on a multi-tenant host.
1199
+ - **`git config --system --add safe.directory '*'`**: legacy Compose examples run as root while mounted repos retain the host UID; the VPS example instead uses a matching non-root UID. The image still pre-trusts mounted Git repositories. This broad trust is for a single-user environment, not a multi-tenant host.
1197
1200
  - **`HEALTHCHECK`**: `curl -f http://localhost:$SERVER_PORT/api/health` every 30s. `/api/health` is exempt from the network-access token gate unconditionally (even with `networkAccessBehindProxy` on) — Docker's `HEALTHCHECK` process has no way to supply the runtime-generated token, and without the exemption a "behind a reverse proxy" container never reports healthy, which makes Traefik's Docker provider filter it out of its router table entirely (Traefik drops unhealthy/starting containers from routing).
1198
1201
  - **`EXPOSE 3000`**, `ENV SERVER_PORT=3000`, entrypoint `docker-entrypoint.sh` → `npm start`.
1199
1202
 
@@ -1201,6 +1204,15 @@ Build once: `docker build -t kobo:local .`
1201
1204
 
1202
1205
  ### Environment variables (Docker)
1203
1206
 
1207
+ The VPS template accepts `KOBO_UID` and `KOBO_GID` (positive numeric IDs, default
1208
+ 1000); the installer defaults to the invoking host user's IDs. It retains
1209
+ `HOME=/root` for volume compatibility and adds the mounted Docker socket's group
1210
+ when applicable. Use `docker compose exec kobo kobo-vps-entrypoint --exec <command>`
1211
+ for logins, skills and project tooling as that user. It also sets
1212
+ `NODE_ENV=development` and `NPM_CONFIG_INCLUDE=dev` for project subprocesses;
1213
+ Kōbō continues serving its prebuilt UI/server. The base image retains its
1214
+ production default for other deployment layouts.
1215
+
1204
1216
  Beyond the [general environment variables](#environment-variables), these matter specifically for a container deployment:
1205
1217
 
1206
1218
  | Variable | Where it's set | Purpose |
@@ -1482,6 +1494,10 @@ rules survive restarts and can be removed from the workspace's permission UI.
1482
1494
 
1483
1495
  Each workspace can run its own dev server. The **Tools** panel shows its status, a clickable URL, the live container count, and a logs button. That works only when your project follows a small contract. This section documents the contract so the panel lights up for *your* project, not just Docker-instance projects.
1484
1496
 
1497
+ For remote installations, `localhost` in that link is the browser's computer.
1498
+ See [VPS dev-server access](./docs/vps-projects-and-skills.md#reach-a-development-server-from-your-pc)
1499
+ for SSH forwarding, separate proxy routes and scoped stop/cleanup commands.
1500
+
1485
1501
  ### What the panel actually does
1486
1502
 
1487
1503
  Kōbō treats the dev server in two independent layers:
@@ -496,6 +496,7 @@ function readEffectiveSettingsSafe(projectPath) {
496
496
  function buildMcpServers(workspaceId, extraEnv, memoryCapability) {
497
497
  const mcpServerCompiled = getCompiledMcpServerPath();
498
498
  const mcpServerSource = getMcpServerSourcePath();
499
+ const global = getGlobalSettings();
499
500
  return [
500
501
  {
501
502
  name: 'kobo-tasks',
@@ -508,6 +509,11 @@ function buildMcpServers(workspaceId, extraEnv, memoryCapability) {
508
509
  KOBO_DB_PATH: getDbPath(),
509
510
  KOBO_SETTINGS_PATH: getSettingsPath(),
510
511
  KOBO_BACKEND_URL: `http://127.0.0.1:${backendPort}`,
512
+ // Behind a proxy, even internal loopback requests require network auth.
513
+ // Per-launch memory/progress capabilities remain independently required.
514
+ ...(global.networkAccessBehindProxy && global.networkAccessToken
515
+ ? { KOBO_NETWORK_TOKEN: global.networkAccessToken }
516
+ : {}),
511
517
  },
512
518
  },
513
519
  ];
@@ -10,10 +10,16 @@ services:
10
10
  init: true
11
11
  restart: unless-stopped
12
12
  # Use the host's SSH and `docker compose exec`; no container SSH daemon.
13
- entrypoint: ["node"]
14
- command: ["dist/server/index.js"]
13
+ # Bootstrap permissions as root, then execute Kōbō/agents as this host UID/GID.
14
+ entrypoint: ["kobo-vps-entrypoint"]
15
+ command: ["node", "dist/server/index.js"]
15
16
  environment:
16
- NODE_ENV: production
17
+ KOBO_UID: ${KOBO_UID:-1000}
18
+ KOBO_GID: ${KOBO_GID:-1000}
19
+ HOME: /root
20
+ # Kōbō serves its built assets; project commands need development dependencies.
21
+ NODE_ENV: development
22
+ NPM_CONFIG_INCLUDE: dev
17
23
  SERVER_PORT: "3000"
18
24
  KOBO_HOME: /root/.config/kobo
19
25
  KOBO_NETWORK_ACCESS_ENABLED: "true"
@@ -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,7 +561,7 @@ 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
566
  build cache; backup duration depends on the volume size.
463
567
 
@@ -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
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@loicngr/kobo",
3
- "version": "2.1.20",
3
+ "version": "2.1.21",
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;
@@ -262,15 +262,29 @@ 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):