@turmind/halo 1.0.1 → 1.0.3

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 (37) hide show
  1. package/admin-out/404.html +1 -1
  2. package/admin-out/_next/static/chunks/212-c5f046048be9c367.js +1 -0
  3. package/admin-out/_next/static/chunks/4-e7ba0b1ced30190b.js +1 -0
  4. package/admin-out/_next/static/chunks/app/page-5a6cf2b73a12b849.js +1 -0
  5. package/admin-out/_next/static/css/62b8bf49e6404ffc.css +3 -0
  6. package/admin-out/index.html +1 -1
  7. package/admin-out/index.txt +5 -5
  8. package/bundled-docs/design/agent.md +2 -1
  9. package/bundled-docs/design/cron.md +9 -3
  10. package/bundled-docs/design/session.md +3 -1
  11. package/bundled-docs/design/storage.md +2 -1
  12. package/bundled-docs/design/ws.md +30 -8
  13. package/bundled-docs/dev/add-model-provider.md +1 -1
  14. package/bundled-docs/dev/api.md +37 -3
  15. package/bundled-docs/dev/env.md +2 -0
  16. package/bundled-docs/dev/tools.md +13 -4
  17. package/bundled-docs/guide/chat.md +1 -1
  18. package/bundled-docs/guide/workspace.md +2 -2
  19. package/bundled-docs/requirements/chat.md +1 -1
  20. package/bundled-docs/requirements/settings.md +8 -0
  21. package/dist/index.js +440 -263
  22. package/package.json +1 -1
  23. package/templates/agents/__apply_agent__/agent.yaml +1 -1
  24. package/templates/agents/__evo_agent__/agent.yaml +1 -1
  25. package/templates/agents/__score__/agent.yaml +1 -1
  26. package/templates/agents/deep-executor/agent.yaml +1 -1
  27. package/templates/agents/default/agent.yaml +1 -1
  28. package/templates/agents/goal/agent.yaml +1 -1
  29. package/templates/models/aws-bedrock-claude-invoke.yaml +27 -0
  30. package/templates/models/aws-bedrock-mantle.yaml +13 -2
  31. package/templates/skills/agent/SKILL.md +2 -2
  32. package/admin-out/_next/static/chunks/212-e217e00ce6cc2bdc.js +0 -1
  33. package/admin-out/_next/static/chunks/397-fc5a406d80d46ca7.js +0 -1
  34. package/admin-out/_next/static/chunks/app/page-19d5f2a0a3412b4a.js +0 -1
  35. package/admin-out/_next/static/css/9e7dadb24830255a.css +0 -3
  36. /package/admin-out/_next/static/{aWThs2cmYntOEy-YdkDPD → CWrUwTypkebkhwZ5i82_G}/_buildManifest.js +0 -0
  37. /package/admin-out/_next/static/{aWThs2cmYntOEy-YdkDPD → CWrUwTypkebkhwZ5i82_G}/_ssgManifest.js +0 -0
@@ -2,7 +2,7 @@
2
2
 
3
3
  All REST endpoints are served by Hono on port 9527 at `/api/`.
4
4
 
5
- Auth: most `/api/*` routes require a valid JWT cookie (`halo_token`). Exceptions in `PUBLIC_PATHS` (`middleware/auth.ts`) bypass the cookie: `/api/auth/*`, the web-channel routes (`/api/web/chat|stop|history|subscribe|file`), `/api/show/state|session`, and `/api/metrics` — these are unauthenticated or use a web-channel `x-token` instead.
5
+ Auth: most `/api/*` routes require a valid JWT cookie (`halo_token`). Exceptions in `PUBLIC_PATHS` (`middleware/auth.ts`) bypass the cookie: `/api/auth/login|check|logout` (but **not** `/api/auth/change-password`), the web-channel routes (`/api/web/chat|stop|history|subscribe|file`), `/api/show/state|session`, and `/api/metrics` — these are unauthenticated or use a web-channel `x-token` instead.
6
6
 
7
7
  ## Health
8
8
 
@@ -17,8 +17,9 @@ File: `packages/server/src/middleware/auth.ts`
17
17
  | Method | Path | Purpose |
