@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.
- package/admin-out/404.html +1 -1
- package/admin-out/_next/static/chunks/212-c5f046048be9c367.js +1 -0
- package/admin-out/_next/static/chunks/4-e7ba0b1ced30190b.js +1 -0
- package/admin-out/_next/static/chunks/app/page-5a6cf2b73a12b849.js +1 -0
- package/admin-out/_next/static/css/62b8bf49e6404ffc.css +3 -0
- package/admin-out/index.html +1 -1
- package/admin-out/index.txt +5 -5
- package/bundled-docs/design/agent.md +2 -1
- package/bundled-docs/design/cron.md +9 -3
- package/bundled-docs/design/session.md +3 -1
- package/bundled-docs/design/storage.md +2 -1
- package/bundled-docs/design/ws.md +30 -8
- package/bundled-docs/dev/add-model-provider.md +1 -1
- package/bundled-docs/dev/api.md +37 -3
- package/bundled-docs/dev/env.md +2 -0
- package/bundled-docs/dev/tools.md +13 -4
- package/bundled-docs/guide/chat.md +1 -1
- package/bundled-docs/guide/workspace.md +2 -2
- package/bundled-docs/requirements/chat.md +1 -1
- package/bundled-docs/requirements/settings.md +8 -0
- package/dist/index.js +440 -263
- package/package.json +1 -1
- package/templates/agents/__apply_agent__/agent.yaml +1 -1
- package/templates/agents/__evo_agent__/agent.yaml +1 -1
- package/templates/agents/__score__/agent.yaml +1 -1
- package/templates/agents/deep-executor/agent.yaml +1 -1
- package/templates/agents/default/agent.yaml +1 -1
- package/templates/agents/goal/agent.yaml +1 -1
- package/templates/models/aws-bedrock-claude-invoke.yaml +27 -0
- package/templates/models/aws-bedrock-mantle.yaml +13 -2
- package/templates/skills/agent/SKILL.md +2 -2
- package/admin-out/_next/static/chunks/212-e217e00ce6cc2bdc.js +0 -1
- package/admin-out/_next/static/chunks/397-fc5a406d80d46ca7.js +0 -1
- package/admin-out/_next/static/chunks/app/page-19d5f2a0a3412b4a.js +0 -1
- package/admin-out/_next/static/css/9e7dadb24830255a.css +0 -3
- /package/admin-out/_next/static/{aWThs2cmYntOEy-YdkDPD → CWrUwTypkebkhwZ5i82_G}/_buildManifest.js +0 -0
- /package/admin-out/_next/static/{aWThs2cmYntOEy-YdkDPD → CWrUwTypkebkhwZ5i82_G}/_ssgManifest.js +0 -0
package/bundled-docs/dev/api.md
CHANGED
|
@@ -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
|
|
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[]}
|
|
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).
|
package/bundled-docs/dev/env.md
CHANGED
|
@@ -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
|
-
|
|
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
|
|
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
|
-
-
|
|
129
|
+
- Log out via **Settings → Security** (returns to the login page; this browser only)
|
|
130
130
|
|
|
131
|
-
**
|
|
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 |
|