@turmind/halo 2.2.2 → 2.2.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 (42) hide show
  1. package/README.md +2 -1
  2. package/admin-out/404.html +1 -1
  3. package/admin-out/_next/static/chunks/1989-9d30cdb1a86e4beb.js +1 -0
  4. package/admin-out/_next/static/chunks/4362.115405260299210d.js +1 -0
  5. package/admin-out/_next/static/chunks/{7613.fe24922a6a839656.js → 7073.4081ed209e6748bd.js} +1 -1
  6. package/admin-out/_next/static/chunks/{7073.9d5f9dd7a77827b7.js → 7613.9c1657bb3b52d1b5.js} +1 -1
  7. package/admin-out/_next/static/chunks/app/{page-10a4afac9cbc27a2.js → page-c9d174a5432b0007.js} +1 -1
  8. package/admin-out/_next/static/chunks/{webpack-9532ec7f1fa9a757.js → webpack-1970c5140e4019b7.js} +1 -1
  9. package/admin-out/_next/static/css/87d4fc049f865389.css +1 -0
  10. package/admin-out/index.html +1 -1
  11. package/admin-out/index.txt +5 -5
  12. package/bundled-docs/design/a2a.md +3 -1
  13. package/bundled-docs/design/architecture.md +1 -1
  14. package/bundled-docs/design/cron.md +1 -1
  15. package/bundled-docs/design/evolution.md +6 -6
  16. package/bundled-docs/design/node.md +18 -39
  17. package/bundled-docs/design/observability.md +2 -2
  18. package/bundled-docs/design/relay.md +1 -1
  19. package/bundled-docs/design/session.md +3 -3
  20. package/bundled-docs/design/storage.md +5 -2
  21. package/bundled-docs/design/stt-local.md +2 -2
  22. package/bundled-docs/design/ws.md +3 -3
  23. package/bundled-docs/dev/api.md +10 -9
  24. package/bundled-docs/dev/deploy.md +1 -1
  25. package/bundled-docs/dev/env.md +2 -1
  26. package/bundled-docs/dev/tools.md +10 -6
  27. package/bundled-docs/guide/README.md +1 -1
  28. package/bundled-docs/guide/cli.md +4 -31
  29. package/bundled-docs/guide/node.md +57 -36
  30. package/bundled-docs/requirements/command.md +1 -1
  31. package/bundled-docs/requirements/settings.md +39 -2
  32. package/dist/evo-wrapper.js +368 -268
  33. package/dist/index.js +2330 -2531
  34. package/package.json +2 -1
  35. package/templates/agents/default/agent.yaml +1 -1
  36. package/templates/models/aws-bedrock-openai.yaml +81 -7
  37. package/templates/skills/cron/SKILL.md +1 -3
  38. package/admin-out/_next/static/chunks/1989-3b2faa2731d973c4.js +0 -1
  39. package/admin-out/_next/static/chunks/4362.9d2ee4f9b6facc49.js +0 -1
  40. package/admin-out/_next/static/css/8fc43fce83e5ce5d.css +0 -1
  41. /package/admin-out/_next/static/{u9pg60rITCj73MBYeXsW1 → vf5W8US5y8B2f1E6zJJw3}/_buildManifest.js +0 -0
  42. /package/admin-out/_next/static/{u9pg60rITCj73MBYeXsW1 → vf5W8US5y8B2f1E6zJJw3}/_ssgManifest.js +0 -0
@@ -84,7 +84,7 @@ To list agents or sessions without running a prompt, use the dedicated subcomman
84
84
  ```
85
85
  [ 17:23:02 in 0.0K out 0.1K ctx 5.8K read 5.3K write 0.3K cache 94% e2e 3.3s think medium claude-sonnet-4-6 ]
86
86
  ```
87
- - **json format**: Single JSON object on stdout after completion (`text` has the same final-answer semantics as text format; `notices` holds the same root system notices as an array of strings, without the `ℹ️ ` prefix):
87
+ - **json format**: Single JSON object on stdout after completion (`text` has the same final-answer semantics as text format; `notices` holds the same root system notices as an array of strings, without the `ℹ️ ` prefix; `error` is `null` on success, else the reason — an error event's text, or the root's last-turn error such as a model refusal):
88
88
 
89
89
  ```json
90
90
  {
@@ -106,10 +106,12 @@ halo cli -w /path/to/review-ws "review the diff" --format json | jq '.text'
106
106
  # Pipe content in
107
107
  git diff HEAD~1 | halo cli -w /path/to/review-ws "review this diff"
108
108
 
109
- # Exit code: 0 = success, 1 = error, 130 = SIGINT, 143 = SIGTERM (graceful: session repaired + flushed before exit)
109
+ # Exit code: 0 = success, 1 = error (including a model refusal on the root's last turn — Anthropic's refusal or an OpenAI-format provider's content-filter block), 130 = SIGINT, 143 = SIGTERM (graceful: session repaired + flushed before exit)
110
110
  halo cli "check for issues" && echo "OK" || echo "FAILED"
111
111
  ```
112
112
 
113
+ A refusal on a sub-agent's turn does not change the exit code — it reaches the root as an aborted report, and the run exits 0 if the root then wraps up normally. A later root turn that ends normally clears an earlier refusal.
114
+
113
115
  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
