@loicngr/kobo 2.1.19 → 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,14 @@ 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
+
11
+ ## 2.1.20
12
+
13
+ - fix(progress): clarify activity updates and completion
14
+
7
15
  ## 2.1.19
8
16
 
9
17
  - feat(progress): add session activity bars and improve launch prompts
package/CONFIGURATION.md CHANGED
@@ -885,10 +885,23 @@ the previous process are interrupted; external reporters may still be running
885
885
  elsewhere, so their last state is preserved. A new internal launch uses a new
886
886
  reporting identity.
887
887
 
888
- Workspace launch instructions require agents to use these tools for long tasks
889
- and pass the requirement to subagents. When a restricted subagent cannot access
890
- the tools, its parent reports the activity on its behalf. Reporting permissions
891
- do not grant permission to edit project files or complete tasks during a review.
888
+ Workspace launch instructions require the full reporting lifecycle: create a
889
+ bar, keep its returned ID and sequence, update that same bar at actual phase
890
+ changes and meaningful completed milestones, and finish it before the final
891
+ report once the outcome is known. Even without a measurable percentage, agents
892
+ must update the current step; omitting both numeric values preserves
893
+ indeterminate progress. No percentage is inferred from tool calls, elapsed time
894
+ or model activity. Unchanged updates, polling and timer-driven reporting loops
895
+ are not requested.
896
+
897
+ Parents pass this lifecycle to subagents, which report and finish their own
898
+ activities before returning. When a restricted subagent cannot access the tools,
899
+ its parent reports observed milestones and the outcome on its behalf, without
900
+ duplicate bars. Yielding while background work continues does not mean success.
901
+ Reporting permissions do not grant permission to edit project files or complete
902
+ tasks during a review. These instructions guide the model; they cannot guarantee
903
+ that it reports every milestone. Updated guidance applies on the next launch or
904
+ resume, not retroactively to an already running agent.
892
905
 
893
906
  The service retains at most 1,000 indicators per session, with at most 32 running
894
907
  at once. Hidden records are retained so retries cannot recreate dismissed bars.
@@ -1170,17 +1183,20 @@ Traefik already exists. Build the image from a source checkout before starting.
1170
1183
  All four use the same underlying image (`docker build -t kobo:local .` from the
1171
1184
  repo root). The original three mount provider/Git configuration from the host;
1172
1185
  the VPS example keeps independent credentials in a persistent container home.
1173
- 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
1174
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).
1175
1191
 
1176
1192
  ### The image
1177
1193
 
1178
1194
  Multi-stage `Dockerfile`, `node:24.19.0-slim` base:
1179
1195
 
1180
1196
  - **Build stage**: installs root, client, and PWA deps, then runs `npm run build` (client via Quasar, server via `tsc`).
1181
- - **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.
1182
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.
1183
- - **`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.
1184
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).
1185
1201
  - **`EXPOSE 3000`**, `ENV SERVER_PORT=3000`, entrypoint `docker-entrypoint.sh` → `npm start`.
1186
1202
 
@@ -1188,6 +1204,15 @@ Build once: `docker build -t kobo:local .`
1188
1204
 
1189
1205
  ### Environment variables (Docker)
1190
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
+
1191
1216
  Beyond the [general environment variables](#environment-variables), these matter specifically for a container deployment:
1192
1217
 
1193
1218
  | Variable | Where it's set | Purpose |
@@ -1469,6 +1494,10 @@ rules survive restarts and can be removed from the workspace's permission UI.
1469
1494
 
1470
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.
1471
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
+
1472
1501
  ### What the panel actually does
1473
1502
 
1474
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
  ];
@@ -2,6 +2,10 @@ export const PROGRESS_BAR_PLAN_EXCEPTION = 'Operational exception in Plan/read-o
2
2
  /** Shared across engines, fresh/resumed launches and delegated agents. */
3
3
  export const PROGRESS_BAR_PROMPT = `<kobo-progress-reporting>
4
4
  ${PROGRESS_BAR_PLAN_EXCEPTION}
