@turmind/halo 2.1.5 → 2.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/admin-out/404.html +1 -1
- package/admin-out/_next/static/chunks/1607-bf227fc5e4db45b7.js +1 -0
- package/admin-out/_next/static/chunks/1989-dab5d5c24b8f4c9f.js +1 -0
- package/admin-out/_next/static/chunks/5287.7ea08918388fa804.js +1 -0
- package/admin-out/_next/static/chunks/app/page-cca81a2ed69100f5.js +1 -0
- package/admin-out/_next/static/chunks/{webpack-7f8156dbbdb54868.js → webpack-9532ec7f1fa9a757.js} +1 -1
- package/admin-out/_next/static/css/8fc43fce83e5ce5d.css +1 -0
- package/admin-out/index.html +1 -1
- package/admin-out/index.txt +5 -5
- package/bundled-docs/design/a2a.md +5 -5
- package/bundled-docs/design/agent.md +1 -1
- package/bundled-docs/design/agentcore.md +3 -2
- package/bundled-docs/design/architecture.md +3 -3
- package/bundled-docs/design/background-dispatch.md +1 -1
- package/bundled-docs/design/canvas-extensions.md +5 -5
- package/bundled-docs/design/cron.md +77 -53
- package/bundled-docs/design/evolution.md +111 -39
- package/bundled-docs/design/node.md +30 -13
- package/bundled-docs/design/observability.md +12 -12
- package/bundled-docs/design/prompt-system.md +1 -1
- package/bundled-docs/design/relay.md +4 -4
- package/bundled-docs/design/session.md +14 -12
- package/bundled-docs/design/slack.md +1 -1
- package/bundled-docs/design/storage.md +14 -11
- package/bundled-docs/design/telegram.md +1 -1
- package/bundled-docs/design/ws.md +4 -4
- package/bundled-docs/dev/api.md +16 -14
- package/bundled-docs/dev/deploy.md +2 -1
- package/bundled-docs/dev/env.md +2 -2
- package/bundled-docs/dev/logging.md +1 -1
- package/bundled-docs/dev/tools.md +16 -30
- package/bundled-docs/guide/cli.md +9 -3
- package/bundled-docs/guide/delegation-and-access.md +1 -1
- package/bundled-docs/guide/node.md +86 -11
- package/bundled-docs/guide/workspace.md +1 -1
- package/bundled-docs/requirements/command.md +1 -1
- package/bundled-docs/requirements/editor.md +5 -2
- package/bundled-docs/requirements/settings.md +2 -2
- package/bundled-docs/requirements/terminal.md +1 -1
- package/bundled-docs/requirements/workspace.md +1 -1
- package/dist/evo-wrapper.js +913 -201
- package/dist/index.js +2670 -1786
- package/package.json +1 -1
- package/templates/agents/__apply_agent__/AGENT.md +10 -6
- package/templates/agents/__evo_agent__/AGENT.md +15 -4
- package/templates/agents/__score__/AGENT.md +4 -0
- package/templates/skills/cron/SKILL.md +37 -10
- package/templates/skills/cron/manage-cron.py +90 -15
- package/admin-out/_next/static/chunks/1101-d2f818895a3aa32e.js +0 -1
- package/admin-out/_next/static/chunks/1989-3aeeefebf77a40f3.js +0 -1
- package/admin-out/_next/static/chunks/5058.8514aa6d6a947cce.js +0 -1
- package/admin-out/_next/static/chunks/app/page-dac9e957de1eb740.js +0 -1
- package/admin-out/_next/static/css/6da86a077e119a6c.css +0 -1
- /package/admin-out/_next/static/{nOzfkh9Uo1Cp5TFa-C4gG → Rs9tIYSSITPLxv274DU9f}/_buildManifest.js +0 -0
- /package/admin-out/_next/static/{nOzfkh9Uo1Cp5TFa-C4gG → Rs9tIYSSITPLxv274DU9f}/_ssgManifest.js +0 -0
|
@@ -161,7 +161,7 @@ On Linux without bwrap only layers 2 + 3 are active.
|
|
|
161
161
|
- a system directory or its direct child (`/etc`, `/usr`, `/var`, `/home`, `/opt`, … plus macOS `/Applications`, `/Library`, `/System`, `/Users`, `/Volumes`, `/private`)
|
|
162
162
|
- `/tmp`, `/private/tmp`, `/root` themselves (their children are ordinary deletes — `/root` is `$HOME` in most containers)
|
|
163
163
|
|
|
164
|
-
Parsing: the command is split into simple commands on unquoted `;` `&` `|` newline, quotes stripped, heredoc bodies skipped (they're data being written, not commands). Leading wrappers / keywords (`sudo`, `env`, `xargs`, `nohup`, `time`, `if`/`then`/`do`/`while`, `VAR=…` assignments) are skipped to find the real command; `cd` earlier in the line moves the base for relative targets (default base = workspace root). `~`, `$HOME`, `$PWD` / `$(pwd)` expand; any other variable or substitution expands to empty — its value when unset, which is how `rm -rf "$DIR/"` goes wrong. A glob meaning "everything in X" (`*`, `.*`,
|
|
164
|
+
Parsing: the command is split into simple commands on unquoted `;` `&` `|` newline, quotes stripped, heredoc bodies skipped (they're data being written, not commands). Leading wrappers / keywords (`sudo`, `env`, `xargs`, `nohup`, `time`, `if`/`then`/`do`/`while`, `VAR=…` assignments) are skipped to find the real command; `cd` earlier in the line moves the base for relative targets (default base = workspace root). `~`, `$HOME`, `$PWD` / `$(pwd)` expand; any other variable or substitution expands to empty — its value when unset, which is how `rm -rf "$DIR/"` goes wrong. A glob meaning "everything in X" (`*`, `.*`, `**`, `?`) is judged by X; a narrower glob is judged as the literal path it spells, so `*.log` passes but one directly under a system directory (`/etc/*.bak`) is blocked. `*` `?` `[` inside `$HOME` or the workspace path, as `~` / `$HOME` / `$PWD` / the starting directory insert them, are plain name characters, never a glob. A block throws `[Sandbox] rm blocked: "<word>" resolves to <path>, which is <reason>. Name the specific files or subdirectories to delete instead.` It's a heuristic against mistakes, not a parser — `eval`, `bash -c "…"`, scripts and command substitution aren't followed.
|
|
165
165
|
|
|
166
166
|
**Windows has no sandbox.** There is no bwrap / Seatbelt equivalent, so `sandbox.ts` (`normalizeOptsForPlatform`) promotes every non-full call to `full` before it reaches layer 1 or layer 3 — `assertPathAllowed()` returns immediately without checking the workspace boundary or the hidden-path lists, and the rm guard is skipped (cmd syntax, no `rm`). Only layer 2 (tool filtering) survives, because `getSandboxBackend()` is always `null` there. The admin's access-level selector reads `sandbox: null` from `/api/health` and locks itself to Full. Net effect on Windows:
|
|
167
167
|
|
|
@@ -243,35 +243,21 @@ List direct child sessions of the current session and their status.
|
|
|
243
243
|
No arguments. Returns JSON:
|
|
244
244
|
|
|
245
245
|
```json
|
|
246
|
-
{
|
|
247
|
-
"code": 0,
|
|
248
|
-
"sessions": [
|
|
249
|
-
{
|
|
250
|
-
"id": "root>sid_xxx",
|
|
251
|
-
"parentId": "root",
|
|
252
|
-
"agentId": "coder",
|
|
253
|
-
"agentName": "Coder",
|
|
254
|
-
"description": "Implement login page",
|
|
255
|
-
"status": "running",
|
|
256
|
-
"accessLevel": null,
|
|
257
|
-
"goalSessionId": null,
|
|
258
|
-
"createdAt": 1714000000000,
|
|
259
|
-
"updatedAt": 1714000005000,
|
|
260
|
-
"stoppedAt": null,
|
|
261
|
-
"archivedAt": null,
|
|
262
|
-
"title": "Implement login page",
|
|
263
|
-
"exchangeCount": 1,
|
|
264
|
-
"contextTokens": 12345,
|
|
265
|
-
"totalOutputTokens": 678
|
|
266
|
-
}
|
|
267
|
-
],
|
|
268
|
-
"count": 1
|
|
269
|
-
}
|
|
246
|
+
{"code":0,"sessions":[{"id":"root>sid_xxx","agentId":"coder","agentName":"Coder","title":"Implement login page","status":"running","createdAt":1714000000000,"updatedAt":1714000005000,"exchangeCount":1}],"count":1}
|
|
270
247
|
```
|
|
271
248
|
|
|
272
249
|
`status`: `running` / `idle` / `stopped`. Archived sessions are excluded.
|
|
273
250
|
|
|
274
|
-
Each entry is the
|
|
251
|
+
Each entry is a compact row, not the full `SessionInfo`, and the JSON has no
|
|
252
|
+
indentation: the full pretty-printed rows overflowed the 8K tool-result cap at
|
|
253
|
+
a couple dozen children. Always present: `id`, `agentId`, `agentName`, `title`,
|
|
254
|
+
`status`, `createdAt`, `updatedAt` (the same set as `relay_list`). Present only
|
|
255
|
+
when set: `goalSessionId` (bound to a goal), `workingDir`, `accessLevel`, and
|
|
256
|
+
`exchangeCount` (only when > 0, i.e. someone has exchanged messages with it).
|
|
257
|
+
Not included: `parentId` (always the caller), `description` (folded into
|
|
258
|
+
`title`), `stoppedAt` (covered by `status`), `archivedAt`, and token counts.
|
|
259
|
+
|
|
260
|
+
`title` is the human-assigned
|
|
275
261
|
label (set by renaming the session in the admin sidebar) mirrored onto the row
|
|
276
262
|
from the session file, so it matches what the UI shows. It falls back to
|
|
277
263
|
`description` (the `start_session` task summary) when no title was set, so the
|
|
@@ -356,7 +342,7 @@ Returns: full SKILL.md content (body + resource files list). For progressive dis
|
|
|
356
342
|
|
|
357
343
|
**Built-in for every agent — the one truly unconditional tool** (`activate_skill` is gated on `skills`, session tools on `team`; not declared in `agent.yaml tools`). Wired in `session-agent-builder` (`buildContinueTaskTool`, next to the session bundle). No parameters.
|
|
358
344
|
|
|
359
|
-
Call it when the current turn was started by an interruption (a user / parent message landed while the agent was working) and the interrupted task is **not** finished: after the current reply ends, `drainQueue` pushes a synthetic `[System] You called continue_task: … resume it now` user turn (traced as a `user` row, `report: true`) and runs one more turn. Only effective in a turn that followed an interrupt — in a normal turn it returns `not_interrupted` and sets nothing (kicks ≤ interrupts). The flag lasts one turn: a second interrupt before the kick resets it and the model is told to call again if still needed. An esc / `/interrupt` (abort without a new message) never kicks; Stop / delete / archive clear it; a callback landing after Stop gets `code: 1` (`no_turn`). The description asks the model to write its reply to the interrupting message **first**, then call the tool. At turn end, whenever another turn follows (the kick, or a message already queued) and the turn was an answer (`continue_task` called, or a natural end_turn), the turn's whole text is forwarded as an **interim report** when the interrupting message came from the parent (sub-agent → `[Interim report · status: still running] …` via `query_session`, no `stoppedAt`) or from a relay caller (root → `[Relay interim report · … · status: still running]`) — the final auto-report / relay report still follows once. The same applies to the opening turn when a follow-up is already queued by the time it ends.
|
|
345
|
+
Call it when the current turn was started by an interruption (a user / parent message landed while the agent was working) and the interrupted task is **not** finished: after the current reply ends, `drainQueue` pushes a synthetic `[System] You called continue_task: … resume it now` user turn (traced as a `user` row, `report: true`) and runs one more turn. Only effective in a turn that followed an interrupt — in a normal turn it returns `not_interrupted` and sets nothing (kicks ≤ interrupts). The flag lasts one turn: a second interrupt before the kick resets it and the model is told to call again if still needed. An esc / `/interrupt` (abort without a new message) never kicks, nor does a turn that ended on a model refusal or a model account error (it would fail the same way; other errors still kick); Stop / delete / archive clear it; a callback landing after Stop gets `code: 1` (`no_turn`). The description asks the model to write its reply to the interrupting message **first**, then call the tool. At turn end, whenever another turn follows (the kick, or a message already queued) and the turn was an answer (`continue_task` called, or a natural end_turn), the turn's whole text is forwarded as an **interim report** when the interrupting message came from the parent (sub-agent → `[Interim report · status: still running] …` via `query_session`, no `stoppedAt`) or from a relay caller (root → `[Relay interim report · … · status: still running]`) — the final auto-report / relay report still follows once. The same applies to the opening turn when a follow-up is already queued by the time it ends.
|
|
360
346
|
|
|
361
347
|
Returns: `{ code: 0, message }` on `set` / `not_interrupted`, `{ code: 1, error }` on `no_turn`. Design in [design/session.md → continue_task](../design/session.md#message-queue-and-drain).
|
|
362
348
|
|
|
@@ -393,7 +379,7 @@ Returns `{ "code": 0, "workspace": "<realpath>", "session_id": "…", "state": "
|
|
|
393
379
|
<target's finalOutput || output>
|
|
394
380
|
```
|
|
395
381
|
|
|
396
|
-
Body capped at `limits.autoReportMax` (head-kept) with a `[Report truncated: N chars total. Use relay_read("<ws>", "<id>") for the full text.]` marker. A turn killed by an unrecoverable error carries `· status: aborted]` in the header and is prefixed `[RELAY TARGET ABORTED: … Error: <text> … Re-send with relay_send to let it resume.]` **before** the cap so it can't be sliced off. When the error is an account problem (401/402/403, bad key, no balance), the closing hint instead says it is a model account / credential / balance / permission problem in the target workspace: retrying or re-sending fails the same way, so fix its model configuration (or tell the user to) and do not resume it with `relay_send` until then. `reply_to` is cleared **before** sending: one dispatch → one report, a failed delivery can't double-fire on the next turn end, and a user chatting directly in the department workspace afterwards never pings the secretary.
|
|
382
|
+
Body capped at `limits.autoReportMax` (head-kept) with a `[Report truncated: N chars total. Use relay_read("<ws>", "<id>") for the full text.]` marker. A turn killed by an unrecoverable error carries `· status: aborted]` in the header and is prefixed `[RELAY TARGET ABORTED: … Error: <text> … Re-send with relay_send to let it resume.]` **before** the cap so it can't be sliced off. When the error is an account problem (401/402/403, bad key, no balance), the closing hint instead says it is a model account / credential / balance / permission problem in the target workspace: retrying or re-sending fails the same way, so fix its model configuration (or tell the user to) and do not resume it with `relay_send` until then. For a model refusal (the target's history tripped the safety classifier) it says not to resume it — `relay_send` to a NEW session id with a clean brief, rewording the part that tripped it. `reply_to` is cleared **before** sending: one dispatch → one report, a failed delivery can't double-fire on the next turn end, and a user chatting directly in the department workspace afterwards never pings the secretary.
|
|
397
383
|
|
|
398
384
|
**Interim report**: if a busy target answers a follow-up `relay_send` in a turn that another turn follows — it called `continue_task` to resume what it was doing, or a further relay message was already queued when the answer ended (also on the opening turn), or the target was waiting on its own sub-agents when asked and the run ended on the answer with a sub-agent still running — the answer is delivered right away as
|
|
399
385
|
|
|
@@ -463,7 +449,7 @@ A `bearer` remote's token is the settings secret `a2a.secrets.<remote>` (workspa
|
|
|
463
449
|
|
|
464
450
|
**Enabling**: opt-in by the single name `a2a_send` in `agent.yaml`'s `tools:` — it brings `a2a_stop` / `a2a_read` / `a2a_list`; the other three names are not recognised on their own. Available at **every access level** (full, workspace, readonly, any channel) **except the A2A read-only profile** — an inbound A2A session at readonly never gets them, whatever its yaml lists, so a remote's readonly token can't make this server call third parties. The admin tool picker shows one `a2a_send` chip naming the set.
|
|
465
451
|
|
|
466
|
-
**Server only**: in the CLI / TUI
|
|
452
|
+
**Server only**: in the CLI / TUI there is no push receiver; every A2A tool returns `{"code": 1, "error": "a2a is not available in the CLI / TUI — it only runs inside `halo server`, which receives the remote agent's push reports. This is permanent for this runtime, not a temporary outage: do not retry. …"}`. The tools are still listed there when the agent names `a2a_send`.
|
|
467
453
|
|
|
468
454
|
**Retries**: each request has a 30 s timeout. An AgentCore `-32054` "… please retry" answer is retried after 0.5 / 1 / 2 / 4 / 8 s, and a halo AgentCore container's lease-busy answer (`-32603` "workspace is in use by another runtime session …") after 5 / 10 / 15 / 20 s; then the last answer is returned as the error. Stopping the turn ends the wait at once — for the RPC and for the agent-card fetch before it.
|
|
469
455
|
|
|
@@ -561,7 +547,7 @@ Act on the user's **linked machines** — computers running `halo node`, paired
|
|
|
561
547
|
|
|
562
548
|
**Paths**: a machine shares one folder (`--root`; `root` in `node_list`) — relative paths resolve against it, paths that resolve outside it (symlinks included) are refused: `"<p>" is outside this machine's shared folder (<root>) — node tools can only reach paths inside it`. A whole-machine node (`root: null, wholeMachine: true`) has no such check; relative paths resolve against its `base`.
|
|
563
549
|
|
|
564
|
-
**Runtime**: in `halo server`
|
|
550
|
+
**Runtime**: in `halo server` (cron runs included) calls go over the machine's WebSocket. In the standalone CLI / TUI, the evo sandbox and AgentCore mode the tools are listed but every one — `node_list` included — returns `linked machines are not available in this runtime — only \`halo server\` accepts \`halo node\` connections. This is permanent for this runtime, not a temporary outage: do not retry. …`.
|
|
565
551
|
|
|
566
552
|
**Errors** (returned as the tool error text):
|
|
567
553
|
|
|
@@ -215,9 +215,9 @@ Attach files or images to your message with `@file` and `@image`, or pull a dire
|
|
|
215
215
|
|
|
216
216
|
## OpenTelemetry export
|
|
217
217
|
|
|
218
|
-
`halo cli` and `halo tui` export traces, metrics and logs the same way the server does: when the global `general.observability.endpoint` setting is non-empty, each run registers the OTLP exporters at start and the agent loop emits the same GenAI spans (`invoke_agent` / `chat` / `execute_tool`). With no endpoint nothing is loaded and startup is unaffected. The
|
|
218
|
+
`halo cli` and `halo tui` export traces, metrics and logs the same way the server does: when the global `general.observability.endpoint` setting is non-empty, each run registers the OTLP exporters at start and the agent loop emits the same GenAI spans (`invoke_agent` / `chat` / `execute_tool`). With no endpoint nothing is loaded and startup is unaffected. The evolution pipeline's runs are spawned `halo cli` children, so they show up in the collector too (`session.id` = the internal agent's name); [Cron Tasks](../design/cron.md) run inside the server since 2.2 and export as the server.
|
|
219
219
|
|
|
220
|
-
- **Telling processes apart**: they all share `service.name`; the `halo.process` resource attribute is `server`, `cli` or `tui`,
|
|
220
|
+
- **Telling processes apart**: they all share `service.name`; the `halo.process` resource attribute is `server`, `cli` or `tui`, evolution runs report `cli`, cron runs `server`.
|
|
221
221
|
- **Flushed on exit**: the batched telemetry is sent before the process ends — on normal completion, TUI quit, an error, and on SIGINT / SIGTERM (after the graceful stop, still exiting 130 / 143). The flush is capped at 3 s, so an unreachable collector delays the exit by at most that and never hangs it.
|
|
222
222
|
- `halo agents` and `halo sessions` only list and don't export.
|
|
223
223
|
|
|
@@ -320,6 +320,12 @@ foreground, reconnects on its own, Ctrl-C to stop. Needs no `halo setup` and
|
|
|
320
320
|
no model keys — nothing is written under `~/.halo/`. Create the token in
|
|
321
321
|
Admin → Channels → Linked machines.
|
|
322
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
|
+
|
|
323
329
|
| Flag / env | Meaning |
|
|
324
330
|
|---|---|
|
|
325
331
|
| `--server <url>` | Server address, `http(s)://host:port` (`ws(s)://` also accepted) — required |
|
|
@@ -361,7 +367,7 @@ Session prefix: `cli_`. Sessions are persisted to `<workspace>/.halo/sessions/`
|
|
|
361
367
|
|
|
362
368
|
`tui` / `cli` / `agents` / `sessions` refuse to run ("~/.halo/global/ not initialized. Run halo setup first.") until `halo setup` has seeded `~/.halo/global/`. Then the CLI replicates the server's init sequence (`initRuntime()` in `harness.ts`):
|
|
363
369
|
1. `refreshTemplatesIfOutdated()` — the server's template startup check: when `~/.halo/global/.template-version` is behind the bundled `TEMPLATE_VERSION` (typically right after `halo upgrade`), re-seed `~/.halo/global/` before anything reads the models registry; logs `[CLI] Templates outdated (vX → vY), refreshing ~/.halo/global/` to stderr. Runs once per upgrade — afterwards the stamp matches and it's a single file read
|
|
364
|
-
2. `initObservability()` — `tui` / `cli` only (the list-only `agents` / `sessions` skip it): when `general.observability.endpoint` is set, register OTel providers so this run's traces, metrics and logs reach the collector, tagged `halo.process=cli` or `tui`; a no-op otherwise.
|
|
370
|
+
2. `initObservability()` — `tui` / `cli` only (the list-only `agents` / `sessions` skip it): when `general.observability.endpoint` is set, register OTel providers so this run's traces, metrics and logs reach the collector, tagged `halo.process=cli` or `tui`; a no-op otherwise. Evolution runs are spawned `halo cli` children, so they export too. See [OpenTelemetry export](#opentelemetry-export)
|
|
365
371
|
3. `initLogger()` — redirect console to stderr + file logger
|
|
366
372
|
4. `initBwrapCheck()` — probe sandbox availability
|
|
367
373
|
5. `setSandboxHiddenPaths()` — configure sandbox paths
|
|
@@ -114,7 +114,7 @@ Extra rules:
|
|
|
114
114
|
| Channel account (Web / Telegram / Slack / Feishu / WeCom / WeChat, and the ACP adapter via its Web token) | The account's `accessLevel`: `full` → full, `workspace` → workspace, `readonly` and `observer` → readonly. New accounts default to **readonly**. | Re-applied on every inbound message. Changing an account's level takes effect on the next message. |
|
|
115
115
|
| Channel `/session new` | The account's level | — |
|
|
116
116
|
| `start_session` (sub-agent) | **Parent's level at creation time** | Kept. A later change on the parent doesn't reach existing children; start a new child to get the new level. |
|
|
117
|
-
| Cron job | Full
|
|
117
|
+
| Cron job | Full | Keeps its stored level (the run passes none) |
|
|
118
118
|
| `halo cli` / TUI | `--access`, default `full` | `--access` value (a different level is persisted) |
|
|
119
119
|
| `relay_send` into another workspace | Full | Keeps its stored level |
|
|
120
120
|
| A2A inbound (`/a2a/<path>`, a web-channel token bound to that workspace) | The token's `accessLevel`, mapped like a channel account. A `readonly` / `observer` token gets the fixed A2A read-only profile: `file_read` / `view_image` / `file_list` / `grep` / `glob` + `continue_task`, even with an OS sandbox — no shell, write or fetch, no delegation, no relay / a2a tools | Re-applied on every inbound message |
|
|
@@ -4,33 +4,68 @@ Let a workspace's agents work on files on **your own computer** — a laptop, a
|
|
|
4
4
|
|
|
5
5
|
```
|
|
6
6
|
[your laptop] [server]
|
|
7
|
-
halo
|
|
7
|
+
halo-node --root ~/proj ──WebSocket──────▶ halo server ── agent in workspace W
|
|
8
8
|
(files, optional shell) node_read / node_write / … / node_run
|
|
9
9
|
```
|
|
10
10
|
|
|
11
|
-
|
|
11
|
+
Two clients do the same job, with the same flags and the same behaviour:
|
|
12
12
|
|
|
13
|
-
|
|
13
|
+
- **Linux (x86_64 / arm64 / armv7)**: `halo-node`, a single ~7 MB binary attached to every GitHub release. It needs no Node.js and idles at about 9 MB of memory.
|
|
14
|
+
- **macOS / Windows** (or a Linux box that already has the npm package): `halo node` from `npm install -g @turmind/halo`.
|
|
15
|
+
|
|
16
|
+
Neither needs `halo setup` or model keys, and neither writes anything under `~/.halo/`.
|
|
17
|
+
|
|
18
|
+
Supported: **Linux and macOS**. Windows works on a best-effort basis (not regularly tested; `node_run` commands go to `cmd.exe`).
|
|
14
19
|
|
|
15
20
|
## 1. Create a token
|
|
16
21
|
|
|
17
|
-
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
|
|
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 `halo node` on macOS / Windows. The install link points at the server's own release, so the machine and the server come from the same version.
|
|
23
|
+
|
|
24
|
+
The token stays on the list row (copy button). Deleting the row revokes it at once and disconnects the machine.
|
|
25
|
+
|
|
26
|
+
## 2. Run it on the machine
|
|
27
|
+
|
|
28
|
+
**Linux** — install once with the release's install script, then run:
|
|
18
29
|
|
|
19
30
|
```bash
|
|
20
|
-
|
|
31
|
+
curl -fsSL https://github.com/turmind/halo-agent/releases/latest/download/install.sh | sh
|
|
32
|
+
export PATH="$HOME/.local/bin:$PATH" # non-root install; Debian / Ubuntu add it only at the next login
|
|
33
|
+
export HALO_NODE_TOKEN=<token> # better than --token: flags show up in ps / shell history
|
|
34
|
+
halo-node --server https://halo.example.com --root ~/proj
|
|
21
35
|
```
|
|
22
36
|
|
|
23
|
-
|
|
37
|
+
What the script does (`sh`, busybox included; curl or wget):
|
|
38
|
+
|
|
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.
|
|
40
|
+
- 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
|
+
- 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
|
+
- runs the downloaded binary's `--version` before replacing anything; a download that doesn't run stops with an error.
|
|
43
|
+
- replaces an existing `halo-node` in place, even one that is running (that process keeps the old file until it restarts). **Re-run the script to upgrade.**
|
|
44
|
+
- needs no token, starts nothing, and writes no service file.
|
|
24
45
|
|
|
25
|
-
|
|
46
|
+
Each release's `install.sh` installs that release's binary. The admin's command uses the server's own version (`releases/download/v<version>/install.sh`); `latest` installs the newest release. `halo-node --version` prints the version.
|
|
47
|
+
|
|
48
|
+
**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
|
+
|
|
50
|
+
```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
|
|
55
|
+
./halo-node --server https://halo.example.com --root ~/proj
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
**macOS / Windows**:
|
|
26
59
|
|
|
27
60
|
```bash
|
|
28
61
|
npm install -g @turmind/halo # once
|
|
29
|
-
export HALO_NODE_TOKEN=<token>
|
|
62
|
+
export HALO_NODE_TOKEN=<token>
|
|
30
63
|
halo node --server https://halo.example.com --root ~/proj
|
|
31
64
|
```
|
|
32
65
|
|
|
33
|
-
|
|
66
|
+
On Linux, `halo node` still works, and prints one `[Node] tip:` line with the install command.
|
|
67
|
+
|
|
68
|
+
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.
|
|
34
69
|
|
|
35
70
|
The admin row turns green (live, no refresh) and shows `folder · shell on/off · os`.
|
|
36
71
|
|
|
@@ -54,10 +89,50 @@ Everything inside the shared folder — including `.env` files, keys, credential
|
|
|
54
89
|
`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):
|
|
55
90
|
|
|
56
91
|
```bash
|
|
57
|
-
halo
|
|
92
|
+
halo-node --server https://halo.example.com --root ~/proj \
|
|
58
93
|
--header "CF-Access-Client-Id: <id>" --header "CF-Access-Client-Secret: <secret>"
|
|
59
94
|
```
|
|
60
95
|
|
|
96
|
+
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`.
|
|
97
|
+
|
|
98
|
+
### TLS certificates (`https://` servers)
|
|
99
|
+
|
|
100
|
+
`halo-node` trusts the system's CA store (`/etc/ssl/certs/…`; `SSL_CERT_FILE` / `SSL_CERT_DIR` 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 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.
|
|
101
|
+
|
|
102
|
+
### Run it as a service (systemd)
|
|
103
|
+
|
|
104
|
+
```ini
|
|
105
|
+
# /etc/systemd/system/halo-node.service
|
|
106
|
+
[Unit]
|
|
107
|
+
Description=halo node
|
|
108
|
+
Wants=network-online.target
|
|
109
|
+
After=network-online.target
|
|
110
|
+
|
|
111
|
+
[Service]
|
|
112
|
+
User=alice
|
|
113
|
+
# whole-machine mode: relative paths start here
|
|
114
|
+
WorkingDirectory=/home/alice
|
|
115
|
+
# HALO_NODE_TOKEN=… (chmod 600)
|
|
116
|
+
EnvironmentFile=/home/alice/.halo-node.env
|
|
117
|
+
# installed as root; as alice: /home/alice/.local/bin/halo-node
|
|
118
|
+
ExecStart=/usr/local/bin/halo-node --server https://halo.example.com --root /home/alice/proj
|
|
119
|
+
Restart=on-failure
|
|
120
|
+
RestartSec=10
|
|
121
|
+
# 1 = bad / revoked token or workspace gone: restarting won't help
|
|
122
|
+
RestartPreventExitStatus=1
|
|
123
|
+
# optional, for very small boards
|
|
124
|
+
# Environment=GOMEMLIMIT=24MiB
|
|
125
|
+
|
|
126
|
+
[Install]
|
|
127
|
+
WantedBy=multi-user.target
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
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.
|
|
131
|
+
|
|
132
|
+
### Settings on the machine
|
|
133
|
+
|
|
134
|
+
`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.
|
|
135
|
+
|
|
61
136
|
## 3. Let the agent use it
|
|
62
137
|
|
|
63
138
|
Node tools are enabled per agent by **`node_list`** in `agent.yaml`'s `tools:` — that one name brings all eight (`node_list`, `node_read`, `node_write`, `node_edit`, `node_list_dir`, `node_grep`, `node_glob`, `node_run`). The bundled `default` agent lists it. If your workspace overrides the default agent (`<workspace>/.halo/agents/default/agent.yaml`), or you use another agent, add it yourself:
|
|
@@ -79,7 +154,7 @@ Then just ask: "list my machines", "read `notes/todo.md` on mbp", "run the tests
|
|
|
79
154
|
|---|---|
|
|
80
155
|
| `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). |
|
|
81
156
|
| `the workspace this token is bound to no longer exists (404)` | The workspace folder was removed on the server. |
|
|
82
|
-
| `protocol mismatch (426)` |
|
|
157
|
+
| `protocol mismatch (426)` | Client and server versions too far apart — upgrade the older side. |
|
|
83
158
|
| `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. |
|
|
84
159
|
| agent: `machine "mbp" is offline` | `halo node` isn't running / connected. |
|
|
85
160
|
| 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. |
|
|
@@ -127,7 +127,7 @@ Common settings:
|
|
|
127
127
|
- `general.session.max_nesting_depth` — max session nesting depth
|
|
128
128
|
- `general.compact.keep_messages` — recent messages kept intact during compaction
|
|
129
129
|
- `general.logging.level` — `debug` / `info` / `warn` / `error`
|
|
130
|
-
- `general.observability.endpoint` — OTLP collector URL; leave empty to keep observability off. The server and every `halo cli` / `halo tui` run (
|
|
130
|
+
- `general.observability.endpoint` — OTLP collector URL; leave empty to keep observability off. The server (cron runs included) and every `halo cli` / `halo tui` run (evolution runs included) export to it (see [design/observability.md](../design/observability.md))
|
|
131
131
|
- `<provider-id>.secrets.api_key` — provider credentials (Kimi / DeepSeek bearer token)
|
|
132
132
|
- `<provider-id>.secrets.access_key_id` / `.secret_access_key` — AWS Bedrock
|
|
133
133
|
- `<skill-id>.params.<key>` — values an agent can inject into its own `shell_exec` via `{{<skill-id>.params.<key>}}`
|
|
@@ -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). 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). 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
|
|
|
@@ -37,10 +37,13 @@ When agents or scripts write files, the canvas picks it up — push-based, no po
|
|
|
37
37
|
With no session selected no `file:changed` arrives (the WS subscribes on session select), so only the tab-switch / focus / reconnect checks apply.
|
|
38
38
|
|
|
39
39
|
### Tab persistence
|
|
40
|
-
Open tabs and the active tab live in localStorage; survive refresh. Read-only tabs (below) are
|
|
40
|
+
Open tabs and the active tab live in localStorage; survive refresh. Read-only tabs (below) are restored too, still read-only.
|
|
41
41
|
|
|
42
42
|
### Read-only tabs (files outside the workspace)
|
|
43
|
-
A file outside the workspace opened from a chat path link ([chat.md](chat.md#message-rendering)) opens as a read-only tab: a lock icon and "Read-only — not in the current workspace" tooltip on the tab, Monaco read-only (typing shows the same message), never saved, no `file:changed` auto-refresh
|
|
43
|
+
A file outside the workspace opened from a chat path link ([chat.md](chat.md#message-rendering)) opens as a read-only tab: a lock icon and "Read-only — not in the current workspace" tooltip on the tab, Monaco read-only (typing shows the same message), never saved, no `file:changed` auto-refresh. Text is read via `/fs/stat` + `/fs/raw` (10 MB cap, same as `/files`); images and other previews load from `/files/download`'s absolute-path branch, tabular previews (csv / parquet / sqlite) from `/data-preview/*`'s.
|
|
44
|
+
- **Reload**: the tab is saved with a `readOnly` flag and reopened through the same `/fs` reads; one that is gone, too large or hidden is dropped, like a deleted workspace file.
|
|
45
|
+
- **Extensions**: an extension viewer (draw.io …) gets no `save` capability in a read-only tab — no Save button, no export, edits are never marked unsaved.
|
|
46
|
+
- **Open as text** on an outside binary preview reopens it as a read-only text tab via `/fs/raw` (10 MB cap); a file over the cap or unreadable shows the path-link hint ("File too large to open" / "File not found").
|
|
44
47
|
|
|
45
48
|
### Diff view
|
|
46
49
|
- A tab with unsaved edits shows a **Diff** button in its pane's tab bar: last saved content vs. the current buffer, computed in the client (no git involved; `GET /api/git/diff` is the Source Control panel's)
|
|
@@ -108,7 +108,7 @@ Declared in [packages/server/src/settings-schema.ts](../../../packages/server/sr
|
|
|
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), `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
|
+
`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).
|
|
112
112
|
|
|
113
113
|
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
114
|
|
|
@@ -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). For observability that applies to the server's own exporter only:
|
|
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 (cron runs included — they run in the server process) only: 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
|
|
|
@@ -47,7 +47,7 @@ When the WS drops, the bottom panel remounts `TerminalPanel` (fresh key on `_dis
|
|
|
47
47
|
|
|
48
48
|
- `terminal:reattached` / `terminal:opened`: a tab is added for each id not shown yet, named as the server says (the name the creating page gave in `terminal:start`).
|
|
49
49
|
- `terminal:snapshot`: the local xterm (created if missing) is `reset()`, sized to the snapshot's `cols × rows`, given the snapshot, then fit to its container; the fitted size is reported with `terminal:resize`.
|
|
50
|
-
-
|
|
50
|
+
- A fresh terminal is started only when `terminal:reattached` comes back with an empty list and the panel shows none — the server's answer decides, never a timer. The reply rides the same socket behind the page load's `subscribe` snapshot, so for a large session on a slow link it can take seconds; the 2 s fallback that used to guess here started an extra bash on every refresh. A reattach sent while offline is queued and re-sent on `_connected`, so every connection gets a reply.
|
|
51
51
|
|
|
52
52
|
### Environment
|
|
53
53
|
- Shell: `$SHELL` or `/bin/bash` (`ComSpec` / `powershell.exe` on Windows); bash / zsh / sh / fish start as login shells (`-l`)
|
|
@@ -36,7 +36,7 @@ The Bottom Panel (Chat + Terminal) can also be **floated** into a draggable wind
|
|
|
36
36
|
| Bot | Agents | — | Agent config editor + Test (full width) | — |
|
|
37
37
|
| Zap | Skills | Skill list | SkillsMain (full height) | — |
|
|
38
38
|
| MessageCircle | Channels | Channel list | Channel config editor | — |
|
|
39
|
-
| Sparkles | Evolution | Run list + status filters | Evolution main (run/apply review) | — |
|
|
39
|
+
| Sparkles | Evolution | Run list + status / auto / observation filters | Evolution main (run/apply review, auto-apply verdict, rollback, observation → GitHub issue) | — |
|
|
40
40
|
| Clock | Cron | Job list | Cron job detail / form + run audit | — |
|
|
41
41
|
| Settings2 | Settings | — | Settings panel | — |
|
|
42
42
|
|