116
 
115
117
  ## TUI Mode (interactive)
@@ -308,35 +310,6 @@ the caller-side `auth: sigv4` remote, session dirs and ops notes:
308
310
  [design/agentcore.md](../design/agentcore.md). The hub skill `agentcore-deploy`
309
311
  builds the image and creates the runtime for you.
310
312
 
311
- ## Linked machine (`halo node`)
312
-
313
- ```bash
314
- HALO_NODE_TOKEN=<token> halo node --server https://halo.example.com --root ~/proj [--allow-shell]
315
- ```
316
-
317
- Links this machine to a workspace on a halo server, so that workspace's
318
- full-access agents can work on files here with the `node_*` tools. Runs in the
319
- foreground, reconnects on its own, Ctrl-C to stop. Needs no `halo setup` and
320
- no model keys — nothing is written under `~/.halo/`. Create the token in
321
- Admin → Channels → Linked machines.
322
-
323
- On Linux (x86_64 / arm64 / armv7) the standalone `halo-node` binary from the
324
- GitHub release does the same job with the same flags and doesn't need Node.js
325
- (`curl -fsSL …/install.sh | sh` installs it); `halo node` there prints a
326
- one-line `[Node] tip:` with that command. See
327
- [node.md](node.md#2-run-it-on-the-machine).
328
-
329
- | Flag / env | Meaning |
330
- |---|---|
331
- | `--server <url>` | Server address, `http(s)://host:port` (`ws(s)://` also accepted) — required |
332
- | `--token <t>` / `HALO_NODE_TOKEN` | Pairing token — required; prefer the env var (flags show up in `ps`) |
333
- | `--root <dir>` | Folder to share; every path must stay inside it. Omit (or `/`) for whole-machine mode: any path this user can reach, relative paths from the current dir |
334
- | `--allow-shell` | Let agents run commands (`node_run`) as this user, not sandboxed. Off by default |
335
- | `--header "Name: value"` | Repeatable. Extra header on the connection, for an auth proxy in front of the server |
336
-
337
- Exits 1 on a rejected / revoked token, a deleted workspace or a protocol
338
- mismatch; retries everything else. Full guide: [node.md](node.md).
339
-
340
313
  ## Architecture
341
314
 
342
315
  ```
@@ -1,6 +1,6 @@
1
- # Linked machines (`halo node`)
1
+ # Linked machines (`halo-node`)
2
2
 
3
- Let a workspace's agents work on files on **your own computer** — a laptop, a desktop, a box behind NAT — while the agents themselves keep running on the halo server. You run `halo node` on that machine; it dials out to the server (no port to open), and the workspace's agents get `node_*` tools that read, write, search and (if you allow it) run commands there.
3
+ Let a workspace's agents work on files on **your own computer** — a laptop, a desktop, a box behind NAT — while the agents themselves keep running on the halo server. You run `halo-node` on that machine; it dials out to the server (no port to open), and the workspace's agents get `node_*` tools that read, write, search and (if you allow it) run commands there.
4
4
 
5
5
  ```
6
6
  [your laptop] [server]
@@ -8,24 +8,21 @@ halo-node --root ~/proj ──WebSocket──────▶ halo server ──
8
8
  (files, optional shell) node_read / node_write / … / node_run
9
9
  ```
10
10
 
11
- Two clients do the same job, with the same flags and the same behaviour:
11
+ The machine runs `halo-node`, a single binary attached to every GitHub release for **Linux (x86_64 / arm64 / armv7)**, **macOS (Apple silicon / Intel)** and **Windows (amd64)**. It needs no Node.js, no `halo setup` and no model keys, and writes nothing under `~/.halo/`. The Linux build is ~7 MB and idles at about 9 MB of memory.
12
12
 
13
- - **Linux (x86_64 / arm64 / armv7) and Windows (amd64)**: `halo-node`, a single binary attached to every GitHub release. It needs no Node.js. The Linux build is ~7 MB and idles at about 9 MB of memory.
14
- - **macOS** (or a Linux / Windows machine that already has the npm package): `halo node` from `npm install -g @turmind/halo`.
13
+ Supported: Linux and Windows run vet and the tests in CI (Linux arm64 / armv7 under qemu). The macOS builds are only cross-compiled — not tested on a Mac; on macOS, file errors from the agent's tools can read `Unknown system error -<n>` instead of the usual `ENOENT: no such file or directory` text. Windows `node_run` commands go to `cmd.exe`, with UTF-8 output and a GBK fallback.
15
14
 
16
- Neither needs `halo setup` or model keys, and neither writes anything under `~/.halo/`.
17
-
18
- Supported: **Linux, macOS and Windows amd64**. The Go client is tested natively on Linux and Windows CI; macOS uses npm. Windows `node_run` commands go to `cmd.exe`, with UTF-8 output and a GBK fallback.
15
+ > `halo node` (the npm client from `npm install -g @turmind/halo`) was removed in 2.2.4; use `halo-node`. Same flags, same token.
19
16
 
20
17
  ## 1. Create a token
21
18
 
22
- Admin → **Channels → Linked machines** → **Add machine**. Pick the workspace and a machine name (`a-z 0-9 -`, e.g. `mbp`; unique within the workspace — the agent uses it to address the machine). The dialog shows two commands to copy: one installs `halo-node` on Linux and runs it with `~/.local/bin` prepended to `PATH` (where a non-root install lands), the other runs the npm `halo node` client. On Windows, use the standalone exe below instead. The install link points at the server's own release, so the machine and the server come from the same version.
19
+ Admin → **Channels → Linked machines** → **Add machine**. Pick the workspace and a machine name (`a-z 0-9 -`, e.g. `mbp`; unique within the workspace — the agent uses it to address the machine). The dialog shows two commands to copy: **Linux / macOS** installs `halo-node` with the install script and runs it with `~/.local/bin` prepended to `PATH` (where a non-root install lands); **Windows** downloads `halo-node-windows-amd64.exe` in PowerShell and runs it. The download link points at the server's own release, so the machine and the server come from the same version.
23
20
 
24
21
  The token stays on the list row (copy button). Deleting the row revokes it at once and disconnects the machine.
25
22
 
26
23
  ## 2. Run it on the machine
27
24
 
28
- **Linux** — install once with the release's install script, then run:
25
+ **Linux / macOS** — install once with the release's install script, then run:
29
26
 
30
27
  ```bash
31
28
  curl -fsSL https://github.com/turmind/halo-agent/releases/latest/download/install.sh | sh
@@ -36,7 +33,7 @@ halo-node --server https://halo.example.com --root ~/proj
36
33
 
37
34
  What the script does (`sh`, busybox included; curl or wget):
38
35
 
39
- - picks `halo-node-linux-{amd64,arm64,armv7}` from `uname -m` (`armv8l`, a 64-bit kernel with 32-bit userland, gets armv7). Any other architecture, or not Linux, stops with a message.
36
+ - picks the binary from `uname -s` and `uname -m`: on Linux `halo-node-linux-{amd64,arm64,armv7}` (`armv8l`, a 64-bit kernel with 32-bit userland, gets armv7); on macOS (`Darwin`) `halo-node-darwin-arm64` for `arm64` (Apple silicon) and `halo-node-darwin-amd64` for `x86_64` (Intel). Any other architecture or OS stops with a message.
40
37
  - downloads that release's binary and checks it against the release's `SHA256SUMS` (`sha256sum` or `shasum`). A mismatch stops with an error and leaves nothing behind. With neither tool installed it warns and skips the check.
41
38
  - installs to `/usr/local/bin/halo-node` when run as root, otherwise to `~/.local/bin/halo-node`. If that directory isn't in `PATH`, it says so. Debian, Ubuntu and Raspberry Pi OS add `~/.local/bin` to `PATH` only at a login after it exists, which is why the commands above (and the admin's) prepend it.