18
18
  |---|---|---|
19
19
  | POST | `/api/auth/login` | Password login, returns a JWT cookie |
20
- | POST | `/api/auth/logout` | Clears the auth cookie |
20
+ | POST | `/api/auth/logout` | Clears the auth cookie (Set-Cookie `Max-Age=0`). In `PUBLIC_PATHS`. No server-side token blacklist — a copied JWT stays valid until natural expiry |
21
21
  | GET | `/api/auth/check` | Validates the current token; refreshes stale tokens |
22
+ | POST | `/api/auth/change-password` | Change the admin password. **Not** in `PUBLIC_PATHS` — requires a valid JWT cookie. Persists the new scrypt hash to `~/.halo/secrets/config.yaml`; takes effect immediately (no restart), does **not** rotate `jwt_secret` so existing sessions stay signed in |
22
23
 
23
24
  ## Files
24
25
 
@@ -169,7 +170,7 @@ File: `packages/server/src/routes/git.ts`. Admin cookie auth. Backs the Source C
169
170
  | Method | Path | Purpose |
170
171
  |---|---|---|
171
172
  | GET | `/api/git/status?projectId=` | Structured working-tree status. A folder that isn't a git work-tree **root** (no repo, or merely nested inside an ancestor's repo) returns `{isRepo: false}` (200, not 500) so the panel shows its initialize/empty state. Otherwise `{isRepo: true, branch, tracking, ahead, behind, files: [{path, index, workingDir, from?}]}` — `index`/`workingDir` are the two porcelain status chars (X staged / Y working) |
172
- | GET | `/api/git/ignored?projectId=` | `.gitignore`'d paths for graying out in the explorer; ignored directories are collapsed to a single entry (e.g. `node_modules`) rather than expanded. Returns `{ignored: string[]}`. Runs with `core.quotepath=false` so non-ASCII (e.g. Chinese) paths come back literal, not octal-escaped — the frontend prefix-matches them against tree node paths |
173
+ | GET | `/api/git/ignored?projectId=` | `.gitignore`'d paths for graying out in the explorer; ignored directories are collapsed to a single entry (e.g. `node_modules`) rather than expanded. Returns `{ignored: string[]}` (`{isRepo:false, ignored:[]}` when the folder isn't a work-tree root — same guard as `status`, applied to all six read endpoints so a workspace nested inside an ancestor's repo never leaks its state). Runs with `core.quotepath=false` so non-ASCII (e.g. Chinese) paths come back literal, not octal-escaped — the frontend prefix-matches them against tree node paths |
173
174
  | GET | `/api/git/diff?projectId=&path=[&staged=0\|1][&from=][&commit=]` | Two sides for the Monaco diff editor. With `commit`, shows that commit's own change (parent vs commit); without it, the working-tree (or `staged=1`) diff. `from` carries the old path on a rename. Returns `{path, original, modified, ...}`. 400 if `path` missing, 403 on traversal |
174
175
  | GET | `/api/git/log?projectId=[&limit=50]` | Recent commits for the graph. `limit` default 50, capped at 2000. Returns `{commits: [{hash, shortHash, message, author, date, refs, pushed}]}` — `pushed: boolean` marks whether the commit is on a remote (graph dims pushed nodes gray, paints local-only ones blue). No-upstream branches compute it via `rev-list HEAD --not --remotes`, not `rev-list HEAD` |
175
176
  | GET | `/api/git/commit-files?projectId=&hash=` | Files changed by one commit. Returns `{files: [{path, status, from?}]}` (`status` = M/A/D/R/C; `from` set on rename/copy). 400 if `hash` missing |
