@turmind/halo 1.6.2 → 1.6.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (38) hide show
  1. package/README.md +58 -67
  2. package/admin-out/404.html +1 -1
  3. package/admin-out/_next/static/chunks/989-5d7743ff56d00dd7.js +1 -0
  4. package/admin-out/_next/static/chunks/app/page-8881b4f87a3d5ae8.js +1 -0
  5. package/admin-out/index.html +1 -1
  6. package/admin-out/index.txt +4 -4
  7. package/bin/halo.js +4 -0
  8. package/bundled-docs/design/architecture.md +4 -1
  9. package/bundled-docs/design/canvas-extensions.md +5 -5
  10. package/bundled-docs/design/cron.md +3 -3
  11. package/bundled-docs/design/observability.md +7 -5
  12. package/bundled-docs/design/storage.md +8 -5
  13. package/bundled-docs/design/wechat.md +1 -1
  14. package/bundled-docs/design/ws.md +1 -0
  15. package/bundled-docs/dev/add-model-provider.md +2 -1
  16. package/bundled-docs/dev/api.md +7 -3
  17. package/bundled-docs/dev/deploy.md +114 -26
  18. package/bundled-docs/dev/desktop-packaging.md +34 -0
  19. package/bundled-docs/dev/env.md +3 -3
  20. package/bundled-docs/dev/logging.md +13 -2
  21. package/bundled-docs/dev/tools.md +2 -2
  22. package/bundled-docs/guide/agents.md +2 -2
  23. package/bundled-docs/guide/cli.md +20 -8
  24. package/bundled-docs/guide/getting-started.md +13 -5
  25. package/bundled-docs/guide/workspace.md +2 -0
  26. package/bundled-docs/requirements/agent.md +1 -1
  27. package/bundled-docs/requirements/explorer.md +14 -2
  28. package/bundled-docs/requirements/settings.md +2 -2
  29. package/bundled-docs/requirements/skill.md +4 -4
  30. package/dist/index.js +2757 -1490
  31. package/package.json +3 -2
  32. package/templates/models/gemini.yaml +93 -0
  33. package/templates/skills/cron/SKILL.md +3 -2
  34. package/templates/skills/workspace/setup-tidy.md +1 -0
  35. package/admin-out/_next/static/chunks/989-ddba0eb72566da23.js +0 -1
  36. package/admin-out/_next/static/chunks/app/page-1d27029caba48956.js +0 -1
  37. /package/admin-out/_next/static/{VgAe62AuGEiYrF4JQ7WQe → kfp1cMc36J6hjZ06L9mm-}/_buildManifest.js +0 -0
  38. /package/admin-out/_next/static/{VgAe62AuGEiYrF4JQ7WQe → kfp1cMc36J6hjZ06L9mm-}/_ssgManifest.js +0 -0
@@ -16,8 +16,8 @@ set -e
16
16
  version='x.y.z' # exact requested version
17
17
  curl -sfI "https://registry.npmjs.org/@turmind/halo/-/halo-$version.tgz" >/dev/null # tarball replicated? (a fresh publish can 404 for ~15 min)
18
18
  sudo sh -c 'umask 022; npm install -g --prefix /usr --no-audit --no-fund "@turmind/halo@$1"' sh "$version"
19
- installed=$(sudo -u ubuntu env HOME=/home/ubuntu /usr/bin/halo --version 2>&1)
20
- test "$installed" = "halo $version" # CLI writes to stderr; mismatch or launch failure stops before restart
19
+ installed=$(sudo -u ubuntu env HOME=/home/ubuntu /usr/bin/halo --version)
20
+ test "$installed" = "halo $version" # mismatch or launch failure stops before restart
21
21
  sudo systemctl restart halo.service
22
22
  curl --fail --retry 10 --retry-connrefused --retry-delay 1 --max-time 3 http://127.0.0.1:9527/api/health # status ok, requested version
23
23
  sudo journalctl -u halo.service --since '2 minutes ago' -n 40 --no-pager
@@ -49,9 +49,40 @@ Nginx is **not required**. Use it only for domain routing, SSL termination, or w
49
49
 
50
50
  ## Prerequisites
51
51
 
