@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 +15 -0
- package/CHANGELOG.md +4 -0
- package/CONFIGURATION.md +19 -3
- package/dist/server/services/agent/orchestrator.js +6 -0
- package/docker-compose.vps.yml +9 -3
- package/docs/deployment-vps.md +128 -24
- package/docs/getting-started.md +5 -0
- package/package.json +1 -1
- package/scripts/kobo-install +35 -8
- package/scripts/kobo-update +18 -4
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
|
|
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
|
|
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 '*'`**:
|
|
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
|
];
|
package/docker-compose.vps.yml
CHANGED
|
@@ -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
|
-
|
|
14
|
-
|
|
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
|
-
|
|
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"
|
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,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
|
|
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
|
|
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
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@loicngr/kobo",
|
|
3
|
-
"version": "2.1.
|
|
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",
|
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;
|
|
@@ -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}\
|
|
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):
|