42
39
  - runs the downloaded binary's `--version` before replacing anything; a download that doesn't run stops with an error.
@@ -48,16 +45,19 @@ Each release's `install.sh` installs that release's binary. The admin's command
48
45
  **Manual download** — if you'd rather not pipe a script into `sh`, have only wget (`wget -qO- <url>/install.sh | sh` also works), or the machine can't reach GitHub (download on another machine, copy it over):
49
46
 
50
47
  ```bash
51
- A=$(uname -m); case "$A" in x86_64) A=amd64;; aarch64|arm64) A=arm64;; armv7l|armv8l) A=armv7;; *) echo "no halo-node build for $A — use npm" >&2;; esac
52
- curl -fLo halo-node "https://github.com/turmind/halo-agent/releases/latest/download/halo-node-linux-$A" && chmod +x halo-node
53
- # optional: verify
54
- curl -fsL https://github.com/turmind/halo-agent/releases/latest/download/SHA256SUMS | grep "halo-node-linux-$A\$" | sed "s/halo-node-linux-$A/halo-node/" | sha256sum -c
48
+ O=$(uname -s | tr A-Z a-z) # linux | darwin
49
+ A=$(uname -m); case "$A" in x86_64) A=amd64;; aarch64|arm64) A=arm64;; armv7l|armv8l) A=armv7;; *) echo "no halo-node build for $A" >&2;; esac
50
+ curl -fLo halo-node "https://github.com/turmind/halo-agent/releases/latest/download/halo-node-$O-$A" && chmod +x halo-node
51
+ # optional: verify (macOS: shasum -a 256 -c instead of sha256sum -c)
52
+ curl -fsL https://github.com/turmind/halo-agent/releases/latest/download/SHA256SUMS | grep "halo-node-$O-$A\$" | sed "s/halo-node-$O-$A/halo-node/" | sha256sum -c
55
53
  ./halo-node --server https://halo.example.com --root ~/proj
56
54
  ```
57
55
 