52
- - Node.js ≥ 22
53
- - pnpm 11 (pinned by the root `packageManager`; `corepack enable` picks it up)
54
- - AWS credentials configured (`~/.aws/credentials` or env vars) with Bedrock access
52
+ - Node.js ≥ 22 — Ubuntu 24.04's apt `nodejs` is 18, too old; use NodeSource (`curl -fsSL https://deb.nodesource.com/setup_22.x | sudo -E bash - && sudo apt-get install -y nodejs`), `brew install node@22`, or nvm / fnm. On an older Node, `halo` exits 1 at launch with the version it found and these install hints instead of crashing. On a just-booted cloud VM the first `apt-get install` can fail on the dpkg lock (`Could not get lock …`, exit 100) while cloud-init / the SSM agent is still installing packages — wait a minute and rerun
53
+ - pnpm 11 — source builds only (pinned by the root `packageManager`; `corepack enable` picks it up)
54
+ - AWS credentials — optional, only for the Bedrock providers (`~/.aws/credentials`, env vars, or an instance role)
55
+ - bubblewrap (Linux) — needed for the `workspace` / `readonly` access levels: `sudo apt install bubblewrap`; on Ubuntu 23.10+ / 24.04 also the [AppArmor profile](#bubblewrap-on-ubuntu-2310-and-2404-apparmor)
56
+
57
+ ### bubblewrap on Ubuntu 23.10 and 24.04 (AppArmor)
58
+
59
+ Ubuntu 23.10+ ships `kernel.apparmor_restrict_unprivileged_userns = 1`, and the `bubblewrap` package brings no AppArmor profile, so `apt install bubblewrap` alone isn't enough. Check with:
60
+
61
+ ```bash
62
+ bwrap --ro-bind / / --die-with-parent -- /bin/true
63
+ # bwrap: setting up uid map: Permission denied ← blocked by AppArmor
64
+ ```
65
+
66
+ Halo's boot probe sees the same failure. The server warns once at startup, `[Server] OS sandbox unavailable (bwrap cannot create namespaces: bwrap: setting up uid map: Permission denied) — workspace/readonly sessions get no shell_exec. Fix: …` with a link to this section, and then runs without an OS sandbox. `halo cli` / `tui` print the same line, tagged `[cli]`, but only for `--access workspace|readonly`; without bubblewrap installed, the fix it names is `sudo apt install bubblewrap`. Fix it with a profile that lets only `bwrap` create user namespaces:
67
+
68
+ ```bash
69
+ sudo apt-get install -y bubblewrap
70
+ sudo tee /etc/apparmor.d/bwrap >/dev/null <<'EOF'
71
+ abi <abi/4.0>,
72
+ include <tunables/global>
73
+ profile bwrap /usr/bin/bwrap flags=(unconfined) {
74
+ userns,
75
+ include if exists <local/bwrap>
76
+ }
77
+ EOF
78
+ sudo apparmor_parser -r /etc/apparmor.d/bwrap
79
+ halo server restart && curl -s --retry 15 --retry-connrefused --retry-delay 1 localhost:9527/api/health # "sandbox":"bwrap"
80
+ # a unit-run server is restarted through systemctl (Option C: may need the printed `sudo systemctl restart halo`)
81
+ ```
82
+
83
+ Alternative: `sudo sysctl -w kernel.apparmor_restrict_unprivileged_userns=0` (persist it in `/etc/sysctl.d/`). That also works, but it lifts the restriction for **every** program, not just bwrap.
84
+
85
+ Check: `GET /api/health` reports `"sandbox":"bwrap"`; `"sandbox":null` means non-full sessions still have no OS sandbox (and no `shell_exec`).
55
86
 
56
87
  ## 1. Install dependencies and build
57
88
 
@@ -78,7 +109,7 @@ halo setup # interactive — picks up arrow-key UI on TTYs
78
109
 
79
110
  Re-run any time to change the password, refresh model keys, or toggle optional skills. Built-in agent / skill files are force-overwritten on every run; user-created agents and skills are left alone.
80
111
 
81
- For Docker / CI builds where stdin isn't a TTY, see the **Docker** section below.
112
+ For Docker / CI builds where stdin isn't a TTY, see [Non-interactive (Docker / CI) details](#non-interactive-docker--ci-details) below.
82
113
 
83
114
  ## 4. Environment variables (optional)
84
115
 
@@ -93,7 +124,9 @@ When `HALO_PASSWORD` is set, the password chosen via `halo setup` is ignored at
93
124
 
94
125
  ## 5. Start the server
95
126
 
96
- > **单实例锁**:server 启动时会写 `~/.halo/global/server.lock`(有 `flock` 命令时用 flock,没有就回退到 pid 探测),退出时自动清理。如果 lock 里记录的进程还活着,新 server 会拒绝启动并打印 `kill <pid>` 提示。原因是 WeChat 长轮询循环跟 HTTP server 解耦,多个进程并存会导致同一条微信消息被 fan out 到多个 session。陈旧 lock(进程已不在)会被自动识别并清除。要手动重启先 kill 旧的:`kill $(cat ~/.halo/global/server.lock)`。
127
+ > **Single-instance lock**: on start the server writes `~/.halo/global/server.lock` (flock when the `flock` command exists, otherwise a pid probe) and removes it on exit. If the process recorded in the lock is still alive, a new server refuses to start and prints a `kill <pid>` hint. The reason: the WeChat long-poll loop is decoupled from the HTTP server, so several processes side by side would fan the same WeChat message out to several sessions. A stale lock (process gone) is detected and cleared automatically. To restart by hand, kill the old one first: `kill $(cat ~/.halo/global/server.lock)`.
128
+
129
+ Options A and B below run a **source checkout**; for the npm install see [systemd for the npm install](#option-c-systemd-for-the-npm-install).
97
130
 
98
131
  ### Option A: quick start
99
132
 
@@ -128,6 +161,39 @@ sudo systemctl daemon-reload
128
161
  sudo systemctl enable --now halo
129
162
  ```
130
163
 
164
+ ### Option C: systemd for the npm install
165
+
166
+ With `@turmind/halo` installed via npm (see [§8](#8-install-via-npm-recommended)) on Linux with systemd, the simple path is `halo server start -d`. It writes and enables the user unit `~/.config/systemd/user/halo.service` (`Restart=on-failure`, `RestartSec=5`), turns on linger so it starts at boot, and saves your shell's environment to `~/.halo/secrets/server.env` for it. If linger can't be enabled it still starts, and warns `sudo loginctl enable-linger <user>`.
167
+
168
+ Write the system unit below when you want a system-level unit instead. If `start -d` already made the user unit, `systemctl --user disable --now halo` first, or both start at boot. Once a unit in `/etc/systemd/system/` runs `halo server start` as you (`User=` you, same HOME), `start -d` starts it instead of creating a user unit. The system unit runs the `halo` binary itself — no `WorkingDirectory`, and no `HALO_PASSWORD` once `halo setup` has stored the password hash (run setup as the same user):
169
+
170
+ ```bash
171
+ sudo tee /etc/systemd/system/halo.service <<'EOF'
172
+ [Unit]
173
+ Description=Halo Server
174
+ After=network.target
175
+
176
+ [Service]
177
+ Type=simple
178
+ User=YOUR_USER
179
+ ExecStart=/usr/bin/halo server start
180
+ Restart=on-failure
181
+ RestartSec=5
182
+
183
+ [Install]
184
+ WantedBy=multi-user.target
185
+ EOF
186
+
187
+ sudo systemctl daemon-reload
188
+ sudo systemctl enable --now halo
189
+ ```
190
+
191
+ Use `which halo` for the `ExecStart` path (`/usr/bin/halo` with apt / NodeSource Node). Each start prints `[Server] Halo <version> listening on port 9527 on all network interfaces — local: http://localhost:9527` and a network-exposure line to stderr, whatever the log level. For a system unit, look in the journal (`journalctl -u halo`).
192
+
193
+ Under either unit kind, `halo server stop|restart|status` find the unit that owns the server (the lock pid's cgroup) and go through `systemctl` (`--user` for the user unit). A system unit may need sudo: when polkit refuses, the command prints the exact `sudo systemctl restart|stop halo` line to run. Running that directly is equally valid. `stop` only stops the unit; it starts again at boot until you disable it (`systemctl --user disable halo`, or `sudo systemctl disable halo`). Upgrade with `halo upgrade && halo server restart`.
194
+
195
+ **Known limits**: the user unit pins the Node binary that ran `start -d` (`process.execPath`), so after an nvm Node upgrade run `halo server restart` once to rewrite it.
196
+
131
197
  ### Crash semantics — why `Restart=on-failure` is load-bearing
132
198
 
133
199
  The server **exits 1 on any uncaught exception** (`process.on('uncaughtException')`
@@ -137,18 +203,22 @@ half-taken locks and half-written files, and since the db is the source of truth
137
203
  — every daemon (cron / evo ticker / channels) reconciles from it on boot — a
138
204
  restart is a full recovery while a zombie process silently corrupts state.
139
205
 
140
- So the supervisor is what turns a crash into a ~5s blip: the unit above pairs
141
- `Restart=on-failure` (fires exactly on non-zero exit) with `RestartSec=5`.
142
- Docker deployments want an equivalent `restart:` policy. Under a bare `nohup
206
+ So the supervisor is what turns a crash into a ~5s blip: the units above — and
207
+ the user unit `halo server start -d` writes — pair `Restart=on-failure` (fires
208
+ exactly on non-zero exit) with `RestartSec=5`. Docker deployments want an
209
+ equivalent `restart:` policy. Under a bare `nohup
143
210
  node dist/index.js` / foreground `halo server start` there is still no
144
211
  supervisor — a crash is a hard stop, with the stack in
145
212
  `~/.halo/global/logs/server.log` for whoever restarts it.
146
213
  `unhandledRejection` is only logged, not fatal.
147
214
 
148
- **`halo server start -d` brings its own bounded supervisor.** The detached
149
- process `-d` spawns is not the server: it re-execs `halo server start`
150
- (foreground) with `HALO_SUPERVISE=1`, and *that* process supervises the real
151
- server as its child (`packages/cli/src/server-supervisor.ts`). Policy:
215
+ **Off systemd, `halo server start -d` brings its own bounded supervisor** —
216
+ on macOS, Windows, Linux without systemd, a HOME that isn't your passwd home,
217
+ or a user manager the shell can't reach (on Linux `-d` prints a one-line note
218
+ saying which). The detached process `-d` spawns is not the server: it re-execs
219
+ `halo server start` (foreground) with `HALO_SUPERVISE=1`, and *that* process
220
+ supervises the real server as its child (`packages/cli/src/server-supervisor.ts`).
221
+ Policy:
152
222
 
153
223
  - **Restarts only on failure** — non-zero exit, or death by any signal other
154
224
  than `SIGTERM` / `SIGINT`. `SIGKILL` (OOM killer) and `SIGSEGV` count as
@@ -159,7 +229,9 @@ server as its child (`packages/cli/src/server-supervisor.ts`). Policy:
159
229
  server start -d`.
160
230
  - **2s pause between attempts**, so an instant-crash loop can't spin the CPU.
161
231
  - Every decision lands in the daemon log as `[respawn] …` — the stdout/stderr
162
- capture `~/.halo/logs/server.log`, which `halo server logs` tails.
232
+ capture `~/.halo/logs/server.log`, which `halo server logs` tails. Only this
233
+ supervisor writes them; under the user unit a crash restart is systemd's
234
+ `RestartSec=5` (`systemctl --user status halo` shows it).
163
235
 
164
236
  `halo server stop` (and `--force`) is not mistaken for a crash: it stamps the
165
237
  server pid into a marker file `~/.halo/global/server.stop` *before* signalling,
@@ -172,9 +244,11 @@ one's marker.
172
244
 
173
245
  Pid ownership is unchanged: `~/.halo/global/server.lock` is still written and
174
246
  flock'd by the **server** process itself, so `halo server stop|restart|status`
175
- keep targeting the server exactly as before. The supervisor has no pidfile of
176
- its own; killing it directly leaves the server running as an orphan (same end
177
- state as the pre-supervisor `-d`), and `halo server stop` still stops it.
247
+ target the server's own pid (or, for a unit-run server, its unit). The
248
+ supervisor has no pidfile of its own; killing it directly leaves the server
249
+ running as an orphan (same end state as the pre-supervisor `-d`), and `halo
250
+ server stop` still stops it. On a systemd host, `halo server restart` moves a
251
+ supervised `-d` server under the user unit.
178
252
 
179
253
  ## 6. Verify
180
254
 
@@ -196,7 +270,7 @@ cd /path/to/halo
196
270
  cd packages/admin && npx next build --no-lint && node scripts/copy-monaco.mjs && cd ../..
197
271
  pnpm --filter @turmind/halo-server build
198
272
 
199
- # 用 lock 文件里的 pid 确保旧进程彻底退出(避免孤儿进程继续长轮询)
273
+ # use the pid in the lock file so the old process really exits (an orphan would keep long-polling)
200
274
  kill $(cat ~/.halo/global/server.lock) 2>/dev/null
201
275
  sleep 2
202
276
  cd packages/server && HALO_PASSWORD=your_password nohup node dist/index.js > /tmp/server.log 2>&1 &
@@ -217,8 +291,8 @@ This installs the `halo` binary on `$PATH`. Subcommands available:
217
291
  | `halo setup` | Interactive password / port / model keys / optional skills setup |
218
292
  | `halo setup --non-interactive` (alias `-y`) | Skip every prompt — seed templates only, supply password via `HALO_PASSWORD` env. Use in Dockerfiles / CI. |
219
293
  | `halo upgrade` | Bump the npm install in place. Compares the bundled version against `npm view @turmind/halo version`; no-op if already latest, otherwise runs `npm install -g @turmind/halo@latest` and prints a server-restart hint. On EACCES, suggests retrying with `sudo`. |
220
- | `halo server start` | Launch HTTP/WS server (foreground). Add `-d` for daemon. |
221
- | `halo server stop` / `restart` / `status` / `logs` | Server lifecycle |
294
+ | `halo server start` | Launch HTTP/WS server (foreground). Add `-d` for daemon — on Linux with systemd, a user unit that starts at boot ([Option C](#option-c-systemd-for-the-npm-install)). |
295
+ | `halo server stop` / `restart` / `status` / `logs` | Server lifecycle; a unit-run server goes through `systemctl` |
222
296
  | `halo tui` | Interactive TUI client |
223
297
  | `halo cli "<prompt>"` | One-shot prompt → reply, exit |
224
298
  | `halo agents` / `halo sessions` | List agents / sessions |
@@ -228,7 +302,7 @@ This installs the `halo` binary on `$PATH`. Subcommands available:
228
302
  ### Upgrade flow
229
303
 
230
304
  1. `halo upgrade` — bumps the on-disk npm package
231
- 2. `halo server restart` — server's startup check sees `~/.halo/global/.template-version` is behind the new bundled `TEMPLATE_VERSION`, runs `ensureHaloHome` automatically, then starts (`halo cli` / `halo tui` run the same check on start, so a CLI-only install refreshes without a server). Refreshes `docs/`, built-in agents, built-in skills, system prompts, and the model registry. User-owned files (USER.md, custom agents/skills, INSTRUCTIONS.md overrides) are left alone. See `init.ts` for the per-category overwrite policy.
305
+ 2. `halo server restart` (under a user or system unit alike; `sudo systemctl restart halo` also works for a system unit) — server's startup check sees `~/.halo/global/.template-version` is behind the new bundled `TEMPLATE_VERSION`, runs `ensureHaloHome` automatically, then starts (`halo cli` / `halo tui` run the same check on start, so a CLI-only install refreshes without a server). Refreshes `docs/`, built-in agents, built-in skills, system prompts, and the model registry. User-owned files (USER.md, custom agents/skills, INSTRUCTIONS.md overrides) are left alone. See `init.ts` for the per-category overwrite policy.
232
306
 
233
307
  ### Public release command reference
234
308
 
@@ -244,7 +318,9 @@ Use only for a new public release; the retained details here are not extra prepa
244
318
 
245
319
  **npm token gotcha**: `npm publish` on this package needs a granular access token created with **"Bypass 2FA"** checked — scope / permission alone yields `403 Two-factor authentication or granular access token with bypass 2fa enabled is required`. `npm whoami` and `npm token list` succeed with a non-bypass token, so neither is a valid pre-flight; check `GET https://registry.npmjs.org/-/npm/v1/tokens` (with the token as bearer) and look for `"bypass_2fa": true` on the token in use before starting a release.
246
320
 
247
- The published package contains a single bundled JS entry (~1.4 MB), all built-in templates (agents / skills / prompts / models), bundled platform docs, and the admin Web UI static export. Total install footprint ≈ 120 MB after npm dedupes shared deps.
321
+ The published package contains a single bundled JS entry (~1.4 MB), all built-in templates (agents / skills / prompts / models), bundled platform docs, and the admin Web UI static export. Total install footprint is about 280 MB on disk (`du`; mostly `node_modules`).
322
+
323
+ Two log files: `~/.halo/logs/server.log` is the stdout/stderr capture of `halo server start -d`, whether the user unit or the off-systemd supervisor runs it (what `halo server logs` tails; the supervisor's `[respawn]` lines appear only off systemd; a system unit logs to the journal instead); `~/.halo/global/logs/server.log` is the server's structured, rotated log (level `general.logging.level`, default `warn`).
248
324
 
249
325
  ### Non-interactive (Docker / CI) details
250
326
 
@@ -252,8 +328,20 @@ The published package contains a single bundled JS entry (~1.4 MB), all built-in
252
328
 
253
329
  - Seeds / refreshes `~/.halo/global/` from the bundled templates (per the per-category overwrite policy in `init.ts`)
254
330
  - Generates `server.jwt_secret` if missing
255
- - Binds the built-in agents to the provider named by `HALO_DEFAULT_PROVIDER`, when set (an unknown id only warns)
331
+ - Binds every built-in agent (default / executor / deep-executor plus the internal ones) to the provider named by `HALO_DEFAULT_PROVIDER`, when set (an unknown id only warns)
332
+ - For that provider, writes each env-backed key into `~/.halo/secrets/settings.yaml` as a `<<NAME>>` placeholder when `NAME` is set in setup's env (e.g. `DEEPSEEK_API_KEY` → `deepseek.secrets.api_key: "<<DEEPSEEK_API_KEY>>"`) — never the key itself; an unset `NAME` only warns. Bedrock is skipped: the AWS credential chain reads `AWS_*` on its own
256
333
  - **Does not** set a password — supply one via `HALO_PASSWORD` env
334
+ - **Does not** store any literal model key — `HALO_PASSWORD` is the admin password only
335
+
336
+ The placeholder is expanded on read (see [env.md](env.md)). Without `HALO_DEFAULT_PROVIDER`, or for another provider, write it yourself; e.g. for DeepSeek:
337
+
338
+ ```yaml
339
+ deepseek:
340
+ secrets:
341
+ api_key: "<<DEEPSEEK_API_KEY>>"
342
+ ```
343
+
344
+ The variable must be set wherever the file is read — the server and `halo cli` / `tui` alike; under systemd add `Environment=DEEPSEEK_API_KEY=…` or an `EnvironmentFile=` to the unit. If you create the file yourself instead of editing the one `halo setup` wrote, `chmod 600` it.
257
345
 
258
346
  Minimal Dockerfile:
259
347
 
@@ -261,10 +349,10 @@ Minimal Dockerfile:
261
349
  FROM node:22-slim
262
350
  RUN apt-get update && apt-get install -y --no-install-recommends bubblewrap && rm -rf /var/lib/apt/lists/*
263
351
  RUN npm install -g @turmind/halo
264
- ENV HALO_PASSWORD=changeme
352
+ ENV HALO_PASSWORD=changeme HALO_DEFAULT_PROVIDER=deepseek
265
353
  ENV HALO_PORT=9527
266
354
  EXPOSE 9527
267
- CMD halo setup -y && halo server start
355
+ CMD halo setup -y && halo server start # key from `docker run -e DEEPSEEK_API_KEY=…`; setup writes its placeholder
268
356
  ```
269
357
 
270
358
  ## 9. Observability (OpenTelemetry collector)
@@ -508,6 +508,40 @@ Still good practice: build server + admin **before** `pnpm dist:arm64`.
508
508
  `find resources/{server,cli}-runtime -type f | awk '{print length($0)}' |
509
509
  sort -rn | head -1` — keep the longest relative path well under ~200.
510
510
 
511
+ - **The same "Halo cannot be closed" prompt, second cause: an orphaned bundled
512
+ `node.exe` the installer can't see.** `installer.nsh customCheckAppRunning`
513
+ runs `taskkill /im Halo.exe /T /F` and then kills any `node.exe` whose image
514
+ lives under `$INSTDIR` — the second pass exists for orphans whose Halo.exe
515
+ parent already died (crash, Task Manager, an interrupted `halo.cmd` child),
516
+ which the tree walk can't reach. That pass used `Get-Process node | Where
517
+ Path -like '$INSTDIR\*'` and **never matched anything**: the NSIS installer
518
+ is a 32-bit process, so the `powershell` it spawns is the 32-bit SysWOW64
519
+ one, and there `Get-Process` returns an empty `.Path` for 64-bit processes.
520
+ The orphan kept its image lock on `resources\node.exe`, extraction failed
521
+ five times, and the user got the Retry prompt with no Halo window left to
522
+ close. Fixed by matching `Get-CimInstance Win32_Process` `.ExecutablePath`,
523
+ which is readable from either bitness. Rule: **any PowerShell the installer
524
+ runs is 32-bit** — never filter processes by `Get-Process .Path` /
525
+ `.MainModule` there. The match itself was a second trap: `$INSTDIR` used to
526
+ be spliced into a single-quoted PS literal with `-like`, so a `'` in the
527
+ install path ended the string (parse error, nothing killed) and `[` / `]`
528
+ read as wildcards (could match a sibling dir). The macro now puts `$INSTDIR`
529
+ in env var `HALO_INSTDIR` (`System::Call
530
+ 'kernel32::SetEnvironmentVariable(…)'`, inherited by the `nsExec` child) and
531
+ matches `ExecutablePath` with a literal, case-insensitive
532
+ `.StartsWith($env:HALO_INSTDIR + '\')` — rule: never splice a path into the
533
+ PS command line. A live `jupyter server` (ipynb kernels) could hold the
534
+ install dir open too, since it inherited the server's cwd inside it; both
535
+ jupyter spawns now pass `cwd: os.homedir()` (see
536
+ [canvas-extensions.md](../design/canvas-extensions.md#notebook-kernels-jupyter-proxy)).
537
+ To tell the two causes apart on a user's box, list
538
+ what still runs from the install dir (64-bit PowerShell): `Get-CimInstance
539
+ Win32_Process | ? ExecutablePath -like "$env:LOCALAPPDATA\Programs\Halo\*"`
540
+ — non-empty = this cause (append `| % { Stop-Process -Id $_.ProcessId
541
+ -Force }` as the workaround, then Retry); empty = look at the MAX_PATH entry
542
+ above. The fix ships in the new installer, and its check runs before the old
543
+ version's uninstaller, so upgrades from any older build benefit.
544
+
511
545
  ## Verifying a build
512
546
 
513
547
  ```bash
@@ -52,8 +52,8 @@ nohup node dist/index.js >> /dev/null 2>&1 & # HALO_PASSWORD read from env/con
52
52
 
53
53
  - Node.js ≥ 22 (better-sqlite3 native binding is bound to v22)
54
54
  - pnpm 11 (pinned by the root `packageManager`; `corepack enable` picks it up)
55
- - AWS credentials configured (`~/.aws/credentials` or env vars) with Bedrock access
56
- - bubblewrap (`sudo apt install bubblewrap`) — OS-level sandbox for non-full access levels on Linux (macOS uses the built-in Seatbelt). Without one, only app-level path validation is active and `shell_exec` is blocked for non-full sessions
55
+ - AWS credentials (`~/.aws/credentials`, env vars or an instance role) — only for the Bedrock providers; any other provider just needs its API key
56
+ - bubblewrap (`sudo apt install bubblewrap`) — OS-level sandbox for non-full access levels on Linux (macOS uses the built-in Seatbelt). Without one, only app-level path validation is active and `shell_exec` is blocked for non-full sessions. On Ubuntu 23.10+ / 24.04 AppArmor blocks it (`bwrap: setting up uid map: Permission denied`) until you add a profile — see [deploy.md](deploy.md#bubblewrap-on-ubuntu-2310-and-2404-apparmor)
57
57
 
58
58
  ### nvm PATH
59
59
 
@@ -118,7 +118,7 @@ Source: `packages/server/src/config.ts`
118
118
  | `HALO_A2A_ROOT` | the user's home | — | Base dir A2A exposes workspaces under: a workspace `<base>/A/B` with `.halo/agent-card.json` is served at `/a2a/A/B` (see [design/a2a.md](../design/a2a.md)); workspaces outside it can't be exposed |
119
119
  | `HALO_A2A_PUBLIC_URL` | (none) | `settings.yaml general.a2a.public_url` | This server's A2A origin as peers see it (no trailing slash), used for the agent card's interface URL and as the default push-webhook base. In agentcore-a2a mode: the runtime's invoke URL (`…/runtimes/<escaped ARN>/invocations`) |
120
120
  | `HALO_A2A_ACCESS` | `workspace` | — | agentcore-a2a mode only: access level of every A2A session — `workspace` \| `full` \| `readonly`; anything else is a boot error |
121
- | `HALO_DEFAULT_PROVIDER` | (none) | — | Read by `halo setup --non-interactive`: binds the built-in agents to this provider id (and its default model). An unknown id only prints a warning |
121
+ | `HALO_DEFAULT_PROVIDER` | (none) | — | Read by `halo setup --non-interactive`: binds every built-in agent to this provider id (and its default model) and, for each of its env-backed keys whose variable is set, writes the `<<NAME>>` placeholder into `settings.yaml` (Bedrock excepted). An unknown id only prints a warning |
122
122
  | `HALO_CLI` | `halo` (`halo.cmd` on Windows) | — | `halo` cli executable that cron runs and the evolution wrapper spawn — a dev override |
123
123
  | `HALO_VERSION` | `dev` | — | Version string reported by `GET /api/health` and the cli; stamped by the bundle / desktop launcher, so normally not set by hand |
124
124
  | `HALO_PROMPT` | (none) | — | `plain` makes `halo setup` use numbered-menu prompts instead of the raw-mode TUI |
@@ -15,18 +15,29 @@ Any `?token=…` value in a message is replaced with `?token=<redacted>` before
15
15
 
16
16
  ## Log locations
17
17
 
18
- One file for the whole server: `~/.halo/global/logs/server.log`. A single server process serves many workspaces, so there is no per-workspace log. Cron runs and evolution runs keep their own logs under `~/.halo/global/logs/{cron,evo}/`. `halo server start -d` additionally captures the process's stdout/stderr in `~/.halo/logs/server.log` (plus the supervisor's `[respawn]` lines) — that is the file `halo server logs` tails.
18
+ One file for the whole server: `~/.halo/global/logs/server.log`. A single server process serves many workspaces, so there is no per-workspace log. Cron runs and evolution runs keep their own logs under `~/.halo/global/logs/{cron,evo}/`. `halo server start -d` additionally captures the process's stdout/stderr in `~/.halo/logs/server.log` — via the systemd user unit's `StandardOutput=append:` on Linux with systemd, otherwise via the built-in supervisor, which adds its `[respawn]` lines — that is the file `halo server logs` tails. A system unit's stdout goes to the journal (`journalctl -u <unit>`).
19
19
 
20
20
  ## Log format
21
21
 
22
22
  ```
23
- time=2026-04-19T10:30:00.000Z level=info [Server] Hono server listening on http://localhost:9527
23
+ time=2026-04-19T10:30:00.000Z level=info [Server] Halo 1.6.3 listening on port 9527 on all network interfaces — local: http://localhost:9527
24
24
  time=2026-04-19T10:30:01.234Z level=error [WS] Chat error: ThrottlingException
25
25
  time=2026-04-19T10:30:02.567Z level=warn [SessionManager] Context overflow, auto-compacting...
26
26
  ```
27
27
 
28
28
  Every line is logfmt-flavored: `time=<ISO timestamp> level=<debug|info|warn|error> <message>`; the message keeps its `[Module]` prefix.
29
29
 
30
+ ### Always-shown lines
31
+
32
+ The startup banner bypasses the level gate: `logAlways()` (`logger.ts`) writes it straight to stderr whatever `general.logging.level` is, so a foreground `halo server start` at the default `warn` still shows that it came up. It is also written to the log file / OTel as an `info` line, but only when `info` passes the gate. The banner names the real bound address (`formatListenBanner()` in `listen-banner.ts`). The server passes no hostname, so it binds every interface, and the banner says so in two lines:
33
+
34
+ ```
35
+ [Server] Halo 1.6.3 listening on port 9527 on all network interfaces — local: http://localhost:9527
36
+ [Server] Reachable from the network: allow the port only from trusted sources, or put a TLS reverse proxy in front
37
+ ```
38
+
39
+ A specific bound address prints one line, `[Server] Halo <version> listening on http://<addr>:<port>` (IPv6 bracketed). AgentCore mode prints only the first line. Lines printed before `initLogger()` (setup / template-refresh notices, the source-run-only `monorepo root … not found` warning) aren't filtered either, and they never reach the log file.
40
+
30
41
  ## Rotation
31
42
 
32
43
  When the log file exceeds the cap:
@@ -132,11 +132,11 @@ Each session carries `accessLevel: 'readonly' | 'workspace' | null` (persisted i
132
132
 
133
133
  `view_image` is additionally gated on `capabilities.image` (see its section above). Models without vision support get the same lists minus `view_image` — so a non-vision model on `readonly` ends up with 4 tools, not 5.
134
134
 
135
- OS sandbox backend per platform — `getSandboxBackend()` in `sandbox.ts` returns `'bwrap' | 'seatbelt' | null` after the boot probe (`initBwrapCheck()`), logged at startup as `[Server] OS sandbox: …` and exposed as `sandbox` on `GET /api/health`:
135
+ OS sandbox backend per platform — `getSandboxBackend()` in `sandbox.ts` returns `'bwrap' | 'seatbelt' | null` after the boot probe (`initBwrapCheck()`), logged at startup as `[Server] OS sandbox: …` (`info` level, so hidden at the default `warn`). When the probe found bwrap missing (ENOENT) or unable to create namespaces, the server instead warns once at boot: `[Server] OS sandbox unavailable (<reason>) — workspace/readonly sessions get no shell_exec. Fix: <hint>` (`sudo apt install bubblewrap`, or the AppArmor profile in [deploy.md](deploy.md#bubblewrap-on-ubuntu-2310-and-2404-apparmor)). `halo cli` / `tui` print the same line (`[cli]` tag) only for a `--access workspace|readonly` session; the probe itself never prints (`getSandboxUnavailableReason()` / `sandboxUnavailableWarning()` in `sandbox.ts`). Also exposed as `sandbox` on `GET /api/health`:
136
136
 
137
137
  | Platform | `shell_exec` | File tools | Dependency |
138
138
  |---|---|---|---|
139
- | Linux | bwrap | bwrap | `bubblewrap` (`apt install bubblewrap`); the probe runs a real sandboxed no-op, so an install that can't create namespaces (Ubuntu 24.04 AppArmor userns restriction) counts as unavailable |
139
+ | Linux | bwrap | bwrap | `bubblewrap` (`apt install bubblewrap`); the probe runs a real sandboxed no-op, so an install that can't create namespaces (Ubuntu 23.10+ / 24.04 AppArmor userns restriction: `bwrap: setting up uid map: Permission denied`) counts as unavailable — fix in [deploy.md](deploy.md#bubblewrap-on-ubuntu-2310-and-2404-apparmor) |
140
140
  | macOS | Seatbelt (`/usr/bin/sandbox-exec -p <profile>`) | in-process, behind `assertPathAllowed` | none — ships with macOS (Apple marks it deprecated, but it's present on every release). The probe runs `sandbox-exec -p '(version 1)(allow default)' /usr/bin/true` |
141
141
  | Windows | none | none | — see below |
142
142
 
@@ -6,7 +6,7 @@ The agent is Halo's core abstraction: personality (AGENT.md) + config (agent.yam
6
6
 
7
7
  | Kind | Description |
8
8
  |---|---|
9
- | Built-in | Server-shipped agents (`default`, `executor`, `deep-executor` + the internal `goal` / `__evo_agent__` / `__score__` / `__apply_agent__`; `goal` is seeded like the rest but only usable when `general.goal_mode_enabled` is set — off by default, no UI). Live under `~/.halo/global/agents/<id>/`. **Force-overwritten on every server startup** — local edits to these files will be lost on the next start. To customize: copy into the workspace scope (workspace replaces global) and edit there. |
9
+ | Built-in | Server-shipped agents (`default`, `executor`, `deep-executor` + the internal ones such as `__evo_agent__` / `__score__` / `__apply_agent__`). Live under `~/.halo/global/agents/<id>/`. **Force-overwritten on every server startup** — local edits to these files will be lost on the next start. To customize: copy into the workspace scope (workspace replaces global) and edit there. |
10
10
  | Global | Any other agent under `~/.halo/global/agents/<id>/`, e.g. one you created via the admin UI. Shared across projects. Never overwritten by the server. |
11
11
  | Workspace | `<project>/.halo/agents/<id>/`, private to the current project; same-id workspace wins over global. Override is **whole-folder**: the workspace agent folder replaces the global one entirely, so copy *both* `agent.yaml` and `AGENT.md` when customizing — a folder with only `AGENT.md` has no model config and won't load. |
12
12
 
@@ -113,7 +113,7 @@ Top-right `Test` button:
113
113
 
114
114
  Better than the old built-in test chat — it has full workspace tools + session persistence.
115
115
 
116
- Internal agents (`internal: true`, e.g. self-evolution agents) have no Test button and never appear in the chat agent selector, the Cron agent picker or `/session new`'s default pick — they're delegated to by other agents, never driven directly. They remain editable in the management sidebar's collapsed **Internal** group. The goal-mode judge (`goal`) isn't listed there either: `/goal` starts it, and there's nothing to configure for it in the UI.
116
+ Internal agents (`internal: true`, e.g. self-evolution agents) have no Test button and never appear in the chat agent selector, the Cron agent picker or `/session new`'s default pick — they're delegated to by other agents, never driven directly. They remain editable in the management sidebar's collapsed **Internal** group.
117
117
 
118
118
  ## Delete
119
119
 
@@ -110,6 +110,8 @@ git diff HEAD~1 | halo cli -w /path/to/review-ws "review this diff"
110
110
  halo cli "check for issues" && echo "OK" || echo "FAILED"
111
111
  ```
112
112
 
113
+ When stdin is not a TTY, piped input is appended after the positional prompt. With a positional prompt, stdin is only read if its first data arrives within 0.5 s — then to EOF, however long it takes. Otherwise it's ignored, so an open-but-idle stdin (scripts, CI, cron, `ssh host 'halo cli …'` without `-t`) no longer hangs the run. A producer that takes longer than that to print anything gets ignored too; capture it first (`out=$(slow-cmd); halo cli "summarize" <<<"$out"`). Without a positional prompt, stdin *is* the prompt: `halo cli` waits for EOF, and after 1 s of silence prints one `waiting for the prompt on stdin` hint to stderr.
114
+
113
115
  ## TUI Mode (interactive)
114
116
 
115
117
  ```bash
@@ -233,13 +235,22 @@ Normally run for you by `/extension models`, which downloads the hub's newest `m
233
235
  ## Server Mode (background daemon)
234
236
 
235
237
  `halo server start|stop|restart|status|logs` manages a local server from the
236
- same binary. `start -d` runs it in the background **with a bounded respawn
237
- supervisor**: the detached process supervises the server and brings it back on
238
- a crash (non-zero exit, or a kill by anything other than SIGTERM/SIGINT), at
239
- most 5 times per 5 minutes with a 2s pause, logging each decision as
240
- `[respawn] …` into `~/.halo/logs/server.log`. `halo server stop` is
241
- recognized as intentional and never respawns; `halo server stop|restart|status`
242
- still target the server's own pid. Full policy in
238
+ same binary. On Linux with systemd, `start -d` (and `restart`) runs it as the
239
+ user unit `~/.config/systemd/user/halo.service`, written and enabled for you
240
+ with linger on, so it starts at boot and comes back 5s after a crash. Your
241
+ shell's environment is saved to `~/.halo/secrets/server.env` for it. A system
242
+ unit that already runs `halo server start` as you is started instead.
243
+ `stop|restart|status` on a unit-run server go through `systemctl`, and print
244
+ the `sudo systemctl …` line when a system unit needs it. See
245
+ [dev/deploy.md](../dev/deploy.md#option-c-systemd-for-the-npm-install).
246
+
247
+ Elsewhere (macOS, Windows, Linux without systemd, a redirected HOME, an
248
+ unreachable user manager) `start -d` runs it in the background **with a bounded
249
+ respawn supervisor**: the detached process supervises the server and brings it
250
+ back on a crash (non-zero exit, or a kill by anything other than
251
+ SIGTERM/SIGINT), at most 5 times per 5 minutes with a 2s pause, logging each
252
+ decision as `[respawn] …` into `~/.halo/logs/server.log`. `halo server stop` is
253
+ recognized as intentional and never respawns. Full policy in
243
254
  [dev/deploy.md](../dev/deploy.md#crash-semantics--why-restarton-failure-is-load-bearing).
244
255
 
245
256
  ## AgentCore runtime (`halo agentcore`)
@@ -274,7 +285,8 @@ packages/cli/
274
285
  src/
275
286
  index.ts — Entry: subcommand dispatch (tui/cli/agents/sessions/server/setup/upgrade/acp), SIGINT/SIGTERM handling
276
287
  harness.ts — Shared agent harness wrapping SessionManager
277
- server-supervisor.ts — Respawn policy for `halo server start -d`
288
+ server-supervisor.ts — Respawn policy for `halo server start -d` off systemd
289
+ server-systemd.ts — Linux systemd path: user unit + env snapshot, unit routing for stop/restart/status
278
290
  cli.ts — Non-interactive: stdin/args → agent → stdout → exit
279
291
  tui.tsx — Interactive entry: mounts the ink app
280
292
  tui/
@@ -2,6 +2,8 @@
2
2
 
3
3
  What to do the first time you open Halo.
4
4
 
5
+ Not installed yet? The [README Install section](../../../README.md#install) covers the three things a fresh box trips on: Node 22 first (Ubuntu 24.04's own `nodejs` is 18 and won't work), `sudo npm install -g` with a system-wide Node, and keeping **Configure…** (the default) at `halo setup`'s key step before binding the default agent.
6
+
5
7
  ## Log in
6
8
 
7
9
  Open `http://localhost:9527` (or your deployment URL) in a browser and enter the password.
@@ -14,6 +16,8 @@ Open `http://localhost:9527` (or your deployment URL) in a browser and enter the
14
16
 
15
17
  After login, you land in the workspace. A URL with `?folder=/abs/path` opens that directory directly; without `?folder`, Halo reopens the last workspace you used, or your home directory if there isn't one — everything still works.
16
18
 
19
+ `halo cli` / `halo tui` use the current directory as the workspace, so running them in `$HOME` makes `~/.halo` itself a workspace (`halo.db`, `sessions/`, `memory/`, `canvas/` appear next to `global/` and `secrets/`). `cd` into a project, or pass `-w <dir>`, to keep them apart.
20
+
17
21
  The Explorer top bar has two ways to switch workspace:
18
22
  - **Type an absolute path** + Enter — invalid paths raise an error
19
23
  - **Click the 📁🔍 button** — opens a VS Code-style directory picker: single click to select, double click to enter, ArrowUp to go up, 🏠 for home, paste paths into the top input
@@ -65,21 +69,25 @@ After `halo setup`:
65
69
  ```
66
70
  ~/.halo/
67
71
  ├── global/ ← server-managed, see overwrite policy below
72
+ │ ├── .template-version ← seed version stamp, compared on every start
68
73
  │ ├── INSTRUCTIONS.md ← refreshed on upgrade
69
74
  │ ├── prompts/{bootstrap,all,root}/ ← refreshed on upgrade
70
75
  │ ├── models/<provider>.yaml ← refreshed on upgrade
71
76
  │ ├── docs/ ← refreshed on upgrade (bundled platform docs)
72
77
  │ ├── agents/ ← built-in ids refreshed; user-added ones untouched
73
- │ └── skills/ ← built-in ids refreshed; user-added ones untouched
74
- └── secrets/ ← never overwritten after first creation
78
+ │ ├── skills/ ← built-in ids refreshed; user-added ones untouched
79
+ │ ├── aliases.yaml ← command aliases, seeded once, never overwritten
80
+ │ └── extensions/ ← preview extensions, created empty
81
+ └── secrets/ ← mode 700, files 600; never overwritten after first creation
75
82
  ├── config.yaml ← leaf-merged: new server knobs added, existing values kept
76
- ├── settings.yaml ← created empty if missing; otherwise untouched
77
- └── channels/channels.db ← per-channel account state
83
+ └── settings.yaml ← created empty if missing; otherwise untouched
78
84
  ```
79
85
 
86
+ The first `halo server start` adds the runtime state: `global/{a2a,cron,evo,runs}.db`, `global/logs/`, `global/models.d/`, `global/server.lock`, `global/server.stop` (after a stop), and `secrets/channels/channels.db` (per-channel account state).
87
+
80
88
  **When does "refreshed on upgrade" actually run?** `halo setup` always re-runs the seed; the server's startup check — and the same check at the start of `halo cli` / `halo tui` — also re-runs it automatically when `~/.halo/global/.template-version` is behind the bundled `TEMPLATE_VERSION`. So the routine flow `halo upgrade && halo server restart` is enough — no need to remember `halo setup` — and a CLI-only user just runs `halo cli` / `halo tui` after `halo upgrade`.
81
89
 
82
- **Server-refreshed**: `INSTRUCTIONS.md`, `prompts/`, `models/`, `docs/`, the built-in agent ids (`default`, `executor`, `deep-executor`, `goal`, `__evo_agent__`, `__score__`, `__apply_agent__`), and the built-in skill ids (`agent`, `skill`, `workspace`, `cron`, `send-file`, `self`, `aws-knowledge`, `web-search`, `halo`, `extension`). To customize one, copy it into the workspace scope (`<project>/.halo/...`) — workspace replaces global at runtime.
90
+ **Server-refreshed**: `INSTRUCTIONS.md`, `prompts/`, `models/`, `docs/`, the built-in agent ids (`default`, `executor`, `deep-executor` plus the internal agents), and the built-in skill ids (`agent`, `skill`, `workspace`, `cron`, `send-file`, `self`, `aws-knowledge`, `web-search`, `halo`, `extension`). To customize one, copy it into the workspace scope (`<project>/.halo/...`) — workspace replaces global at runtime.
83
91
 
84
92
  **Never overwritten**: anything else under `agents/` or `skills/` (your own creations), and everything under `secrets/`.
85
93
 
@@ -64,6 +64,8 @@ VS Code style:
64
64
 
65
65
  Right-click menu: New File / New Folder / Open in Integrated Terminal / Reveal in File Manager (desktop app only) / Open to the Side / Download / Rename / Delete / Open as Workspace (folders).
66
66
 
67
+ **Download a folder**: right-click a folder (or the empty area for the whole workspace) → Download. A confirm shows the approximate size and file count; `node_modules`, `.git` and other build/cache folders are left out unless you tick the checkbox, and `.env` files, secrets (plus common credential stores: ~/.kube, cloud CLI configs, shell histories) and Halo's session data are always left out. You get one `<folder>.zip`; anything left out is listed in `_halo-excluded.txt` inside it. Folders with more than 200,000 files can't be downloaded in one go — pick a subfolder.
68
+
67
69
  **Quick Open**: `Cmd/Ctrl+P` opens fuzzy file search — server-side, full-project, independent of expansion state.
68
70
 
69
71
  ### Skipped directories
@@ -93,6 +93,6 @@ API: `GET/PUT /api/agent-configs/:id/md/:fileType`, `GET /api/agent-configs/:id/
93
93
  - **Not rendered for internal agents** (`internal: true`) — they're delegated to by other agents, never driven directly
94
94
 
95
95
  ### Internal agents
96
- Agents flagged `internal: true` in agent.yaml (e.g. `__evo_agent__`, `__apply_agent__`, `__score__`, `goal`) are platform tooling. They are hidden from every user-facing surface: the delegation roster, `/session new` default pick, the chat agent selector, the Cron job agent picker and `halo cli`'s agent list (`halo agents`) — and have no Test button. They stay editable in the management sidebar's collapsed **Internal** group — except the goal-mode judge (`goal`), which is only ever started by `/goal` and isn't listed on the Agents page at all (edit its `agent.yaml` on disk if needed).
96
+ Agents flagged `internal: true` in agent.yaml (e.g. `__evo_agent__`, `__apply_agent__`, `__score__`) are platform tooling. They are hidden from every user-facing surface: the delegation roster, `/session new` default pick, the chat agent selector, the Cron job agent picker and `halo cli`'s agent list (`halo agents`) — and have no Test button. They stay editable in the management sidebar's collapsed **Internal** group.
97
97
 
98
98
  Replaces the old built-in test chat panel, giving a more realistic environment (full workspace tools + session persistence).
@@ -66,7 +66,7 @@ VS Code-style highlight selection (no checkboxes):
66
66
 
67
67
  ### Right-click menu
68
68
 
69
- Items shown depend on the click target. With several items selected the menu offers only Delete; on the workspace root row only New File / New Folder / Open in Integrated Terminal / Reveal in File Manager apply:
69
+ Items shown depend on the click target. With several items selected the menu offers only Delete; on the workspace root row only New File / New Folder / Open in Integrated Terminal / Reveal in File Manager / Download apply:
70
70
 
71
71
  | Action | Shown when | API / behavior |
72
72
  |---|---|---|
@@ -74,13 +74,25 @@ Items shown depend on the click target. With several items selected the menu off
74
74
  | Open in Integrated Terminal | Always | Spawns a terminal at the target dir (or file's parent) |
75
75
  | Reveal in File Manager | Desktop app only | Opens the OS file manager (Finder / Explorer / Linux FM) at the target — a folder opens itself, a file is highlighted in its parent dir. Hidden in a plain browser (gated on the `window.haloReveal` IPC bridge exposed by the desktop preload) |
76
76
  | Open to the Side | File or bundle directory | Splits the editor and opens the file (or bundle) in the right pane |
77
- | Download | File only | `GET /api/files/download?path=...` |
77
+ | Download | File, folder, workspace root | File: `GET /api/files/download?path=...`. Folder / root: estimate + confirm, then one streamed zip — see [Folder download](#folder-download) |
78
78
  | Rename | Single file/folder | `POST /api/files/rename` |
79
79
  | Delete | Single or multi-select | `DELETE /api/files?path=...` (with confirm) |
80
80
  | Open as Workspace | Folder only | Switches the active workspace to that folder, reusing the path-input switch flow (validate → persist → reload). Shown at the bottom of the menu |
81
81
 
82
82
  The menu auto-clamps to the viewport, so right-clicking near the window edge does not clip the bottom items.
83
83
 
84
+ ### Folder download
85
+ Download on a folder (or the workspace root) zips it on the server while it streams — nothing is staged on disk or in memory (`GET /api/files/download-folder`, see [dev/api.md](../dev/api.md#files)).
86
+ - Clicking Download first fetches an estimate (`estimate=1`, a walk without reading contents) and opens a confirm: `≈ <size> · <n> files`, what is excluded by default with counts (node_modules / .git by name, the rest as "other build/cache folders"), how many items are always excluded or skipped
87
+ - Checkbox "Include node_modules, .git and other build/cache folders" (`include=excluded`) re-fetches the estimate when toggled
88
+ - More than 200,000 files, or a scan longer than 10 s → the dialog shows the server's 413 message ("download a subfolder instead") and Download stays disabled
89
+ - Confirm → a plain `<a href>` download of `<folder name>.zip` (workspace root: the workspace dir name); the browser / desktop default save flow takes it from there
90
+ - **Excluded by default**: any folder whose name is in the file watcher's ignore list (`IGNORED_SEGMENTS`: `.git`, `node_modules`, `dist`, `.venv`, `__pycache__`, caches …)
91
+ - **Always excluded**, checkbox or not: the sandbox hidden host paths (`~/.halo/secrets`, `~/.ssh`, `~/.aws` … — a home dir can be a workspace), workspace runtime state (`.halo/sessions`, logs, evo, the dbs — also in a nested workspace's `.halo/`), `.halo/settings.yaml`, any `.env` / `.env.*` file, plus common credential stores (~/.kube, cloud CLI configs, shell histories)
92
+ - **Names a zip can't hold** (containing `\`, or a top-level `C:`-style prefix) are skipped and listed as unreadable
93
+ - **Symlinks**: folder links are never followed; a file link is included only when its target stays inside the workspace and is not excluded
94
+ - When anything was left out, the zip carries `_halo-excluded.txt` at its root: excluded paths grouped by reason (a pruned folder is listed once, not its contents; capped at 1000 per reason plus a count), including unreadable files and skipped links
95
+
84
96
  ### Modification indicator
85
97
  Files with unsaved editor edits show a coloured dot in the tree (synced from `editorStore.modifiedPaths`).
86
98
 
@@ -102,7 +102,7 @@ An installed canvas extension declares its own config in its manifest: `"setting
102
102
 
103
103
  ### General — built-in
104
104
 
105
- Declared in [packages/server/src/settings-schema.ts](../../../packages/server/src/settings-schema.ts) `generalSection()`. The server itself is the implicit declarer. Keys: `language`, `theme`, `agent.max_retries`, `server.*`, `session.*`, `compact.*`, `sandbox.*`, `logging.*`, `observability.*` (read once at boot — restart to apply), `evolution.*`, `limits.*`. All `general.*` keys are `globalOnly`: `config.ts` resolves them through `settingsValue()` against `~/.halo/secrets/settings.yaml` only, so a workspace `settings.yaml` cannot override them. Per-workspace layering applies to namespaced `params` / `secrets` (`getServerSecret(ns, key, workspaceRoot)`, `substituteSecrets`).
105
+ Declared in [packages/server/src/settings-schema.ts](../../../packages/server/src/settings-schema.ts) `generalSection()`. The server itself is the implicit declarer. Keys: `language`, `theme`, `agent.max_retries`, `server.*`, `session.*`, `compact.*`, `sandbox.*`, `logging.*`, `observability.*` (read once at boot — restart the server to apply; cron / evolution runs pick a change up on their next run), `evolution.*`, `limits.*`. All `general.*` keys are `globalOnly`: `config.ts` resolves them through `settingsValue()` against `~/.halo/secrets/settings.yaml` only, so a workspace `settings.yaml` cannot override them. Per-workspace layering applies to namespaced `params` / `secrets` (`getServerSecret(ns, key, workspaceRoot)`, `substituteSecrets`).
106
106
 
107
107
  `server.trust_proxy` (boolean, default `false`, `globalOnly`): whether the brute-force rate limiter (`middleware/brute-force.ts` `getClientIp`) trusts the `x-forwarded-for` header for client IP resolution. Direct-connect deployments leave it `false` and get the socket address. Behind a reverse proxy (nginx / Cloudflare / etc.), set it to `true` so the real client IP is honored instead of the proxy's — but only when that proxy is one you control and rewrites the header itself, otherwise a client can forge XFF to dodge lockouts.
108
108
 
@@ -123,7 +123,7 @@ There is no "default provider" setting: a newly created agent's `model:` is a co
123
123
 
124
124
  **Advanced fold**: on the General page, `language` and `theme` are shown in the open; every other General field is in an "Advanced" area, collapsed by default (schema flag `advanced: true`, set in `generalSection()`).
125
125
 
126
- **Restart-required fields**: `agent.max_retries`, `session.max_queue_size`, `session.max_nesting_depth`, `compact.keep_messages`, `compact.max_summary_input`, `compact.max_message_slice`, `logging.level` and the four `observability.*` keys are read once at server start (object literals in `config.ts`; observability is mapped onto `OTEL_*` env by `initObservability()` at boot). They carry `restartRequired: true`; after a save or Reset of one of them the Settings page shows a notice above the section listing the saved keys and saying the Halo server must be restarted (dismissable; it stays until dismissed). Every other General key is read live.
126
+ **Restart-required fields**: `agent.max_retries`, `session.max_queue_size`, `session.max_nesting_depth`, `compact.keep_messages`, `compact.max_summary_input`, `compact.max_message_slice`, `logging.level` and the four `observability.*` keys are read once at server start (object literals in `config.ts`; observability is mapped onto `OTEL_*` env by `initObservability()` at boot). For observability that applies to the server's own exporter only: cron / evolution runs are `halo cli` children that don't inherit those settings-derived `OTEL_*` vars (`cleanChildEnv()` strips them), so they use a new value on their next run — as the field descriptions say. They carry `restartRequired: true`; after a save or Reset of one of them the Settings page shows a notice above the section listing the saved keys and saying the Halo server must be restarted (dismissable; it stays until dismissed). Every other General key is read live.
127
127
 
128
128
  ### Field attributes
129
129
 
@@ -48,15 +48,15 @@ The Skills sidebar mirrors the Agents sidebar layout:
48
48
 
49
49
  ### Auto-sync behaviour (workspace vs. global)
50
50
 
51
- The Skills sidebar and any open mini-workspace editor stay in sync with the file system via `file:changed` WS events — but the server's watcher is scoped to the current workspace, so the guarantees differ by scope:
51
+ Every skill list — the chat's slash-command popup, the Skills sidebar, the agent form's skill picker — refetches on the `skills:changed` WS frame (→ `skill-bus` bump), in every open admin tab. The server sends it after every real mutation: REST create / delete / toggle and `/skill disable|enable|delete` from any chat or channel (a no-op toggle sends nothing). A workspace change reaches only that workspace's tabs; a global one reaches all. File-system edits differ by scope, because the server's watcher covers only the current workspace:
52
52
 
53
53
  | Change | Workspace skills | Global skills |
54
54
  |---|---|---|
55
- | Create / delete a skill dir | Live (watcher emits add/unlink → `skill-bus` bump → sidebar refetches) | Refreshed opportunistically on **window focus** |
55
+ | Via Skills tab / REST / `/skill` command | Live | Live |
56
+ | Create / delete a skill dir or edit SKILL.md frontmatter on disk (agent `file_write`, CLI / SSH) | Live (watcher → 300 ms debounce → `skills:changed` only when the folder ids / frontmatter changed) | Refreshed opportunistically on **window focus** while the Skills page is open |
56
57
  | Edit a file inside a skill while the mini-workspace editor has it open | Live (EditorPanel translates workspace-rel path into its own tab.path and refetches) | Live for the currently-open tab (same code path) — but the file tree change won't show new/removed sibling files until focus refresh |
57
- | Edit from external CLI / SSH | Same as above | Same as above |
58
58
 
59
- So when an agent (or the `skill` skill) creates a **global** skill, the Skills sidebar won't reflect it until the user refocuses the browser window. The `skill` skill documents this so agents can warn the user.
59
+ So when an agent writes a **global** skill folder directly, the lists won't reflect it until the user refocuses the browser window on the Skills page, or switches session / agent. The `skill` skill documents this so agents can warn the user.
60
60
 
61
61
  Extension skills follow the extension, not a file watcher. Installing or uninstalling an extension broadcasts `extension:changed`, which bumps `skill-bus`, so the sidebar and the agent form's skill picker refetch live. Edits inside an installed extension's `skills/` push nothing (show on focus refresh); reinstalling the extension is how a change ships.
62
62