5
- Use Kōbō's create_progress_bar, update_progress_bar and finish_progress_bar for long-running tasks, including tests and delegated work. Give each activity a clear label and stable activity_key; follow tool schemas for ordered updates and retries. Report meaningful steps and measured current/total only when known; otherwise keep progress indeterminate. Never invent percentages or poll these tools. Finish with succeeded or failed only when known. A dismissed bar stays hidden: continue without recreating it; dismissal is not cancellation. Reporting never substitutes for verification, and an interrupted reporter does not prove background work stopped.
6
- Pass this requirement to all subagents when delegating. Subagents must not schedule or replace workspace wakeups; the parent coordinates follow-up. Each subagent reports its own long task through the inherited Kōbō MCP tools; the parent must not duplicate that activity. If a custom subagent lacks these tools, the parent reports its observed progress accurately instead. After resume, start new activity keys for new work; do not reopen a finished activity.
5
+ For each long-running activity (tests, audits, builds or delegated work), follow this reporting lifecycle:
6
+ 1. Call create_progress_bar with a clear label, current step and stable activity_key. Save its returned id and sequence; creating a bar is only the start of reporting.
7
+ 2. Call update_progress_bar with that id and an increasing sequence: update the same bar at each actual phase change or meaningful completed milestone, including during a long phase. Even without a measured percentage, report the current step; omit both current and total for step-only updates. Use measured numbers only when known. Never invent percentages, poll, send unchanged updates or start timer-driven reporting loops; follow tool retry guidance.
8
+ 3. Call finish_progress_bar on that id before your final report (or return to the parent) when the activity's succeeded/failed outcome is known. Do not declare ongoing background work finished merely because you are yielding control.
9
+ Pass this requirement to all subagents when delegating: each reports and finishes its own long activity through inherited Kōbō MCP tools; the parent must not duplicate it. If a custom subagent lacks these tools, the parent reports its observed milestones and outcome instead. Subagents must not schedule or replace workspace wakeups; the parent coordinates follow-up.
10
+ A dismissed bar stays hidden: continue without recreating it; dismissal is not cancellation. Reporting never substitutes for verification, and an interrupted reporter does not prove background work stopped. After resume, use new activity keys for new work; do not reopen finished activities.
7
11
  </kobo-progress-reporting>`;
@@ -22,15 +22,15 @@ function tools(external) {
22
22
  annotations: { readOnlyHint: readOnly, destructiveHint: false, openWorldHint: false },
23
23
  });
24
24
  return [
25
- definition('create_progress_bar', 'Report a labelled long-running activity in this session. Use a new activity_key per activity; identical retries return its current state, including dismissal. Report measured current/total together, or omit both for indeterminate progress. Closing a bar never cancels work.', {
25
+ definition('create_progress_bar', 'Start reporting a labelled long-running activity. Save the returned id and sequence, update that same bar at actual phase changes and meaningful completed milestones, and finish it before the final report when the outcome is known. Use a new activity_key per activity; identical retries return its current state, including dismissal. Supply measured current/total together or omit both for indeterminate progress. Closing a bar never cancels work.', {
26
26
  activity_key: { type: 'string', minLength: 1, maxLength: 100 },
27
27
  label: { type: 'string', minLength: 1, maxLength: 200 },
28
28
  step,
29
29
  current: numberOrNull,
30
30
  total: numberOrNull,
31
31
  }, ['activity_key', 'label']),
32
- definition('update_progress_bar', 'Update absolute measured progress and/or step. Both numbers must be provided together, or both null. At most one accepted running update per second; respect retry guidance. Dismissed bars remain hidden. Never invent a percentage.', { progress_id: id, sequence, step, current: numberOrNull, total: numberOrNull }, ['progress_id', 'sequence']),
33
- definition('finish_progress_bar', 'Finish reporting with succeeded or failed. Terminal state is immutable except an exact retry. Success hides after five seconds; failure remains until the user closes it. Reporting does not mark workspace tasks done.', { progress_id: id, sequence, step, outcome: { type: 'string', enum: ['succeeded', 'failed'] } }, ['progress_id', 'sequence', 'outcome']),
32
+ definition('update_progress_bar', 'Update the existing bar at actual phase changes or meaningful completed milestones, even when no percentage is known. Omit both numbers for step-only updates (previous numeric values are preserved); provide both measured numbers together, or both null to switch to indeterminate. Use the returned id as progress_id and increase sequence. Never invent percentages or send unchanged updates. At most one accepted running update per second; respect retry guidance without polling. Dismissed bars remain hidden.', { progress_id: id, sequence, step, current: numberOrNull, total: numberOrNull }, ['progress_id', 'sequence']),
33
+ definition('finish_progress_bar', 'Finish the same bar with succeeded or failed before your final report or return to the parent, only once the activity outcome is known. Do not finish ongoing background work merely because you yield control. Increase sequence; terminal state is immutable except an exact retry. Success hides after five seconds; failure remains until the user closes it. Reporting does not mark workspace tasks done.', { progress_id: id, sequence, step, outcome: { type: 'string', enum: ['succeeded', 'failed'] } }, ['progress_id', 'sequence', 'outcome']),
34
34
  definition('list_progress_bars', 'Read progress bars in the explicitly selected session, including dismissed/terminal bars. Responses contain bounded pages and serverTime; do not poll without a concrete need.', {
35
35
  limit: { type: 'integer', minimum: 1, maximum: 100, default: 50 },
36
36
  offset: { type: 'integer', minimum: 0, default: 0 },
@@ -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.19",
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):
@@ -578,7 +578,9 @@ IDs. Workspace-bound callers omit those fields: the backend derives their scope
578
578
  from a per-launch capability. They cannot forge another workspace, session or
579
579
  launch. The managed server exposes the same tools to capable subagents; custom
580
580
  subagent tool restrictions still apply. If a child lacks access, the parent
581
- reports its activity rather than creating a duplicate indicator.
581
+ reports its observed milestones and outcome rather than creating a duplicate
582
+ indicator. Pass the full create → update → finish lifecycle to delegates; they
583
+ must finish their own activity before returning when its outcome is known.
582
584
 
583
585
  For example, an external client can create an activity with:
584
586
 
@@ -593,7 +595,9 @@ For example, an external client can create an activity with:
593
595
  }
594
596
  ```
