@turmind/halo 0.2.1-bc7cf2b → 0.2.2

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.
@@ -170,10 +170,8 @@ node server.js
170
170
 
171
171
  ### Security model
172
172
 
173
- - `HALO_TOKEN` and `HALO_API` are server-side only, never exposed to the browser
174
- - Frontend uses same-origin relative paths (`/chat`, `/history`, etc.)
175
- - web-demo server proxies all requests to Halo, injecting the token in `x-token` header
176
- - Session auth: random hex token stored in `x-session` header + localStorage
173
+ - **Proxy mode (default)**: `HALO_TOKEN` and `HALO_API` are server-side only, never exposed to the browser; frontend uses same-origin relative paths (`/chat`, `/history`, etc.); web-demo server proxies all requests to Halo, injecting the token in `x-token` header. Session auth: HMAC token in `x-session` header + localStorage. All proxy routes including `GET /file` are gated by the auth middleware.
174
+ - **Direct-connect mode (opt-in)**: the gear panel takes a halo server URL + web-channel token; the browser then calls `<server>/api/web/*` directly with `x-token`, bypassing the proxy (no password step). The token is stored in that browser's localStorage by explicit user choice — the UI says so next to the field. One slot; clearing it returns to proxy mode. Works because the server CORS reflects any origin and allowlists `x-token`.
177
175
 
178
176
  ### Proxy routes
179
177
 
@@ -183,17 +181,21 @@ node server.js
183
181
  | `POST /stop` | `POST /api/web/stop` |
184
182
  | `GET /history` | `GET /api/web/history` |
185
183
  | `GET /subscribe` | `GET /api/web/subscribe` |
184
+ | `GET /file` | `GET /api/web/file` |
186
185
 
187
186
  ### Features
188
187
 
189
- - SSE streaming with tool call display (collapsible)
190
- - Auto-reconnect: if session is running on page load, subscribes to live stream
188
+ - Markdown rendering for assistant text (marked@12, gfm + breaks) with typewriter streaming; user text stays escaped plain text
189
+ - SSE streaming with collapsible thinking / tool-call step blocks
190
+ - Auto-reconnect: if session is running on page load, subscribes to live stream; history renders rebuild-from-scratch (never append)
191
191
  - Message queue: can send while agent is running, messages queue and send sequentially
192
- - Slash commands (`/help`, `/session`, `/agent`, `/skill`, `/workspace`)
193
- - Image upload (file picker + camera capture)
192
+ - Slash commands (`/help`, `/session`, `/agent`, `/skill`, `/workspace`), session/agent switcher menus
193
+ - Image upload + camera capture (camera button is mobile-only — the `capture` attribute is a no-op on desktop, where it would duplicate the file picker)
194
194
  - Voice recording (webm audio saved to workspace, path given to agent)
195
195
  - Stop/interrupt button
196
196
  - i18n: auto-detects browser language (`navigator.language`), toggle button in header, persists to `localStorage`
197
+ - Mobile-first layout: 100dvh-safe, safe-area insets, ≥44px touch targets, bottom-sheet menus/settings on small widths, visual language shared with `packages/agentcore-demo`
198
+ - Page load needs internet for two CDN deps (tailwindcss, marked) — no longer fully self-contained
197
199
 
198
200
  ## Integration guide (custom frontend)
199
201
 
@@ -222,7 +222,7 @@ File: `packages/server/src/routes/web.ts`
222
222
  | POST | `/api/web/stop` | Stop running task → `{stopped: boolean}` |
223
223
  | GET | `/api/web/history` | Active session history → `{sessionId, messages[], running}` |
224
224
  | GET | `/api/web/subscribe` | Reconnect SSE to running session |