56
+ On macOS, a binary downloaded in a browser is quarantined and Gatekeeper blocks it (the builds aren't notarized); clear that once before running it: `xattr -d com.apple.quarantine ./halo-node-darwin-*` (or whatever you renamed it to). `curl` / `wget` downloads — including `install.sh` — aren't quarantined.
57
+
58
58
  ### Windows (amd64)
59
59
 
60
- Download `halo-node-windows-amd64.exe` from the [GitHub release](https://github.com/turmind/halo-agent/releases/latest) matching your server (no npm or Node.js needed). Put it in e.g. `C:\halo-node`. In PowerShell:
60
+ Download `halo-node-windows-amd64.exe` from the [GitHub release](https://github.com/turmind/halo-agent/releases/latest) matching your server (no Node.js needed; the admin's command does it with `Invoke-WebRequest` into the current folder). Put it in e.g. `C:\halo-node`. In PowerShell:
61
61
 
62
62
  ```powershell
63
63
  $env:HALO_NODE_TOKEN = '<token>'
@@ -66,30 +66,35 @@ $env:HALO_NODE_TOKEN = '<token>'
66
66
 
67
67
  The release's `SHA256SUMS` includes the exe; compare its entry with `Get-FileHash C:\halo-node\halo-node-windows-amd64.exe -Algorithm SHA256`. Use either `/` or `\` in paths; `~/` expands from `USERPROFILE`. Folder mode resolves symlinks and junctions before checking the root. Add `--allow-shell` only if you want commands as this Windows user. Stop the process before replacing the exe during an upgrade.
68
68
 
69
- **macOS (npm)**:
69
+ It runs in the foreground and logs each call (`[Node] read src/app.ts`, `[Node] refused write ../x: outside root`). Ctrl-C stops it. It reconnects by itself after network drops or a server restart; it exits (code 1) on a bad / revoked token or when the workspace is gone.
70
70
 
71
- ```bash
72
- npm install -g @turmind/halo # once
73
- export HALO_NODE_TOKEN=<token>
74
- halo node --server https://halo.example.com --root ~/proj
75
- ```
71
+ The admin row turns green (live, no refresh) and shows `folder · shell on/off · os`.
76
72
 
77
- On Linux, `halo node` still works, and prints one `[Node] tip:` line with the install command.
73
+ ### Flags
78
74
 
79
- Either one runs in the foreground and logs each call (`[Node] read src/app.ts`, `[Node] refused write ../x: outside root`). Ctrl-C stops it. It reconnects by itself after network drops or a server restart; it exits (code 1) on a bad / revoked token or when the workspace is gone.
75
+ | Flag / env | Meaning |
76
+ |---|---|
77
+ | `--server <url>` | Server address, `http(s)://host:port` (`ws(s)://` also accepted) — required |
78
+ | `--token <t>` / `HALO_NODE_TOKEN` | Pairing token — required; prefer the env var (flags show up in `ps`) |
79
+ | `--root <dir>` | Folder to share; every path must stay inside it. Omit (or `/`) for whole-machine mode: any path this user can reach, relative paths from the current dir |
80
+ | `--allow-shell` | Let agents run commands (`node_run`) as this user, not sandboxed. Off by default |
81
+ | `--proxy <url>` | `http(s)://` forward proxy for the connection, e.g. `--proxy http://127.0.0.1:12334`; when given, `HTTPS_PROXY` / `HTTP_PROXY` / `NO_PROXY` are ignored ([below](#through-a-forward-proxy)) |
82
+ | `--header "Name: value"` | Repeatable. Extra header on the connection, for an auth proxy in front of the server |
83
+ | `HALO_SHELL_TIMEOUT` | `node_run` timeout in ms (default 120000) |
84
+ | `--version` / `--help` | Print the version / usage |
80
85
 
81
- The admin row turns green (live, no refresh) and shows `folder · shell on/off · os`.
86
+ Exits 1 on a rejected / revoked token, a deleted workspace, a protocol mismatch or a bad flag; retries everything else.
82
87
 
83
88
  ### Folder mode vs whole-machine mode
84
89
 
85
90
  - **`--root <folder>`** — share one folder. Every path the agent passes must stay inside it (symlinks pointing out are refused too); relative paths start from it.
86
- - **No `--root`** (or `--root /`) — **whole-machine mode**: the agent can reach every path your user can, like a full-access local session. Relative paths start from the directory you ran `halo node` in. A startup line warns about it, and the admin shows "Whole machine".
91
+ - **No `--root`** (or `--root /`) — **whole-machine mode**: the agent can reach every path your user can, like a full-access local session. Relative paths start from the directory you ran `halo-node` in. A startup line warns about it, and the admin shows "Whole machine".
87
92
 
88
93
  Prefer `--root` unless you really mean the whole machine.
89
94
 
90
95
  ### `--allow-shell`
91
96
 
92
- Off by default — `node_run` then refuses with "restart `halo node` with --allow-shell". With it, agents can run **any command as your user, not sandboxed**. In folder mode only the command's working directory is held inside the folder; the command itself (`cat ~/.ssh/...`, `curl ...`) can reach anything your user can. Turn it on only for agents and workspaces you trust. Commands are killed after the shell timeout (120 s, or `HALO_SHELL_TIMEOUT` in ms on the machine).
97
+ Off by default — `node_run` then refuses with "restart `halo-node` with --allow-shell". With it, agents can run **any command as your user, not sandboxed**. In folder mode only the command's working directory is held inside the folder; the command itself (`cat ~/.ssh/...`, `curl ...`) can reach anything your user can. Turn it on only for agents and workspaces you trust. Commands are killed after the shell timeout (120 s, or `HALO_SHELL_TIMEOUT` in ms on the machine).
93
98
 
94
99
  ### What the agent can see
95
100
 
@@ -97,25 +102,41 @@ Everything inside the shared folder — including `.env` files, keys, credential
97
102
 
98
103
  ### Behind a login proxy
99
104
 
100
- `halo node` sends a bearer token, nothing else. If the server sits behind an auth proxy (Cloudflare Access, an SSO gateway, basic auth), either point `--server` at an address the machine can reach directly, or pass the proxy's credentials with `--header` (repeatable, `Name: value`, split on the first colon):
105
+ `halo-node` sends a bearer token, nothing else. If the server sits behind an auth proxy (Cloudflare Access, an SSO gateway, basic auth), either point `--server` at an address the machine can reach directly, or pass the proxy's credentials with `--header` (repeatable, `Name: value`, split on the first colon):
101
106
 
102
107
  ```bash
103
108
  halo-node --server https://halo.example.com --root ~/proj \
104
109
  --header "CF-Access-Client-Id: <id>" --header "CF-Access-Client-Secret: <secret>"
105
110
  ```
106
111
 
107
- Neither client follows `HTTP(S)_PROXY`. To reach the server through a forward proxy, put a reverse proxy in front of the server instead, or use `--header`.
112
+ ### Through a forward proxy
113
+
114
+ To send the connection out through a local or corporate proxy (Hiddify / Clash system proxy, a company HTTP proxy), pass `--proxy`:
115
+
116
+ ```bash
117
+ halo-node --server https://halo.example.com --root ~/proj --proxy http://127.0.0.1:12334
118
+ ```
119
+
120
+ `http://` and `https://` proxies only, `user:pass@` allowed (`--proxy http://alice:pw@proxy.corp:3128` — sent as `Proxy-Authorization`, never printed; the startup line shows only `scheme://host:port`). Percent-encode a `/`, `?`, `#` or `@` in the user or password (`%2F`, `%3F`, `%23`, `%40`); a URL with anything after the host is refused. Without `--proxy`, halo-node reads the usual environment variables — `HTTPS_PROXY` for an `https://` server, `HTTP_PROXY` for `http://`, `NO_PROXY` for exceptions:
121
+
122
+ ```bash
123
+ HTTPS_PROXY=http://127.0.0.1:12334 halo-node --server https://halo.example.com --root ~/proj
124
+ ```
125
+
126
+ `--proxy` wins: when it's given, all three variables are ignored. A server on `localhost` / `127.0.0.1` is never proxied through the variables. Most system-proxy apps don't set these variables for the terminal, so pass one of the two explicitly. Conversely, a machine whose shell sets `HTTPS_PROXY` / `HTTP_PROXY` for other tools but reaches the server directly needs the server's host in `NO_PROXY` (halo-node before 2.2.4 ignored the variables).
127
+
128
+ Use a `https://` server through a proxy. An `https://` (wss) connection is tunnelled with `CONNECT`, so the proxy sees only the host and the token stays inside TLS. An `http://` (ws) server is requested from the proxy as a plain absolute-form `GET` — the token passes through the proxy in clear text, and a proxy that doesn't forward the WebSocket `Upgrade` can't connect at all.
108
129
 
109
130
  ### TLS certificates (`https://` servers)
110
131
 
111
- `halo-node` trusts the system's CA store (the Windows certificate store on Windows; `/etc/ssl/certs/…` on Linux, where `SSL_CERT_FILE` / `SSL_CERT_DIR` can point it elsewhere). It falls back to a built-in Mozilla root set only on a system with no CA bundle at all. For a private or corporate CA, install it into the system store, or on Linux set `SSL_CERT_FILE`. `halo node` (Node.js) does the opposite: it uses Node's built-in roots plus `NODE_EXTRA_CA_CERTS`, and ignores the system store.
132
+ `halo-node` trusts the system's CA store (the Windows certificate store on Windows; `/etc/ssl/certs/…` on Linux, where `SSL_CERT_FILE` / `SSL_CERT_DIR` can point it elsewhere). It falls back to a built-in Mozilla root set only on a system with no CA bundle at all. For a private or corporate CA, install it into the system store, or on Linux set `SSL_CERT_FILE`.
112
133
 
113
134
  ### Run it as a service (systemd)
114
135
 
115
136
  ```ini
116
137
  # /etc/systemd/system/halo-node.service
117
138
  [Unit]
118
- Description=halo node
139
+ Description=halo-node
119
140
  Wants=network-online.target
120
141
  After=network-online.target
121
142
 
@@ -138,7 +159,7 @@ RestartPreventExitStatus=1
138
159
  WantedBy=multi-user.target
139
160
  ```
140
161
 
141
- Network drops, server restarts, a busy token (409) and a lockout (429) are handled inside the process, which keeps running, so `on-failure` is enough. Don't use `Restart=always`: it keeps restarting after a revoked token, and each restart is a failed login. After 5 failures in 15 minutes the server locks out that IP address, which blocks every machine connecting from it. ExecStart takes the path the install script printed. For the npm client, set `ExecStart=/usr/bin/halo node …`. After upgrading with the install script, `systemctl restart halo-node` to run the new binary.
162
+ Network drops, server restarts, a busy token (409) and a lockout (429) are handled inside the process, which keeps running, so `on-failure` is enough. Don't use `Restart=always`: it keeps restarting after a revoked token, and each restart is a failed login. After 5 failures in 15 minutes the server locks out that IP address, which blocks every machine connecting from it. ExecStart takes the path the install script printed. After upgrading with the install script, `systemctl restart halo-node` to run the new binary.
142
163
 
143
164
  ### Start at boot on Windows (Task Scheduler)
144
165
 
@@ -170,7 +191,7 @@ If your server is reached through **Tailscale**, enable Tailscale's **Run unatte
170
191
 
171
192
  ### Settings on the machine
172
193
 
173
- `halo-node` takes its shell timeout only from `HALO_SHELL_TIMEOUT` (ms, default 120 s); grep's default of 50 matches and the 5 MiB command-output cap are fixed. `halo node` (npm) also reads them from the machine's own `~/.halo/secrets/` (`timeout.shell_exec` in `config.yaml`, the `general.limits.*` keys in `settings.yaml`). This only matters on a machine that also runs a halo server with those settings changed.
194
+ `halo-node` takes its shell timeout only from `HALO_SHELL_TIMEOUT` (ms, default 120 s); grep's default of 50 matches and the 5 MiB command-output cap are fixed. It doesn't read a halo server's `~/.halo/secrets/` settings on the same machine.
174
195
 
175
196
  ## 3. Let the agent use it
176
197
 
@@ -193,8 +214,8 @@ Then just ask: "list my machines", "read `notes/todo.md` on mbp", "run the tests
193
214
  |---|---|
194
215
  | `token rejected by the server (401)` | Wrong / deleted token — create a new one. 5 bad tries in 15 min lock the address out for a while (429). |
195
216
  | `the workspace this token is bound to no longer exists (404)` | The workspace folder was removed on the server. |
196
- | `protocol mismatch (426)` | Client and server versions too far apart — upgrade the older side. |
197
- | `this token is already connected from another process` | Another `halo node` with the same token is running; this one keeps retrying until that one stops. Also normal right after the machine rebooted or woke from sleep: the server needs 40–60 s to notice the old connection is dead, so the new process backs off and can take up to ~90 s to come online. |
198
- | agent: `machine "mbp" is offline` | `halo node` isn't running / connected. |
217
+ | `protocol mismatch (426)` | `halo-node` and the server speak different protocol versions — upgrade the older one: re-run `install.sh` (or download the server's release) here, or `halo upgrade` on the server. |
218
+ | `this token is already connected from another process` | Another `halo-node` with the same token is running; this one keeps retrying until that one stops. Also normal right after the machine rebooted or woke from sleep: the server needs 40–60 s to notice the old connection is dead, so the new process backs off and can take up to ~90 s to come online. |
219
+ | agent: `machine "mbp" is offline` | `halo-node` isn't running / connected. |
199
220
  | agent: `went offline during the call` | The connection dropped mid-call; the operation may or may not have happened. Running commands on the machine were stopped. |
200
221
  | agent: `not available in this runtime` | You're in the standalone CLI / TUI — linked machines only work through `halo server` (admin UI or a channel). |
@@ -11,7 +11,7 @@ Top-level built-in slash commands: `/help` `/evo` `/session` `/agent` `/skill` `
11
11
  | Name | Slash | Type | Purpose |
12
12
  |---|---|---|---|
13
13
  | `help` | `/help` | server | List commands — object commands show only the verbs the user can run; a command is hidden entirely if no verb is runnable |
14
- | `evo` | `/evo [text]` | server | Flat command (no verbs), **full-only**. Trigger self-evolution on the current root session: snapshot the conversation + queue an evo run. Optional text becomes a hint for what to focus on. Works at every `general.evolution.level` (L0 = manual only; L1 also drafts automatically on pre-compact; L2 / L3 may also auto-apply the result — prompt-only / capability changes that pass the gates — then post one `chat:system` line in this session). Hidden from `/help` for non-full users. See [design/evolution.md](../design/evolution.md). |
14
+ | `evo` | `/evo [text]` | server | Flat command (no verbs), **full-only**. Trigger self-evolution on the current root session: snapshot the conversation + queue an evo run. Optional text becomes a hint for what to focus on. Works at every `general.evolution.level` (L0 = manual only; L1 also drafts automatically on pre-compact; L2 / L3 may also auto-apply the result — prompt-only / capability changes that pass the gates — then post one `chat:system` line in this session). The level is the workspace's own: `evolution.level` can be set per workspace in `<ws>/.halo/settings.yaml`, falling back to the global one. Hidden from `/help` for non-full users. See [design/evolution.md](../design/evolution.md). |
15
15
 
16
16
  The rest are noun-verb **object commands**: `/<obj> <verb> [args]`. Some verbs are built-in deterministic code (`SUBCOMMAND_ROUTES`); the others fall through to the same-name skill (LLM, dispatched via `$1`). Bare `/<obj>` or `/<obj> help` lists the verbs available to the user (filtered by access level).
17
17
 
@@ -102,13 +102,49 @@ 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 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`).
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), `outbound_proxy.*`, `evolution.*`, `limits.*`. All `general.*` keys are `globalOnly` except `evolution.level` and `evolution.auto_apply_threshold`: `config.ts` resolves the `globalOnly` ones 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
 
109
109
  `sandbox.hidden_dirs` / `sandbox.hidden_files` / `sandbox.writable_dirs` are `globalOnly` — they define the security boundary agents run inside, so a workspace `settings.yaml` cannot override them (a workspace overriding them could lift its own sandbox constraints). `hidden_dirs` / `hidden_files` hold **extra** entries: the effective list is the built-in default (`DEFAULT_HIDDEN_DIRS` / `DEFAULT_HIDDEN_FILES` in `tools/sandbox.ts`, the single source — `~/.halo/secrets`, `~/.aws`, `~/.ssh`, `~/.gnupg`, `~/.docker`, `~/.config/gh`, `~/.halo/global/internal-sessions`, `~/.halo/global/logs`; `~/.npmrc`, `~/.bash_history`, `~/.gitconfig`, `~/.git-credentials`, `~/.netrc`, the global `evo` / `cron` / `runs` databases + `-wal` / `-shm`) plus the user's entries, deduped (`config.ts` `resolveSandboxPaths()`, used by both the server and the CLI). A default entry cannot be removed from the list in settings. The lists apply only to workspace / readonly sessions (bwrap / Seatbelt, or `assertPathAllowed` without an OS sandbox); Full sessions, the admin file explorer (`validatePath`, workspace-bound only) and the terminal never consult them. The schema `default` is empty so the placeholder doesn't suggest copying the built-ins; the field description names them. `writable_dirs` is a plain list (no built-ins).
110
110
 
111
- `evolution.*` controls the self-evolution subsystem (see [design/evolution.md](../design/evolution.md)). All `evolution.*` keys are `globalOnly` — they live in `~/.halo/secrets/settings.yaml` only, not workspace settings. Notable knobs: `evolution.level` (`L0` = manual only: `/evo` drafts, a reviewer approves; `L1` = L0 plus automatic drafting on pre-compact; `L2` = L1 plus auto-applying prompt-only changes that pass the gates; `L3` = L2 plus auto-applying capability changes — auto-applied changes still pass the regression check and can be rolled back from the Evolution tab; only changes learned from a `full`-access session auto-apply, never from one that took cron webhook events; a cron run's own turn never drafts on pre-compact), `evolution.auto_apply_threshold` (int 0–100, default 80 — minimum average score at L2/L3, on top of the fixed floors confidence high / behavior ≥ 70 / lint 100 / all sandbox checks; a save outside 0–100 is rejected with 400), `evolution.max_concurrent_run` / `max_concurrent_apply` (wrapper concurrency caps), `evolution.run_timeout_minutes` / `apply_timeout_minutes` (heartbeat timeouts), `evolution.max_attempts` (per-row retry cap), `evolution.triggers.pre_compact` (snapshot session before compaction).
111
+ #### Outbound proxy
112
+
113
+ For a machine whose way out is a local proxy (e.g. Hiddify / Clash in system-proxy mode on `127.0.0.1:12334`). Command-line tools ignore the OS proxy setting, so without this they connect directly. Three `globalOnly` fields, all read live (the mtime-cached settings, one `stat` per spawn / request) — no restart:
114
+
115
+ | Key | Type | Default | Meaning |
116
+ |---|---|---|---|
117
+ | `outbound_proxy.enabled` | boolean | `false` | Master switch. On with an empty `url` = off, no error |
118
+ | `outbound_proxy.url` | string | `''` (placeholder `http://127.0.0.1:12334`) | Proxy address. Must parse with scheme `http:` or `https:` and carry **no user-info** — `PUT` / `PATCH` reject `user:pass@…` with 400 (`… put them in Token …`) so a credential never lands in a plain-text field. A hand-edited bad value is ignored at read time (proxy off, one `[OutboundProxy]` warning that never echoes the URL) |
119
+ | `outbound_proxy.token` | string, **secret** | `''` | Optional credential: `user:password`, or a bare token (= user name, empty password). Masked in `GET /api/settings/schema` like any secret |
120
+
121
+ **What goes through it** (when on):
122
+ - **Spawned children** — `shell_exec` at every access level, Terminals, notebook kernels, evolution runs. `cleanChildEnv()` (`child-env.ts`) adds `http_proxy`, `https_proxy`, `HTTP_PROXY`, `HTTPS_PROXY`, `no_proxy`, `NO_PROXY`. The bwrap (`--clearenv` + `--setenv`) and macOS Seatbelt (allowlisted env) paths add the same vars from the same function. Neither isolates the network, so a proxy on the host's loopback is reachable from inside.
123
+ - **web_fetch** and **A2A requests to remotes** (agent card + JSON-RPC, `a2a/outbound.ts`) — an undici `ProxyAgent` passed per request. It is never the global dispatcher: model provider calls share the global `fetch` and keep their own route. A2A's `general.a2a.url_allowlist` check still runs on the target before the request goes to the proxy. A2A push webhooks to callers and inbound image URL fetches stay direct.
124
+
125
+ **Not covered**: model provider calls, channel connections (Telegram / Slack / Feishu …), the hub / update checks.
126
+
127
+ **no_proxy**: `localhost,127.0.0.1,::1`, plus whatever `NO_PROXY` / `no_proxy` the server process itself inherited, deduped (an inherited `*` makes the list just `*`). A child gets that list. web_fetch / A2A match it the way curl 8.x does, so in-process requests and a child's curl route alike:
128
+ - a host name matches an entry equal to it or to a parent domain, case-insensitive, a leading dot ignored — `corp.example` and `.corp.example` both match `corp.example` and `a.corp.example`, not `notcorp.example`;
129
+ - an IP address (v4, or v6 as in `http://[fd00::1]/`) matches an entry that is that address or a CIDR containing it — `10.0.0.0/8`, `fd00::/8`; an IP entry never matches a host name;
130
+ - `*` alone matches everything;
131
+ - nothing else is a pattern: `*.corp.example` is compared literally (matches no real host), and an entry with a port (`host:8080`) never matches — ports play no part.
132
+
133
+ web_fetch applies this per redirect hop: a redirect into a no_proxy host connects directly, one out of it goes through the proxy.
134
+
135
+ **Off** = Halo sets no proxy variables at all. Proxy variables the server process inherited from its own environment are not removed — they are the operator's.
136
+
137
+ **Token transport** — Basic auth both ways, `base64(user:password)`:
138
+ - children: in the proxy URL's user-info, percent-encoded (`http://<user>:<pass>@host:port`) — the one form curl, git and pip all read;
139
+ - web_fetch / A2A: the `Proxy-Authorization` header (on the CONNECT for https and http alike). While the proxy is on, a web_fetch call carrying its own `Proxy-Authorization` header (any case) is refused with a tool error before any request — no_proxy targets included.
140
+
141
+ `<<ENV_NAME>>` in the token expands from the server's environment, as in any secret; an unset variable stays literal.
142
+
143
+ The credential never appears in logs (only the user-info-free origin is printed: `[OutboundProxy] on: http://host:port (with token)`). In `shell_exec` results (e.g. `env` output) its URL form `<user:pass>@` (percent-encoded or not) and its base64 are masked at any length; the bare token / encoded user-info on their own only from 4 characters (shorter strings would mangle ordinary output). In bwrap sessions only the command itself gets the proxy variables, not the file tools' helper processes.
144
+
145
+ **Terminals** get their env when they are opened: a Terminal already open keeps the old setting, a newly opened one picks up the change.
146
+
147
+ `evolution.*` controls the self-evolution subsystem (see [design/evolution.md](../design/evolution.md)). `evolution.level` and `evolution.auto_apply_threshold` can be set **per workspace**: `<workspace>/.halo/settings.yaml` overrides `~/.halo/secrets/settings.yaml`, which overrides the schema default (an open workspace can sit at L0 while a private one auto-applies), and the workspace Settings page edits them like any non-`globalOnly` field. The rest of `evolution.*` is `globalOnly` — it lives in `~/.halo/secrets/settings.yaml` only, not workspace settings. Notable knobs: `evolution.level` (`L0` = manual only: `/evo` drafts, a reviewer approves; `L1` = L0 plus automatic drafting on pre-compact; `L2` = L1 plus auto-applying prompt-only changes that pass the gates; `L3` = L2 plus auto-applying capability changes — auto-applied changes still pass the regression check and can be rolled back from the Evolution tab; only changes learned from a `full`-access session auto-apply; a cron run's own turn never drafts on pre-compact), `evolution.auto_apply_threshold` (int 0–100, default 80 — minimum average score at L2/L3, on top of the fixed floors confidence high / behavior ≥ 70 / lint 100 / all sandbox checks; a save outside 0–100 is rejected with 400), `evolution.max_concurrent_run` / `max_concurrent_apply` (wrapper concurrency caps), `evolution.run_timeout_minutes` / `apply_timeout_minutes` (heartbeat timeouts), `evolution.max_attempts` (per-row retry cap), `evolution.triggers.pre_compact` (snapshot session before compaction).
112
148
 
113
149
  Goal mode's entry points are offline (the `/goal` command, admin banner and creation of new goal sessions are hidden). To reopen them, set `general.goal_mode_enabled` — a global-only boolean, default `false`, read from `~/.halo/secrets/settings.yaml` only (a workspace `settings.yaml` has no effect) and not shown in the Settings UI — then restart the Halo server and refresh the browser (existing goal bindings and history are not touched either way):
114
150
 
@@ -134,6 +170,7 @@ There is no "default provider" setting: a newly created agent's `model:` is a co
134
170
  | `options` / `optionLabels` | no | For `type: enum`: the allowed values, and optional display labels parallel to them |
135
171
  | `description` | no | English description rendered as help text |
136
172
  | `description_zh` | no | Chinese description (UI picks based on lang) |
173
+ | `placeholder` | no | Example shown in an empty text input, display only — unlike `default` it is not a value anything falls back to (e.g. `outbound_proxy.url`). Built-in `general` section only |
137
174
  | `default` | no | Placeholder shown when the value is unset; supports `<<ENV>>`. For provider / skill / agent fields it is display-only: an unset param's `{{<id>.params.<key>}}` stays literal at substitution, so the consumer applies its own fallback (the `extension` skill's `ext.sh` treats `{{…}}` as "use the default hub"; `web-search`'s `search.py` as "use the default region") |
138
175
  | `secret` | no | `true` → masked in API responses + password input in UI |
139
176
  | `restartRequired` | no | `true` → read once at server start; the Settings page shows a restart notice after saving it. Built-in `general` section only |