@@ -360,6 +361,26 @@ Source: [packages/server/src/middleware/auth.ts:123-134](../../../packages/serve
360
361
  { "authenticated": false }
361
362
  ```
362
363
 
364
+ ### POST `/api/auth/change-password`
365
+
366
+ Source: [packages/server/src/middleware/auth.ts](../../../packages/server/src/middleware/auth.ts). Admin-cookie authed (not in `PUBLIC_PATHS`). Validation order: wrong current password → 401; strength (≥8 chars, at least one letter and one digit) → 400 with the specific reason; new === old → 400. When `HALO_PASSWORD` env is set it overrides the stored hash at login, so a file rewrite would silently not take effect — the endpoint rejects with 400 instead.
367
+
368
+ ```json
369
+ // Request
370
+ { "oldPassword": "...", "newPassword": "..." }
371
+
372
+ // 200 OK — new scrypt hash persisted to ~/.halo/secrets/config.yaml,
373
+ // live for the next login without restart; jwt_secret is NOT rotated
374
+ // (existing cookies stay valid)
375
+ { "ok": true }
376
+
377
+ // 401 wrong current password
378
+ { "error": "Incorrect current password" }
379
+
380
+ // 400 strength / same-as-old / env-managed (message names the reason)
381
+ { "error": "Password must be at least 8 characters" }
382
+ ```
383
+
363
384
  ### GET `/api/files/tree?projectId=<absPath>&path=<relPath>`
364
385
 
365
386
  Source: [packages/server/src/routes/files.ts](../../../packages/server/src/routes/files.ts). One directory level, lazy.
@@ -683,6 +704,18 @@ Attach this connection to a session's event stream. Sent on initial connect and
683
704
  }
684
705
  ```
685
706
 
707
+ Idempotent per connection: re-subscribing to the same session releases this connection's previous listener and registers exactly one new one — safe to repeat. The client must also re-send it on receiving `listener:released` (see below).
708
+
709
+ ### `listener:released` (S→C)
710
+
711
+ The server reclaimed this connection's event listener — sent when the connection was silent for >3 min (no inbound frames; e.g. a frozen tab whose network stack still answers protocol pings) or found CLOSED with a listener attached. The socket stays open; only the event stream is detached.
712
+
713
+ ```json
714
+ { "type": "listener:released", "sessionId": "sid_abc" }
715
+ ```
716
+
717
+ On receipt the client re-sends `subscribe` to reattach — this frame is the only recovery signal a frozen-and-resumed tab gets, because the server's `__pong__` replies keep the client's own staleness detection from ever firing. Any WS consumer must send `__ping__` periodically (any inbound frame counts) or it will be reclaimed. See [design/ws.md](../design/ws.md#abandoned-listener-reclaim-and-the-__ping__-contract).
718
+
686
719
  ### `session:clear` (C→S)
687
720
 
688
721
  Detach from the current session (for `/session new`). Server unsubscribes the event listener without deleting the session.
@@ -712,5 +745,6 @@ Hard delete (JSON + SQLite rows, cascades to descendants) goes through the REST
712
745
  | `complete` | `stopReason`, `taskId?` | Turn finished |
713
746
  | `error` | `error` | Error message |
714
747
  | `chat:queued` | `reason`, `message` | Message queued (compact/busy) |
748
+ | `listener:released` | `sessionId` | Event listener reclaimed — re-send `subscribe` (see above) |
715
749
 
716
750
  Events with `taskId` set belong to sub-agent turns (nested sessions); events without are the root agent's. Full list in [design/ws.md](../design/ws.md).
@@ -156,6 +156,8 @@ Two ways to authenticate:
156
156
  1. **Interactive** — run `halo setup`, pick "Set / change password". The value is scrypt-hashed and written to `~/.halo/secrets/config.yaml server.password`. Best for personal installs.
157
157
  2. **Env** — export `HALO_PASSWORD=...` before launching the server. The plaintext value is compared directly (no hashing) and takes precedence over any stored hash. A stored hash isn't required at all — env plaintext alone satisfies the startup gate, so env-only deployments are legal. Best for Docker / systemd / CI where a secrets manager already protects the env.
158
158
 
159
+ Once logged in, the password can also be changed from the panel (**Settings → Security**, requires the current password) — except when `HALO_PASSWORD` env manages it, in which case the endpoint refuses (the env value would keep winning at login).
160
+
159
161
  ## Verify
160
162
 
161
163
  ```bash
@@ -117,8 +117,8 @@ Each session carries `accessLevel: 'readonly' | 'workspace' | null` (persisted i
117
117
  | Level | DB value | Tools (with bwrap) | Tools (without bwrap) | Sandbox |
118
118
  |---|---|---|---|---|
119
119
  | `null` (full) | `full` | All 9 tools | All 9 tools | None |
120
- | `workspace` | `workspace` | All 9 tools | All 9 tools | bwrap: workspace rw, sensitive paths hidden |
121
- | `readonly` | `readonly` | All 9 tools | file_read, view_image, file_list, grep, glob (5 tools) | bwrap: workspace ro, sensitive paths hidden |
120
+ | `workspace` | `workspace` | All 9 tools | All 9 tools | bwrap: workspace rw, sensitive paths + workspace runtime state hidden |
121
+ | `readonly` | `readonly` | All 9 tools | file_read, view_image, file_list, grep, glob (5 tools) | bwrap: workspace ro, sensitive paths + workspace runtime state hidden |
122
122
 
123
123
  `view_image` is additionally gated on `capabilities.image` (see its section above). Models without vision support get the same lists minus `view_image` — so a non-vision model on `readonly` ends up with 4 tools, not 5.
124
124
 
@@ -131,9 +131,9 @@ Dependency: `bubblewrap` (`apt install bubblewrap`) on Linux. Without it, only l
131
131
 
132
132
  ### Sandbox hidden paths
133
133
 
134
- Sensitive directories and files are hidden from workspace/readonly sessions via bwrap overlays. Paths that don't exist on the filesystem are silently skipped.
134
+ Sensitive directories and files are hidden from workspace/readonly sessions via bwrap overlays (and the same lists gate `assertPathAllowed` on the no-bwrap fallback). Paths that don't exist on the filesystem are silently skipped. Two categories coexist:
135
135
 
136
- Configured in `settings.yaml` under `general.sandbox`:
136
+ **Global lists (configurable)** — credentials and cross-workspace state, configured in `settings.yaml` under `general.sandbox`:
137
137
 
138
138
  | Setting | Default | Method |
139
139
  |---|---|---|
@@ -143,6 +143,15 @@ Configured in `settings.yaml` under `general.sandbox`:
143
143
 
144
144
  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.
145
145
 
146
+ **Workspace-relative set (hardcoded)** — the workspace's own runtime state, which holds other channels'/users' conversations on a shared workspace. Code constants in `sandbox.ts` (`WORKSPACE_HIDDEN_DIRS` / `WORKSPACE_HIDDEN_FILES`), deliberately not settings: this is a security boundary (a config edit must not be able to open it), and the entries are workspace-relative while the settings lists are absolute/`~` paths.
147
+
148
+ | Constant | Entries | Method |
149
+ |---|---|---|
150
+ | `WORKSPACE_HIDDEN_DIRS` | `.halo/sessions` (session transcripts), `.halo/logs`, `.halo/evo` (run dirs contain full source-session snapshots) | `--tmpfs` overlay |
151
+ | `WORKSPACE_HIDDEN_FILES` | `.halo/halo.db` + `-wal`/`-shm` (sqlite `agent_sessions` rows) | `--ro-bind /dev/null` |
152
+
153
+ The rest of `.halo/` (INSTRUCTIONS.md, INDEX.md, docs/, memory/, skills/, agents/, prompts/, tmp/, canvas/, goal/, settings.yaml) stays readable — it's workspace knowledge agents need to work. `full` sessions bypass the sandbox entirely and see everything.
154
+
146
155
  `/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.
147
156
 
148
157
  Per-channel defaults:
@@ -74,7 +74,7 @@ If you're chatting from WeChat, the same shared commands are available (routed t
74
74
  | `/qr [level]` | Generate an invite QR for a new bot account (full 权限 bot 专用) |
75
75
  | `/help` | Show help |
76
76
 
77
- If the session is currently compacting or busy when your message arrives, you'll see a hint — ("⏳ integrating context…" / "🔄 queued…") — and the message is queued (while compacting it's dropped; while busy it's kept).
77
+ If the session is currently compacting or busy when your message arrives, you'll see a hint — ("⏳ integrating context…" / "🔄 queued…") — and the message is queued either way, processed as soon as the compact / current turn finishes.
78
78
 
79
79
  ## Interrupt vs Stop
80
80
 
@@ -126,6 +126,6 @@ Two tabs in the header: Chat / Terminal. Current tab lives in `editorStore.botto
126
126
  The login page runs through `POST /api/auth/login`; the JWT lives in an HTTP-only cookie:
127
127
  - 14-day expiry
128
128
  - Auto-refresh after 24 hours of access
129
- - Click the avatar in the top-right to log out
129
+ - Log out via **Settings → Security** (returns to the login page; this browser only)
130
130
 
131
- **Where to set the password**: `~/.halo/secrets/config.yaml`'s `server.password`, or the `HALO_PASSWORD` env var.
131
+ **Password**: set initially by `halo setup` (or the `HALO_PASSWORD` env var); change it later in **Settings → Security** (needs the current password). Forgot it? Re-run `halo setup` to reset.
@@ -66,7 +66,7 @@ When `contextEnabled` is on (default), user messages are auto-prepended with:
66
66
  Images ride along as base64; multimodal supported. Pasted images are also persisted to `<workspace>/.halo/assets/web/inbound/web/<date>/` so a `[图片已保存: /abs/path]` marker survives page reload and renders as a click-to-preview chip (shared with the WeChat channel's inbound media flow).
67
67
 
68
68
  ### Inline media chips
69
- Any message containing `[图片/视频/语音/文件 已保存: /path]` markers (WeChat + web) or a leading `MEDIA: /path` line (agent-emitted, e.g. from `wechat-send`) renders a compact chip with filename + icon. Clicking opens a full-size preview modal (image/video/audio inline, file → download link). Paths inside the active workspace or under `/tmp/` are previewable; everything else degrades to a non-clickable chip.
69
+ Any message containing `[图片/视频/语音/文件 已保存: /path]` markers (WeChat + web) or a leading `MEDIA: /path` line (agent-emitted, e.g. from `wechat-send`) renders a compact chip with filename + icon. Clicking opens a full-size preview modal (image/video/audio inline, file → download link). The modal has a Download button (top-right, next to close) for image/video/audio; the media URL carries a per-open cache-buster (`&t=<timestamp>`) so overwritten files (same path, new bytes) always show current content. Paths inside the active workspace or under `/tmp/` are previewable; everything else degrades to a non-clickable chip.
70
70
 
71
71
  ### Live capture (desktop only)
72
72
  Lets the agent *see something live* on demand. Desktop client (Electron) only — the entry points never render in a plain browser. Borrows the meeting-app "share" model: the user binds one source, then the agent requests a frame when it actually needs to look.
@@ -164,6 +164,14 @@ Values present in `settings.yaml` whose namespace doesn't appear in any current
164
164
 
165
165
  `general.*` is intentionally excluded from orphan detection — its declared keys are enumerated by the built-in schema, so anything else there is treated as either a typo or a forward-compat field, not an orphan.
166
166
 
167
+ ## Security view (change password + logout)
168
+
169
+ A **Security** entry in the left nav (below the System group) opens a page with two cards. Like `__orphans` it's a synthetic nav target, not a schema section — the credential lives in `~/.halo/secrets/config.yaml` (`server.password`, scrypt hash), not `settings.yaml`, and the header shows that path accordingly.
170
+
171
+ **Change password** — three inputs: current password, new password, confirm. Live client-side feedback while typing: strength rule (≥8 chars, at least one letter and one digit), new ≠ current, confirm matches; the submit button stays disabled until all pass. Submit posts to `POST /api/auth/change-password` (see [dev/api.md](../dev/api.md)) — the server re-runs the same checks authoritatively; a server rejection is shown verbatim under the form. Success shows an inline confirmation and clears all three fields. Existing sessions stay signed in (`jwt_secret` is not rotated). Forgotten password (can't provide the current one) is out of scope here — that's `halo setup`'s reset path.
172
+
173
+ **Log out** — the login state is an httpOnly JWT cookie, so JS can't clear it directly: the button calls `POST /api/auth/logout` (server expires the cookie via Set-Cookie) and reloads; the boot auth check then lands on the login page. This browser only — no server-side token blacklist.
174
+
167
175
  ## API
168
176
 
169
177
  | Operation | Method | Endpoint | Purpose |