595
597
 
596
- Keep the returned `id`. Report absolute progress with `update_progress_bar`:
598
+ Keep the returned `id` and `sequence`. Update **the same bar** at actual phase
599
+ changes and meaningful completed milestones, including during a long phase.
600
+ Report absolute progress with `update_progress_bar`:
597
601
 
598
602
  ```json
599
603
  {
@@ -607,8 +611,28 @@ Keep the returned `id`. Report absolute progress with `update_progress_bar`:
607
611
  }
608
612
  ```
609
613
 
610
- Then call `finish_progress_bar` with the same scope/id, a higher `sequence`,
611
- `outcome: "succeeded"` or `"failed"`, and an optional summary in `step`.
614
+ An audit with no measurable denominator still needs step updates. After creating
615
+ an indeterminate activity, send this when its phase changes:
616
+
617
+ ```json
618
+ {
619
+ "workspace_id": "<workspace ID>",
620
+ "session_id": "<session ID>",
621
+ "progress_id": "<returned id>",
622
+ "sequence": 1,
623
+ "step": "Code inspection complete; verifying findings"
624
+ }
625
+ ```
626
+
627
+ Omitting **both** `current` and `total` is valid on updates. It keeps an
628
+ indeterminate activity indeterminate, or preserves existing numeric values.
629
+ Do not invent a percentage, send unchanged updates or create periodic LLM turns
630
+ just to refresh a bar.
631
+
632
+ Before your final report (or return to the parent), call `finish_progress_bar`
633
+ with the same scope/id, a higher `sequence`, `outcome: "succeeded"` or `"failed"`,
634
+ and an optional summary in `step`, once the outcome is known. Do not finish an
635
+ activity merely because you yield while its background work continues.
612
636
  Supply numeric progress only when known: both values must be finite, with
613
637
  `total > 0` and `0 <= current <= total`. Omit both at creation, or send both as
614
638
  `null`, for an indeterminate indicator. Updates with no numeric pair keep the