225
- | GET | `/api/web/file?path=` | Inline-serve a workspace-relative file (image / video / pdf etc.). Path-traversal-checked against the token's bound workspace. |
225
+ | GET | `/api/web/file?path=` | Inline-serve a workspace-relative file (image / video / pdf etc.). Path-traversal-checked against the token's bound workspace: the lexical check is re-verified against the realpath'd root, so a symlink inside the workspace pointing outside it returns 403; a dangling symlink (target doesn't exist) returns 404. |
226
226
 
227
227
  See [design/web.md](../design/web.md).
228
228
 
@@ -238,6 +238,8 @@ runtime so the frontend can render rooms (workspaces) + characters (sessions).
238
238
  | GET | `/api/show/state` | `full` or `observer` token → every known workspace; otherwise the account's own. Returns `{ serverTime, uptime, accessLevel, skills[], workspaces[] }` |
239
239
  | GET | `/api/show/session?ws=&id=` | Inspector-panel detail for a single session. Trimmed message log (last 40, content/tool I/O capped) plus `contextTokens` / `outputTokens` / `maxContextTokens` / `isRunning`. Non-`full`/`observer` tokens may only read their own workspace. |
240
240
 
241
+ `observer` is more than an aggregate-counts role: past `/show/state`, it can also call `/show/session` to read **any workspace's** session transcript (last 40 messages, content truncated to 600 chars, tool input to 200 chars). Mint it knowing it grants cross-workspace transcript read access, not just dashboard counters.
242
+
241
243
  Each `workspace` = `{ path, key, label, counts{running,idle,stopped}, totalSessions, skills[], sessions[] }`; each `session` = `{ id, parentId, depth, agentName, description, status, lastTool, activeSkill, contextTokens, outputTokens, messageCount, updatedAt }`. `lastTool` / `activeSkill` come from the live in-memory UI log (empty when the session isn't loaded). Sessions per room are capped (`totalSessions` reports the true total). Frontend: `halo-city/` at repo root.
242
244
 
243
245
  **Read-only by construction — degraded snapshots.** Both endpoints resolve a workspace's runtime via `registry.peek()` — in-memory lookup only, **never** `getOrCreate`: constructing a SessionManager has write side effects (boot orphan-reconciliation batch-stops live sub-session rows; `.halo/` scaffolding), so a pure visualization poll must not trigger it. When no live SessionManager exists in this process, the handler opens that workspace's `.halo/halo.db` **read-only** (cached connection; missing db/tables silently degrade to an empty room) and projects rows / the persisted session file into the same wire shape. Sessions not in memory therefore return a **persisted snapshot**: `status` / token counts / `messageCount` may lag behind the last persistence point, and the live-only signals (`lastTool` / `activeSkill`) are empty by definition. The degraded `/api/show/session` path reports `maxContextTokens: 0` (the real cap needs a live agent config; the city inspector hides the meter when the cap is unknown).
@@ -152,6 +152,14 @@ This installs the `halo` binary on `$PATH`. Subcommands available:
152
152
  1. `halo upgrade` — bumps the on-disk npm package
153
153
  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. 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.
154
154
 
155
+ ### Release checklist (before `npm publish`)
156
+
157
+ 1. **Bump version** in root `package.json` (and any workspace `package.json` that mirrors it).
158
+ 2. **Update `CHANGELOG.md`**: rename `[Unreleased]` → `[x.y.z] - YYYY-MM-DD`, add a fresh empty `[Unreleased]` section above it.
159
+ 3. **Bump `TEMPLATE_VERSION`** in `packages/server/src/init.ts` if any file under `templates/` was touched.
160
+ 4. **Build admin**: `pnpm --filter @turmind/halo-admin build` — verify `admin/out/monaco/vs/loader.js` exists.
161
+ 5. Commit, tag `vx.y.z`, push, then publish.
162
+
155
163
  The published package contains a single bundled JS entry (~620 KB), 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.
156
164
 
157
165
  ### Non-interactive (Docker / CI) details
@@ -196,3 +204,5 @@ server {
196
204
  }
197
205
  }
198
206
  ```
207
+
208
+ Behind a reverse proxy, set `general.server.trust_proxy: true` in `settings.yaml` (global scope only). Without it, brute-force rate-limiting / lockout resolves the client IP from the direct socket address — which behind a proxy is the proxy's own IP, so every client collapses into one bucket and can't be told apart. Only enable this when the proxy in front is one you control and it rewrites `x-forwarded-for` itself; otherwise a client can forge the header and bypass lockouts. Default is `false` (direct-connect deployments).
@@ -108,8 +108,9 @@ settings.yaml only (no env override):
108
108
  - `general.compact.max_summary_input` (default 15000) — local compaction fallback input cap
109
109
  - `general.compact.max_message_slice` (default 800) — local compaction per-message cap
110
110
  - `general.compact.summarize_timeout_sec` (default 300) — self-compact timeout
111
- - `general.sandbox.hidden_dirs` (default `~/.halo/secrets,~/.aws,~/.ssh,~/.gnupg,~/.docker`) — bwrap tmpfs overlays, scope: global
112
- - `general.sandbox.hidden_files` (default `~/.npmrc,~/.bash_history,~/.gitconfig`) — bwrap /dev/null binds, scope: global
111
+ - `general.server.trust_proxy` (default `false`) — trust `x-forwarded-for` for client IP resolution, scope: global (enable only behind a reverse proxy you control)
112
+ - `general.sandbox.hidden_dirs` (default `~/.halo/secrets,~/.aws,~/.ssh,~/.gnupg,~/.docker,~/.config/gh`) — bwrap tmpfs overlays, scope: global
113
+ - `general.sandbox.hidden_files` (default `~/.npmrc,~/.bash_history,~/.gitconfig,~/.git-credentials,~/.netrc`) — bwrap /dev/null binds, scope: global
113
114
  - `general.logging.level` (default `warn`) — log level: debug | info | warn | error
114
115
 
115
116
  `<provider-id>.secrets.*` (server-only, hard-rejected by `{{}}` substitution; declared in `models/<provider-id>.yaml` `secrets:`):
@@ -137,10 +137,10 @@ Configured in `settings.yaml` under `general.sandbox`:
137
137
 
138
138
  | Setting | Default | Method |
139
139
  |---|---|---|
140
- | `hidden_dirs` | `~/.halo/secrets,~/.aws,~/.ssh,~/.gnupg,~/.docker` | `--tmpfs` overlay (empty directory) |
141
- | `hidden_files` | `~/.npmrc,~/.bash_history,~/.gitconfig` | `--ro-bind /dev/null` (empty file) |
140
+ | `hidden_dirs` | `~/.halo/secrets,~/.aws,~/.ssh,~/.gnupg,~/.docker,~/.config/gh` | `--tmpfs` overlay (empty directory) |
141
+ | `hidden_files` | `~/.npmrc,~/.bash_history,~/.gitconfig,~/.git-credentials,~/.netrc` | `--ro-bind /dev/null` (empty file) |
142
142
 
143
- Changes take effect immediately — `config.ts` reads settings.yaml via an mtime-watched lazy cache, so the next `shell_exec` reads the latest values. A workspace-level `settings.yaml` can override these the same way it overrides any other key (no global-scope lock).
143
+ Changes take effect immediately — `config.ts` reads settings.yaml via an mtime-watched lazy cache, so the next `shell_exec` reads the latest values. These keys are `globalOnly` in the schema — a workspace `settings.yaml` cannot override them, since they define the security boundary agents run inside.
144
144
 
145
145
  `/tmp` is not in the hidden list — it receives a standalone `--tmpfs` mount for process isolation (each bwrap invocation gets its own empty `/tmp`), not for hiding secrets.
146
146
 
@@ -172,6 +172,7 @@ Start a sub-agent session asynchronously. When the sub-agent finishes, its **wra
172
172
  | `agent_id` | string | yes | Agent ID (e.g. `"coder"`) |
173
173
  | `message` | string | yes | Task description |
174
174
  | `system_prompt_context` | string | no | Extra context prepended to the initial message |
175
+ | `title` | string | no | Session title shown in the admin sidebar. When omitted, auto-generated from the task message (first 60 chars of `description`). |
175
176
  | `working_dir` | string | no | Sub-agent's focus directory (absolute or workspace-relative; default: project root). It's persistent session identity (stored in the DB, restored on resume), so the directory-chain `.halo/INSTRUCTIONS.md` along the path root→dir is baked into the sub-agent's **system prompt every turn** (it never forgets the directory's rules), and the prompt is tagged with this focus. Does **not** change where tools run. |
176
177
 
177
178
  **Output (JSON string)**
@@ -134,6 +134,8 @@ Environment variables:
134
134
 
135
135
  The demo holds your token server-side and exposes its own password-based session — the browser never sees the halo token. Open `http://localhost:9528` to use it.
136
136
 
137
+ Alternatively, the gear icon (header / login screen) opens **direct-connect mode**: enter any halo server's URL + a web-channel token and the browser talks to that server's `/api/web/*` directly, skipping the proxy and its password. Handy for pointing one deployed web-demo at several halo servers. The token is kept in that browser's localStorage — only use it on devices you trust; clear it in the same panel to return to proxy mode.
138
+
137
139
  ## Security notes
138
140
 
139
141
  - **Tokens are unhashed** in `~/.halo/secrets/channels/channels.db`. Restrict that file to the user that runs halo
@@ -7,6 +7,8 @@ Global + workspace configuration. The shape mirrors VSCode's `contributes.config
7
7
  ```yaml
8
8
  # ~/.halo/secrets/settings.yaml — values only
9
9
  general: # built-in declarer (server itself)
10
+ server:
11
+ trust_proxy: false # true only behind a reverse proxy you control — see below
10
12
  session:
11
13
  max_queue_size: 256
12
14
  max_nesting_depth: 16
@@ -15,8 +17,8 @@ general: # built-in declarer (server itself)
15
17
  max_summary_input: 15000
16
18
  ...
17
19
  sandbox:
18
- hidden_dirs: "~/.halo/secrets,~/.aws,~/.ssh,~/.gnupg,~/.docker"
19
- hidden_files: "~/.npmrc,~/.bash_history,~/.gitconfig"
20
+ hidden_dirs: "~/.halo/secrets,~/.aws,~/.ssh,~/.gnupg,~/.docker,~/.config/gh"
21
+ hidden_files: "~/.npmrc,~/.bash_history,~/.gitconfig,~/.git-credentials,~/.netrc"
20
22
  writable_dirs: "" # e.g. ~/.kiro,~/.local/share/kiro-cli — rw bind-mounts in bwrap; ignored for readonly sessions
21
23
  logging:
22
24
  level: warn
@@ -91,7 +93,11 @@ secrets: []
91
93
 
92
94
  ### General — built-in
93
95
 
94
- Declared in [packages/server/src/settings-schema.ts](../../../packages/server/src/settings-schema.ts) `generalSection()`. The server itself is the implicit declarer. Keys: `language`, `agent.*`, `session.*`, `compact.*`, `sandbox.*`, `logging.*`, `evolution.*`, `limits.*`.
96
+ Declared in [packages/server/src/settings-schema.ts](../../../packages/server/src/settings-schema.ts) `generalSection()`. The server itself is the implicit declarer. Keys: `language`, `agent.*`, `server.*`, `session.*`, `compact.*`, `sandbox.*`, `logging.*`, `evolution.*`, `limits.*`.
97
+
98
+ `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.
99
+
100
+ `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).
95
101
 
96
102
  `evolution.*` controls the self-evolution subsystem (see [plans/self-evolution.md](../plans/self-evolution.md)). All `evolution.*` keys are `globalOnly` — they live in `~/.halo/secrets/settings.yaml` only, not workspace settings. Notable knobs: `evolution.level` (`L0` = off, `L1` = human + LLM assist), `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).
97
103
 
@@ -106,6 +112,7 @@ Declared in [packages/server/src/settings-schema.ts](../../../packages/server/sr
106
112
  | `description_zh` | no | Chinese description (UI picks based on lang) |
107
113
  | `default` | no | Placeholder shown when the value is unset; supports `<<ENV>>` |
108
114
  | `secret` | no | `true` → masked in API responses + password input in UI |
115
+ | `globalOnly` | no | `true` → read from global settings only; workspace overrides are ignored at runtime. UI disables the workspace input and shows a "global only" hint |
109
116
 
110
117
  ## Scope: global vs. workspace
111
118