@turmind/halo 1.5.3-alpha → 1.5.5
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/{133-33aa091fba853037.js → 422-cc74b1fdd3af5415.js} +1 -1
- package/admin-out/_next/static/chunks/989-bc743317498d3fbc.js +1 -0
- package/admin-out/_next/static/chunks/app/page-0a390059f1914664.js +1 -0
- package/admin-out/_next/static/chunks/{webpack-1a264efd965db7ab.js → webpack-edf248e07333566d.js} +1 -1
- package/admin-out/_next/static/css/b53542687ebd4c65.css +3 -0
- package/admin-out/index.html +1 -1
- package/admin-out/index.txt +5 -5
- package/bundled-docs/design/agent.md +4 -4
- package/bundled-docs/design/agentcore.md +13 -10
- package/bundled-docs/design/architecture.md +24 -14
- package/bundled-docs/design/command.md +5 -5
- package/bundled-docs/design/cron.md +2 -2
- package/bundled-docs/design/evolution.md +34 -35
- package/bundled-docs/design/express-self.md +6 -4
- package/bundled-docs/design/feishu.md +18 -14
- package/bundled-docs/design/halo-city.md +5 -4
- package/bundled-docs/design/observability.md +9 -9
- package/bundled-docs/design/prompt-system.md +12 -12
- package/bundled-docs/design/session.md +12 -12
- package/bundled-docs/design/slack.md +22 -20
- package/bundled-docs/design/storage.md +18 -19
- package/bundled-docs/design/telegram.md +26 -8
- package/bundled-docs/design/web.md +3 -1
- package/bundled-docs/design/wechat.md +8 -6
- package/bundled-docs/design/wecom.md +23 -9
- package/bundled-docs/design/ws.md +6 -6
- package/bundled-docs/dev/acp-adapter.md +8 -8
- package/bundled-docs/dev/add-channel.md +12 -9
- package/bundled-docs/dev/add-model-provider.md +31 -20
- package/bundled-docs/dev/api.md +83 -57
- package/bundled-docs/dev/deploy.md +8 -6
- package/bundled-docs/dev/env.md +16 -11
- package/bundled-docs/dev/logging.md +15 -29
- package/bundled-docs/dev/previews.md +20 -11
- package/bundled-docs/dev/tools.md +24 -12
- package/bundled-docs/guide/README.md +1 -0
- package/bundled-docs/guide/agents.md +19 -53
- package/bundled-docs/guide/channels/README.md +7 -4
- package/bundled-docs/guide/channels/acp.md +3 -1
- package/bundled-docs/guide/channels/feishu.md +13 -11
- package/bundled-docs/guide/channels/slack.md +12 -10
- package/bundled-docs/guide/channels/telegram.md +21 -27
- package/bundled-docs/guide/channels/web.md +15 -11
- package/bundled-docs/guide/channels/wechat.md +15 -14
- package/bundled-docs/guide/channels/wecom.md +7 -2
- package/bundled-docs/guide/chat.md +11 -10
- package/bundled-docs/guide/cli.md +10 -8
- package/bundled-docs/guide/getting-started.md +3 -3
- package/bundled-docs/guide/secrets-and-credentials.md +3 -3
- package/bundled-docs/guide/sessions.md +3 -3
- package/bundled-docs/guide/skills.md +8 -6
- package/bundled-docs/guide/testing-agents-and-skills.md +14 -15
- package/bundled-docs/guide/workspace.md +27 -15
- package/bundled-docs/requirements/agent.md +19 -18
- package/bundled-docs/requirements/chat.md +19 -21
- package/bundled-docs/requirements/command.md +10 -10
- package/bundled-docs/requirements/editor.md +10 -9
- package/bundled-docs/requirements/explorer.md +10 -7
- package/bundled-docs/requirements/session.md +16 -17
- package/bundled-docs/requirements/settings.md +18 -14
- package/bundled-docs/requirements/skill.md +4 -4
- package/bundled-docs/requirements/source-control.md +4 -3
- package/bundled-docs/requirements/terminal.md +13 -13
- package/bundled-docs/requirements/workspace.md +19 -13
- package/dist/index.js +2038 -3558
- package/package.json +1 -1
- package/templates/config.yaml +6 -0
- package/templates/skills/workspace/share.md +11 -3
- package/templates/skills/workspace/stage.py +85 -5
- package/admin-out/_next/static/chunks/989-c6c7772db8a50926.js +0 -1
- package/admin-out/_next/static/chunks/app/page-5f41b086cd5ad353.js +0 -1
- package/admin-out/_next/static/css/8809d33379458514.css +0 -3
- /package/admin-out/_next/static/{Wqi_ugna9MYsiV7ECuawQ → cuVcldfdg1PXevaQMQSzb}/_buildManifest.js +0 -0
- /package/admin-out/_next/static/{Wqi_ugna9MYsiV7ECuawQ → cuVcldfdg1PXevaQMQSzb}/_ssgManifest.js +0 -0
package/bundled-docs/dev/api.md
CHANGED
|
@@ -105,13 +105,14 @@ Unified session log API — list + read session files across all agents.
|
|
|
105
105
|
|
|
106
106
|
| Method | Path | Purpose |
|
|
107
107
|
|---|---|---|
|
|
108
|
-
| GET | `/api/sessions/logs?projectId=` | List session metadata, keyset-paginated. Default returns each top-level row + all descendants (sidebar tree); `rootOnly=1` returns roots only (chat-panel session tabs).
|
|
108
|
+
| GET | `/api/sessions/logs?projectId=` | List session metadata, keyset-paginated. Default returns each top-level row + all descendants (sidebar tree); `rootOnly=1` returns roots only (chat-panel session tabs). A `projectId` with no `.halo/` (plain or missing directory) returns `{sessions: [], nextCursor: null}` without scaffolding it — a list never turns a directory into a workspace |
|
|
109
109
|
| GET | `/api/sessions/logs/:id?projectId=` | Full session log (scans across agent dirs) — the **active** file only; archived history is a separate call |
|
|
110
110
|
| GET | `/api/sessions/logs/:id/archive/:n?projectId=` | One archived UI-log segment — see [detail](#get-apisessionslogsidarchivenprojectidabs) |
|
|
111
|
-
| DELETE | `/api/sessions/logs/:id?projectId=` |
|
|
112
|
-
| PATCH | `/api/sessions/logs/:id?projectId=` | Rename a session (admin-only) — updates the log file's title and the mirrored `agent_sessions.title`. Accepts any session id (root or sub-agent); the sidebar exposes the rename affordance on every row |
|
|
111
|
+
| DELETE | `/api/sessions/logs/:id?projectId=` | Permanently delete the session and all its descendants — log files (with archive segments) and `agent_sessions` rows. Returns `{ok, deleted}` |
|
|
112
|
+
| PATCH | `/api/sessions/logs/:id?projectId=` | Rename a session (admin-only; body `{title}`) — updates the log file's title and the mirrored `agent_sessions.title`. Accepts any session id (root or sub-agent); the sidebar exposes the rename affordance on every row |
|
|
113
|
+
| GET | `/api/sessions/goal?projectId=` | Legacy goal binding of the workspace (`{goal: {goalSessionId, workerSessionId, status, round, maxRounds} \| null}`); refresh seed for the goal banner, which only shows when `general.goal_mode_enabled` is on |
|
|
113
114
|
|
|
114
|
-
The list endpoint returns flat metadata (id / agentId / agentName / title / timestamps / exchangeCount / parentSessionId / stoppedAt / contextTokens / totalOutputTokens / goalSessionId — the last is a retained legacy field, non-null only on a goal-bound worker row; existing bindings are unchanged), served from the mirrored `agent_sessions` columns rather than by parsing each session file (rows predating the columns are backfilled from the file on first read). The frontend builds the tree from `parentSessionId`.
|
|
115
|
+
The list endpoint returns flat metadata (id / agentId / agentName / title / timestamps / exchangeCount / parentSessionId / stoppedAt / archivedAt / contextTokens / totalOutputTokens / status / goalSessionId — the last is a retained legacy field, non-null only on a goal-bound worker row; existing bindings are unchanged), served from the mirrored `agent_sessions` columns rather than by parsing each session file (rows predating the columns are backfilled from the file on first read). The frontend builds the tree from `parentSessionId`.
|
|
115
116
|
|
|
116
117
|
The get endpoint returns the full session file. If only `rawMessages` is present (no event-log `messages`), `convertRawMessages()` transforms it into display format on the fly.
|
|
117
118
|
|
|
@@ -261,8 +262,8 @@ Every write except `/api/git/init` is gated on the project being a git work-tree
|
|
|
261
262
|
| GET | `/api/git/credentials` | List stored HTTPS credentials — `{credentials: [{host, username}]}`. **Never returns the token.** Source of truth is `~/.git-credentials` (one line per host) |
|
|
262
263
|
| POST | `/api/git/credentials` | Body `{host, username, token}`. Upserts the `https://user:token@host` line in `~/.git-credentials` (0600). 400 if any field missing |
|
|
263
264
|
| DELETE | `/api/git/credentials/:host` | Remove the credential line(s) for one host (idempotent). `:host` is sent `encodeURIComponent`'d; Hono decodes it |
|
|
264
|
-
| GET | `/api/git/ssh/keys` | Private keys found in `~/.ssh` + an `encrypted` flag each. Never returns key contents. Returns `{keys: [
|
|
265
|
-
| GET | `/api/git/ssh/agent` | ssh-agent reachability + loaded
|
|
265
|
+
| GET | `/api/git/ssh/keys` | Private keys found in `~/.ssh` + an `encrypted` flag each. Never returns key contents. Returns `{keys: [{name, path, encrypted}]}` |
|
|
266
|
+
| GET | `/api/git/ssh/agent` | ssh-agent reachability + loaded keys. Returns `{agentRunning, loadedKeys: string[]}` (`ssh-add -l` lines) |
|
|
266
267
|
| POST | `/api/git/ssh/unlock` | Body `{keyPath, passphrase}`. Loads a passphrase-protected key into the shared ssh-agent. `keyPath` must resolve to a file **directly inside `~/.ssh`** (rejects traversal / arbitrary paths, 400). Passphrase is fed to `ssh-add` via a throwaway `SSH_ASKPASS` helper (never argv / disk / log); the helper is answer-once so a wrong passphrase returns immediately. Returns `{ok}` on success, `{ok:false, error}` (normalized — never ssh-add's raw stderr) on failure |
|
|
267
268
|
| GET | `/api/git/remote/protocol?projectId=` | Current `origin` url + detected protocol. Returns `{url, protocol}` |
|
|
268
269
|
| POST | `/api/git/remote/protocol` | Body `{projectId, to: 'https'\|'ssh'}`. Rewrites `origin` between HTTPS and scp-style SSH. Returns `{ok, url}` |
|
|
@@ -301,10 +302,10 @@ gets JSON, and additionally gates on `accessLevel`.
|
|
|
301
302
|
|
|
302
303
|
| Method | Path | Purpose |
|
|
303
304
|
|---|---|---|
|
|
304
|
-
| POST | `/api/web/chat` | Send message, receive SSE stream. Body: `{message, images?, workspace?, sessionId?, agentId?}` (overrides also accepted as `?workspace=`/`?sessionId=` query or `x-workspace`/`x-session-id` headers; `workspace` only honored when token has `accessLevel: full`). `sessionId` must be owned by the token unless `accessLevel: full` → otherwise 403. |
|
|
305
|
+
| POST | `/api/web/chat` | Send message, receive SSE stream. Body: `{message, images?, workspace?, sessionId?, agentId?}` (overrides also accepted as `?workspace=`/`?sessionId=` query or `x-workspace`/`x-session-id` headers; `workspace` only honored when token has `accessLevel: full`). `sessionId` must be a safe id segment (else 400) and owned by the token unless `accessLevel: full` → otherwise 403. A client that disconnects ends its stream right away; the turn keeps running (reconnect via `/subscribe`, stop via `/stop`). |
|
|
305
306
|
| POST | `/api/web/sessions` | Mint a root session in the token's own `web_<accountId>_` namespace → `{sessionId}`. Body / query / header: `workspace?` (full tokens only), `agentId?`. Used by the ACP adapter's `session/new`. |
|
|
306
307
|
| POST | `/api/web/stop` | Stop running task → `{stopped: boolean}` |
|
|
307
|
-
| GET | `/api/web/history` | Active session history → `{sessionId, messages[], running}` |
|
|
308
|
+
| GET | `/api/web/history` | Active session history → `{sessionId, messages[], running}`. With an explicit `sessionId` that doesn't exist in the workspace → 404 `{error: "session not found"}` |
|
|
308
309
|
| GET | `/api/web/subscribe` | Reconnect SSE to running session |
|
|
309
310
|
| 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. Workspace runtime state (`.halo/sessions`, `.halo/logs`, `.halo/evo`, `halo.db*`) is hidden for every access level → 403 `{error: "path is not accessible"}`, checked on the realpath so symlinks into those dirs are caught too. |
|
|
310
311
|
|
|
@@ -321,7 +322,7 @@ runtime so the frontend can render rooms (workspaces) + characters (sessions).
|
|
|
321
322
|
| Method | Path | Purpose |
|
|
322
323
|
|---|---|---|
|
|
323
324
|
| GET | `/api/show/state` | `full` or `observer` token → every known workspace; otherwise the account's own. Returns `{ serverTime, uptime, accessLevel, skills[], workspaces[] }` |
|
|
324
|
-
| 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, and within it only sessions their own account minted (`web_<accountId>_*`); anything else is 403. |
|
|
325
|
+
| GET | `/api/show/session?ws=&id=` | Inspector-panel detail for a single session. Trimmed message log (last 40, content/tool I/O capped) plus `totalMessages` / `contextTokens` / `outputTokens` / `maxContextTokens` / `isRunning`. Non-`full`/`observer` tokens may only read their own workspace, and within it only sessions their own account minted (`web_<accountId>_*`); anything else is 403. |
|
|
325
326
|
|
|
326
327
|
`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.
|
|
327
328
|
|
|
@@ -339,20 +340,20 @@ File: `packages/server/src/routes/agent-configs.ts`
|
|
|
339
340
|
| GET | `/api/agent-configs/tools` | Available workspace tools |
|
|
340
341
|
| GET | `/api/agent-configs/models` | Available model providers and models |
|
|
341
342
|
| POST | `/api/agent-configs` | Create a new agent |
|
|
342
|
-
| GET | `/api/agent-configs/:id/yaml?scope=&projectId=` | Read agent.yaml |
|
|
343
|
-
| PUT | `/api/agent-configs/:id/yaml?scope=&projectId=` | Write agent.yaml |
|
|
344
|
-
| DELETE | `/api/agent-configs/:id?scope=&projectId=` | Delete agent (global has "last one" protection) |
|
|
343
|
+
| GET | `/api/agent-configs/:id/yaml?scope=&projectId=` | Read agent.yaml. `scope=workspace` without `projectId` → 400; `scope` other than `global` / `workspace` (omitted = `global`) → 400 `Invalid scope` |
|
|
344
|
+
| PUT | `/api/agent-configs/:id/yaml?scope=&projectId=` | Write agent.yaml. Same 400s as GET |
|
|
345
|
+
| DELETE | `/api/agent-configs/:id?scope=&projectId=` | Delete agent (global has "last one" protection). Same 400s as GET — never falls back to the same-named global agent or skips the last-one guard |
|
|
345
346
|
| PATCH | `/api/agent-configs/:id/toggle?scope=&projectId=` | Toggle disabled in workspace DB. `projectId` required. Returns `{ ok, disabled }`. |
|
|
346
347
|
| GET | `/api/agent-configs/:id/md/:fileType` | Read an MD file (AGENT.md / INSTRUCTIONS.md / INDEX.md) |
|
|
347
|
-
| PUT | `/api/agent-configs/:id/md/:fileType` | Write an MD file (AGENT.md / INSTRUCTIONS.md) |
|
|
348
|
-
| GET | `/api/agent-configs/:id/md-all` | Read every MD at once |
|
|
348
|
+
| PUT | `/api/agent-configs/:id/md/:fileType` | Write an MD file (AGENT.md / INSTRUCTIONS.md). `scope=workspace` without `projectId` → 400 (never writes the global AGENT.md / INSTRUCTIONS.md) |
|
|
349
|
+
| GET | `/api/agent-configs/:id/md-all` | Read every MD at once. `scope=workspace` without `projectId` → 400 |
|
|
349
350
|
| GET | `/api/agent-configs/:id/sessions?projectId=` | List the agent's session files |
|
|
350
351
|
| GET | `/api/agent-configs/:id/sessions/:sessionId` | Read a session |
|
|
351
352
|
| POST | `/api/agent-configs/:id/sessions` | Save / update a session |
|
|
352
353
|
| DELETE | `/api/agent-configs/:id/sessions/:sessionId` | Delete a session |
|
|
353
354
|
| DELETE | `/api/agent-configs/:id/sessions?all=1` | Delete every session for the agent |
|
|
354
355
|
|
|
355
|
-
Every `:id` / `:sessionId` route param that becomes a path segment (here and in `DELETE /api/skills/:id`) is validated with `isSafeIdSegment()` (`routes/workspace-path.ts`) before it reaches `path.join` — `.` / `..` rejected outright, everything else must match `/^[\w.:>\u4e00-\u9fff-]+$/` (the union of the real id charsets: slug + CJK agent names, `__internal__`, and session ids embedding `_ - : . >`). Hono decodes `%2F` / `%2e` into params, so a raw `..%2F..%2Fetc` would otherwise arrive as a traversal. A rejected param is **400** (`Invalid agent id` / `Invalid skill id` / `Invalid session id`), distinct from the **404** a well-formed-but-missing session file gets.
|
|
356
|
+
Every `:id` / `:sessionId` route param that becomes a path segment (here and in `DELETE /api/skills/:id`) is validated with `isSafeIdSegment()` (`routes/workspace-path.ts`) before it reaches `path.join` — `.` / `..` rejected outright, everything else must match `/^[\w.:>\u4e00-\u9fff-]+$/` (the union of the real id charsets: slug + CJK agent names, `__internal__`, and session ids embedding `_ - : . >`). Hono decodes `%2F` / `%2e` into params, so a raw `..%2F..%2Fetc` would otherwise arrive as a traversal. A rejected param is **400** (`Invalid agent id` / `Invalid skill id` / `Invalid session id`, or `Invalid id` on the session read / delete routes), distinct from the **404** a well-formed-but-missing session file gets.
|
|
356
357
|
|
|
357
358
|
## Skills
|
|
358
359
|
|
|
@@ -409,18 +410,18 @@ In `PUBLIC_PATHS` (no admin cookie). Auth is a web-channel `x-token` (header or
|
|
|
409
410
|
|
|
410
411
|
## Self-Evolution
|
|
411
412
|
|
|
412
|
-
File: `packages/server/src/routes/evolution.ts`. Surfaces the global `evolution_runs` / `evolution_applies` queues to the admin UI's Evolution tab; see [
|
|
413
|
+
File: `packages/server/src/routes/evolution.ts`. Surfaces the global `evolution_runs` / `evolution_applies` queues to the admin UI's Evolution tab; see [design/evolution.md](../design/evolution.md) for the full design. Unknown run id → 404 `{error: "not found"}`.
|
|
413
414
|
|
|
414
415
|
| Method | Path | Purpose |
|
|
415
416
|
|---|---|---|
|
|
416
|
-
| GET | `/api/evolution/runs
|
|
417
|
+
| GET | `/api/evolution/runs?archived=0\|1&limit=20&before=<createdAt>` | List evolution runs across workspaces, newest first, cursor-paginated (`limit` 1–300, `before` = previous page's `nextCursor`; `archived=1` lists archived runs instead). Returns `{runs, hasMore, nextCursor}`; each row carries the latest `applyId`/`applyStatus`. (Score isn't surfaced in the list — it's read from `score.json` on the detail fetch only.) |
|
|
417
418
|
| GET | `/api/evolution/runs/:id` | Detail: db row + `patch.md` + `score.json` + `.skip.md` (when `status='skipped'`) + wrapper/sub-cli logs + a snapshot summary (first user message + first assistant reply + message count). Row carries `failureReason` (and `applyFailureReason` from the latest apply) — apply-side values include `phase A': apply cli exited <code>`, `phase A': apply agent aborted: <first line of ABORT.md>`, and `phase A': apply agent didn't produce apply.log`. |
|
|
418
|
-
| POST | `/api/evolution/runs/:id/approve` `{reviewerHint?}` | Move run from `awaiting_review` → `approved`, insert a pending `evolution_applies` row that the ticker will pick up. |
|
|
419
|
-
| POST | `/api/evolution/runs/:id/reject` | Move run from `awaiting_review` → `rejected
|
|
420
|
-
| POST | `/api/evolution/runs/:id/retry` `{hint}` | Reset a finished run back to `pending` with the supplied (required)
|
|
421
|
-
| POST | `/api/evolution/runs/:id/hint` `{hint}` | Append text to `user_hint` — memo only, doesn't change status. |
|
|
422
|
-
| DELETE | `/api/evolution/runs/:id` | Delete a finished run: its on-disk artifacts (run dir + archive zip) **and** the DB row. Rejected with 409 for in-flight states (`pending` / `running` / `approved`) so a live wrapper / queued apply isn't pulled out from under. Broadcasts `evolution:run_changed` with `kind:'deleted'`. |
|
|
423
|
-
| GET | `/api/evolution/applies` | List apply rows (used for status badges). |
|
|
419
|
+
| POST | `/api/evolution/runs/:id/approve` `{reviewerHint?}` | Move run from `awaiting_review` → `approved`, insert a pending `evolution_applies` row that the ticker will pick up. Returns `{ok, applyId}`; 409 when the run isn't `awaiting_review`. |
|
|
420
|
+
| POST | `/api/evolution/runs/:id/reject` | Move run from `awaiting_review` → `rejected`; 409 otherwise. |
|
|
421
|
+
| POST | `/api/evolution/runs/:id/retry` `{hint}` | Reset a finished run back to `pending` (attempts cleared) with the supplied hint (required, else 400) so the wrapper picks it up again. Rejected with 409 when the run is already `running` / `pending`. |
|
|
422
|
+
| POST | `/api/evolution/runs/:id/hint` `{hint}` | Append text to `user_hint` (hint required, else 400) — memo only, doesn't change status. Returns `{ok, userHint}`. |
|
|
423
|
+
| DELETE | `/api/evolution/runs/:id` | Delete a finished run: its on-disk artifacts (run dir + archive zip + wrapper log) **and** the DB row, plus every apply that references it (artifacts + row, `evolution:apply_changed` `kind:'deleted'`). Rejected with 409 for in-flight states (`pending` / `running` / `approved`) so a live wrapper / queued apply isn't pulled out from under. Broadcasts `evolution:run_changed` with `kind:'deleted'`. |
|
|
424
|
+
| GET | `/api/evolution/applies` | List in-flight apply rows (`pending` / `running` / `syncing`, newest 200; used for status badges). Returns `{applies}`. |
|
|
424
425
|
|
|
425
426
|
## AgentCore adapter (runtime-mode only)
|
|
426
427
|
|
|
@@ -434,7 +435,7 @@ Bodies and response shapes for the endpoints agents most commonly help users cal
|
|
|
434
435
|
|
|
435
436
|
### POST `/api/auth/login`
|
|
436
437
|
|
|
437
|
-
Source: [packages/server/src/middleware/auth.ts
|
|
438
|
+
Source: [packages/server/src/middleware/auth.ts](../../../packages/server/src/middleware/auth.ts)
|
|
438
439
|
|
|
439
440
|
```json
|
|
440
441
|
// Request
|
|
@@ -492,6 +493,9 @@ Source: [packages/server/src/routes/files.ts](../../../packages/server/src/route
|
|
|
492
493
|
```json
|
|
493
494
|
// 200
|
|
494
495
|
{
|
|
496
|
+
"projectId": "<absPath>",
|
|
497
|
+
"root": "<project dir name>",
|
|
498
|
+
"path": "<relPath>",
|
|
495
499
|
"tree": [
|
|
496
500
|
{ "name": "packages", "path": "packages", "type": "directory", "hasChildren": true },
|
|
497
501
|
{ "name": "README.md", "path": "README.md", "type": "file" }
|
|
@@ -499,7 +503,7 @@ Source: [packages/server/src/routes/files.ts](../../../packages/server/src/route
|
|
|
499
503
|
}
|
|
500
504
|
```
|
|
501
505
|
|
|
502
|
-
Skipped entries:
|
|
506
|
+
Skipped entries: `.git`, `.DS_Store`, `node_modules`, `__pycache__` (other dotfiles — `.halo`, `.gitignore`, `.env` — are listed).
|
|
503
507
|
|
|
504
508
|
### GET `/api/files?path=<rel>&projectId=<abs>`
|
|
505
509
|
|
|
@@ -507,7 +511,10 @@ Reads file content. Max 10 MB.
|
|
|
507
511
|
|
|
508
512
|
```json
|
|
509
513
|
// 200
|
|
510
|
-
{ "content": "..." }
|
|
514
|
+
{ "path": "...", "content": "...", "size": 123, "modifiedAt": 1714000000000, "createdAt": 1714000000000 }
|
|
515
|
+
|
|
516
|
+
// 413 over 10 MB
|
|
517
|
+
{ "error": "File too large (max 10MB)" }
|
|
511
518
|
|
|
512
519
|
// 404 if path doesn't exist
|
|
513
520
|
{ "error": "File not found" }
|
|
@@ -515,7 +522,7 @@ Reads file content. Max 10 MB.
|
|
|
515
522
|
|
|
516
523
|
### POST `/api/agent-configs`
|
|
517
524
|
|
|
518
|
-
Source: [packages/server/src/routes/agent-configs.ts
|
|
525
|
+
Source: [packages/server/src/routes/agent-configs.ts](../../../packages/server/src/routes/agent-configs.ts)
|
|
519
526
|
|
|
520
527
|
```json
|
|
521
528
|
// Request
|
|
@@ -540,12 +547,18 @@ Source: [packages/server/src/routes/agent-configs.ts:199-255](../../../packages/
|
|
|
540
547
|
"conflictScope": null // or "global" / "workspace" to warn of override
|
|
541
548
|
}
|
|
542
549
|
|
|
550
|
+
// 400 empty id after slugging ("Invalid agent name") or workspace scope without projectId
|
|
551
|
+
{ "error": "Invalid agent name" }
|
|
552
|
+
|
|
553
|
+
// 403 id `goal` while general.goal_mode_enabled is off
|
|
554
|
+
{ "error": "Goal mode is disabled." }
|
|
555
|
+
|
|
543
556
|
// 409 already exists
|
|
544
557
|
{ "error": "Agent already exists" }
|
|
545
558
|
```
|
|
546
559
|
|
|
547
560
|
Files created:
|
|
548
|
-
- `<agentDir>/agent.yaml` — scaffold
|
|
561
|
+
- `<agentDir>/agent.yaml` — scaffold whose `model` block comes from the provider named by `general.agent.default_provider` (else `aws-bedrock-claude-invoke`, else the first installed provider), empty tools/skills
|
|
549
562
|
- `<agentDir>/AGENT.md` — `# <name>\n\n<description>\n`
|
|
550
563
|
|
|
551
564
|
### PUT `/api/agent-configs/:id/yaml?scope=&projectId=`
|
|
@@ -560,6 +573,12 @@ Files created:
|
|
|
560
573
|
// 400 invalid YAML
|
|
561
574
|
{ "error": "Invalid YAML: <parser message>" }
|
|
562
575
|
|
|
576
|
+
// 400 scope=workspace without projectId
|
|
577
|
+
{ "error": "projectId required for workspace agents" }
|
|
578
|
+
|
|
579
|
+
// 400 scope other than global / workspace
|
|
580
|
+
{ "error": "Invalid scope" }
|
|
581
|
+
|
|
563
582
|
// 404
|
|
564
583
|
{ "error": "Agent not found" }
|
|
565
584
|
```
|
|
@@ -574,8 +593,7 @@ Source: [packages/server/src/routes/skills.ts](../../../packages/server/src/rout
|
|
|
574
593
|
"name": "Code Review",
|
|
575
594
|
"description": "Review code for correctness and style",
|
|
576
595
|
"scope": "workspace",
|
|
577
|
-
"projectId": "/abs/ws"
|
|
578
|
-
"command": "/review" // optional — registers as slash command
|
|
596
|
+
"projectId": "/abs/ws"
|
|
579
597
|
}
|
|
580
598
|
|
|
581
599
|
// 201
|
|
@@ -588,11 +606,16 @@ Source: [packages/server/src/routes/skills.ts](../../../packages/server/src/rout
|
|
|
588
606
|
"scope": "workspace"
|
|
589
607
|
}
|
|
590
608
|
}
|
|
609
|
+
|
|
610
|
+
// 409 already exists
|
|
611
|
+
{ "error": "Skill already exists" }
|
|
591
612
|
```
|
|
592
613
|
|
|
614
|
+
Also adds the skill's entry to the scope's `settings.yaml`.
|
|
615
|
+
|
|
593
616
|
### GET `/api/sessions/logs?projectId=<abs>&rootOnly=0|1&includeArchived=0|1&cursor=<ms>&limit=<n>`
|
|
594
617
|
|
|
595
|
-
Source: [packages/server/src/routes/sessions.ts
|
|
618
|
+
Source: [packages/server/src/routes/sessions.ts](../../../packages/server/src/routes/sessions.ts)
|
|
596
619
|
|
|
597
620
|
Keyset-paginated over `updatedAt` (descending). `limit` defaults to 50 (max 500); the
|
|
598
621
|
response's `nextCursor` (epoch ms of the last row's `updatedAt`, or `null` on
|
|
@@ -618,15 +641,14 @@ Two shapes, selected by `rootOnly`:
|
|
|
618
641
|
"agentId": "default",
|
|
619
642
|
"agentName": "Default",
|
|
620
643
|
"title": "First user message...",
|
|
621
|
-
"
|
|
622
|
-
"
|
|
623
|
-
"updatedAt": "2026-04-30T08:05:00Z",
|
|
644
|
+
"createdAt": 1777536000000,
|
|
645
|
+
"updatedAt": 1777536300000,
|
|
624
646
|
"exchangeCount": 12,
|
|
625
|
-
"parentSessionId": null,
|
|
626
647
|
"contextTokens": 5975,
|
|
627
648
|
"totalOutputTokens": 6058,
|
|
628
649
|
"stoppedAt": null,
|
|
629
650
|
"archivedAt": null,
|
|
651
|
+
"goalSessionId": null,
|
|
630
652
|
"status": "idle"
|
|
631
653
|
}
|
|
632
654
|
],
|
|
@@ -636,7 +658,7 @@ Two shapes, selected by `rootOnly`:
|
|
|
636
658
|
|
|
637
659
|
Archived sessions are excluded by default; pass `?includeArchived=1` to include them.
|
|
638
660
|
|
|
639
|
-
`status` is `'running' | 'idle' | 'stopped'`. A root counts as `running` while any of its sub-sessions runs.
|
|
661
|
+
`status` is `'running' | 'idle' | 'stopped'`. A root counts as `running` while any of its sub-sessions runs. Returned since 1.5.3-alpha; the current admin no longer reads it. `parentSessionId` is present only on sub-sessions (omitted for roots). Timestamps are epoch ms.
|
|
640
662
|
|
|
641
663
|
Rows are served from the mirrored `agent_sessions` metadata columns (`title` / `exchange_count` / `context_tokens` / `total_output_tokens`) — no session file is opened. A row whose `exchangeCount` is `null` predates those columns: the route reads that one file once, mirrors the values back, and never pays the cost again. `exchangeCount` counts **main user turns over the session's lifetime** (kept + archived), so unlike the file's `messageCount` it doesn't shrink when a compact archives history.
|
|
642
664
|
|
|
@@ -691,7 +713,7 @@ The `n > archiveCount` check is the point, not a formality: `archiveCount` in th
|
|
|
691
713
|
|
|
692
714
|
### GET `/api/settings/schema?projectId=<abs>`
|
|
693
715
|
|
|
694
|
-
Resolves declared schema (from `models/<id>.yaml` `secrets
|
|
716
|
+
Resolves declared schema (from `models/<id>.yaml` `secrets:`, `skills/<id>/config.yaml`, and global `agents/<id>/agent-config.yaml`) against current settings, returning per-field source/state and a list of orphan keys. Drives the Settings page.
|
|
695
717
|
|
|
696
718
|
```json
|
|
697
719
|
// 200
|
|
@@ -699,8 +721,8 @@ Resolves declared schema (from `models/<id>.yaml` `secrets:` and `skills/<id>/co
|
|
|
699
721
|
"scope": "global", // or "workspace" when projectId set
|
|
700
722
|
"sections": [
|
|
701
723
|
{
|
|
702
|
-
"namespaceId": "aws-bedrock-claude-invoke", // 'general' | provider id | skill id
|
|
703
|
-
"source": "provider", // 'general' | 'provider' | 'skill'
|
|
724
|
+
"namespaceId": "aws-bedrock-claude-invoke", // 'general' | provider id | skill id | agent id
|
|
725
|
+
"source": "provider", // 'general' | 'provider' | 'skill' | 'agent'
|
|
704
726
|
"displayName": "AWS Bedrock Claude (Invoke API)",
|
|
705
727
|
"displayName_zh": "AWS Bedrock Claude(Invoke API)",
|
|
706
728
|
"description": "...",
|
|
@@ -711,7 +733,7 @@ Resolves declared schema (from `models/<id>.yaml` `secrets:` and `skills/<id>/co
|
|
|
711
733
|
"kind": "secret", // 'param' | 'secret'
|
|
712
734
|
"description": "...",
|
|
713
735
|
"description_zh": "...",
|
|
714
|
-
"default":
|
|
736
|
+
"default": "...", // schema-default placeholder; omitted when the schema declares none
|
|
715
737
|
"secret": true, // UI masks input + value
|
|
716
738
|
"value": "AK****ST", // already masked when secret:true; null = unset
|
|
717
739
|
"hasValue": true, // any layer has a non-empty value
|
|
@@ -744,18 +766,18 @@ Resolves declared schema (from `models/<id>.yaml` `secrets:` and `skills/<id>/co
|
|
|
744
766
|
{ "ok": true }
|
|
745
767
|
```
|
|
746
768
|
|
|
747
|
-
PUT replaces the full scope; DELETE takes `{scope, projectId, key}` and removes the key. The Settings page uses DELETE for both Reset (current scope removed → falls back to lower scope / unset) and orphan Remove.
|
|
769
|
+
PUT replaces the full scope; PUT / PATCH / DELETE with `scope: "workspace"` reject a `globalOnly` key with 400 (`<key> is global-only and cannot be set per workspace`) and an unresolvable `projectId` with 404 `Project not found`. DELETE takes `{scope, projectId, key}` and removes the key. PUT / PATCH / DELETE fire the server's `onSettingsChange` listeners (e.g. the sandbox hidden-paths reload); a DELETE of an already-absent key is a no-op — `{ ok: true }`, no file write, no notify. The Settings page uses DELETE for both Reset (current scope removed → falls back to lower scope / unset) and orphan Remove.
|
|
748
770
|
|
|
749
771
|
### POST `/api/wechat/login/start`
|
|
750
772
|
|
|
751
|
-
Source: [packages/server/src/routes/wechat.ts
|
|
773
|
+
Source: [packages/server/src/routes/wechat.ts](../../../packages/server/src/routes/wechat.ts)
|
|
752
774
|
|
|
753
775
|
```json
|
|
754
776
|
// Request
|
|
755
777
|
{ "sessionKey": "optional-resume-key", "force": false }
|
|
756
778
|
|
|
757
779
|
// 200
|
|
758
|
-
{ "qrcodeUrl": "https://...", "sessionKey": "abc123" }
|
|
780
|
+
{ "qrcodeUrl": "https://...", "message": "...", "sessionKey": "abc123" }
|
|
759
781
|
```
|
|
760
782
|
|
|
761
783
|
### POST `/api/wechat/login/wait`
|
|
@@ -766,7 +788,7 @@ Source: [packages/server/src/routes/wechat.ts:39-43](../../../packages/server/sr
|
|
|
766
788
|
"sessionKey": "abc123",
|
|
767
789
|
"workspacePath": "/abs/path", // required, must be absolute
|
|
768
790
|
"label": "My Bot", // optional
|
|
769
|
-
"accessLevel": "readonly", // "full" | "readonly", default readonly
|
|
791
|
+
"accessLevel": "readonly", // "full" | "workspace" | "readonly", default readonly
|
|
770
792
|
"language": "en", // "en" | "zh", default "en" — controls system message language
|
|
771
793
|
"timeoutMs": 120000 // optional, how long to wait for scan
|
|
772
794
|
}
|
|
@@ -774,10 +796,11 @@ Source: [packages/server/src/routes/wechat.ts:39-43](../../../packages/server/sr
|
|
|
774
796
|
// 200 waiting for scan (retry polling)
|
|
775
797
|
{ "connected": false, "message": "..." }
|
|
776
798
|
|
|
777
|
-
// 200 connected — account inserted + long-poll started
|
|
778
|
-
{ "connected": true, "accountId": "abc-im-bot" }
|
|
799
|
+
// 200 connected — account inserted (or updated if it already exists) + long-poll started
|
|
800
|
+
{ "connected": true, "accountId": "abc-im-bot", "message": "..." }
|
|
779
801
|
|
|
780
|
-
// 400 bad input
|
|
802
|
+
// 400 bad input (sessionKey / workspacePath missing, relative path, bad accessLevel;
|
|
803
|
+
// after a successful scan also "workspace path not found")
|
|
781
804
|
{ "error": "workspacePath required" }
|
|
782
805
|
```
|
|
783
806
|
|
|
@@ -797,17 +820,18 @@ Full WS protocol in [design/ws.md](../design/ws.md). The four high-traffic clien
|
|
|
797
820
|
"message": "hello",
|
|
798
821
|
"agentId": "default", // optional override
|
|
799
822
|
"clientMsgId": "abc123", // optional — ack/resend dedup id
|
|
823
|
+
"accessLevel": "workspace", // optional — "full" | "workspace" | "readonly", idle session only
|
|
800
824
|
"images": [ { "data": "<base64>", "mimeType": "image/png" } ] // optional
|
|
801
825
|
}
|
|
802
826
|
```
|
|
803
827
|
|
|
804
|
-
Server behaviour (
|
|
828
|
+
Server behaviour (`handleChat` in `ws/handler.ts`; `sessionId`, `projectId` and `message` are all required, else an `error` frame):
|
|
805
829
|
- Creates / reuses a session with the specified agent
|
|
806
|
-
- Persists pasted images to `<ws>/.halo/web/inbound/<date>/`
|
|
830
|
+
- Persists pasted images to `<ws>/.halo/assets/web/inbound/web/<date>/`
|
|
807
831
|
- If the model does not support image input (`capabilities.image: false`), images are filtered out and a text notice is appended instead of sending to the API
|
|
808
|
-
- Queues if busy/compacting; otherwise runs the agent turn
|
|
832
|
+
- Queues if busy/compacting (a `chat:queued` frame is sent only for the compacting case); otherwise runs the agent turn. `accessLevel` applies only on the idle path and is forced to `full` when the host has no OS sandbox
|
|
809
833
|
- When `clientMsgId` is present, replies `{ "type": "chat:ack", "clientMsgId" }` once the message is appended to the session log; resends with an already-acked id are re-acked without re-appending (dedup — see [design/ws.md](../design/ws.md#chat-delivery-ack--resend--dedup))
|
|
810
|
-
- Streams events back via `
|
|
834
|
+
- Streams events back via `chat:stream`, `agent:tool_call`, `chat:usage`, etc. (table below)
|
|
811
835
|
|
|
812
836
|
### `subscribe` (C→S)
|
|
813
837
|
|
|
@@ -871,13 +895,15 @@ It does not remove session files. The full delete (rows + JSON + archive segment
|
|
|
871
895
|
|
|
872
896
|
| Type | Fields | Purpose |
|
|
873
897
|
|---|---|---|
|
|
874
|
-
| `
|
|
875
|
-
| `
|
|
876
|
-
| `
|
|
877
|
-
| `
|
|
878
|
-
| `
|
|
898
|
+
| `chat:stream` | `text`, `agentName`, `turnId`, `taskId?` | Text chunk from the agent |
|
|
899
|
+
| `chat:thinking` | `text`, `agentName`, `turnId`, `taskId?` | Thinking chunk |
|
|
900
|
+
| `agent:tool_call` | `tool`, `toolUseId`, `input`, `agentName`, `turnId`, `taskId?` | Tool invocation card |
|
|
901
|
+
| `agent:tool_result` | `result`, `toolUseId`, `agentName`, `durationMs`, `taskId?` | Tool result |
|
|
902
|
+
| `chat:usage` | `contextTokens`, `outputTokens`, `usage`, `modelId`, `turnId` | Token accounting per model call (root only) |
|
|
903
|
+
| `chat:complete` | `batchBoundary?` | Turn finished |
|
|
879
904
|
| `error` | `error`, `sessionId?` | Error message |
|
|
880
|
-
| `chat:queued` | `reason`, `message`, `sessionId` | Message queued
|
|
905
|
+
| `chat:queued` | `reason: 'compact'`, `message`, `sessionId` | Message queued behind a compact |
|
|
906
|
+
| `chat:ack` | `clientMsgId` | Chat appended to the session log (or re-acked on resend) |
|
|
881
907
|
| `listener:released` | `sessionId` | Event listener reclaimed — re-send `subscribe` (see above) |
|
|
882
908
|
|
|
883
909
|
Events with `taskId` set belong to sub-agent turns (nested sessions); events without are the root agent's. Session-scoped frames carry `sessionId` (the root session they belong to); the admin routes each one to the tab holding that session, and a frame without one goes to the tab on screen. Full list in [design/ws.md](../design/ws.md).
|
|
@@ -47,7 +47,7 @@ Nginx is **not required**. Use it only for domain routing, SSL termination, or w
|
|
|
47
47
|
## Prerequisites
|
|
48
48
|
|
|
49
49
|
- Node.js ≥ 22
|
|
50
|
-
- pnpm
|
|
50
|
+
- pnpm 11 (pinned by the root `packageManager`; `corepack enable` picks it up)
|
|
51
51
|
- AWS credentials configured (`~/.aws/credentials` or env vars) with Bedrock access
|
|
52
52
|
|
|
53
53
|
## 1. Install dependencies and build
|
|
@@ -63,7 +63,7 @@ pnpm --filter @turmind/halo-admin build # next build + copy-monaco; never a ba
|
|
|
63
63
|
|
|
64
64
|
## 2. Runtime data locations
|
|
65
65
|
|
|
66
|
-
No directory needs to be created by hand. SQLite databases are created automatically on first use: per-workspace state at `<workspace>/.halo/halo.db`, plus global queues at `~/.halo/global/evo.db`, `~/.halo/global/cron.db` and `~/.halo/global/runs.db`.
|
|
66
|
+
No directory needs to be created by hand. SQLite databases are created automatically on first use: per-workspace state at `<workspace>/.halo/halo.db`, plus global queues at `~/.halo/global/evo.db`, `~/.halo/global/cron.db` and `~/.halo/global/runs.db`, and channel accounts at `~/.halo/secrets/channels/channels.db`.
|
|
67
67
|
|
|
68
68
|
## 3. Run `halo setup`
|
|
69
69
|
|
|
@@ -90,7 +90,7 @@ When `HALO_PASSWORD` is set, the password chosen via `halo setup` is ignored at
|
|
|
90
90
|
|
|
91
91
|
## 5. Start the server
|
|
92
92
|
|
|
93
|
-
> **单实例锁**:server 启动时会写 `~/.halo/global/server.lock
|
|
93
|
+
> **单实例锁**:server 启动时会写 `~/.halo/global/server.lock`(有 `flock` 命令时用 flock,没有就回退到 pid 探测),退出时自动清理。如果 lock 里记录的进程还活着,新 server 会拒绝启动并打印 `kill <pid>` 提示。原因是 WeChat 长轮询循环跟 HTTP server 解耦,多个进程并存会导致同一条微信消息被 fan out 到多个 session。陈旧 lock(进程已不在)会被自动识别并清除。要手动重启先 kill 旧的:`kill $(cat ~/.halo/global/server.lock)`。
|
|
94
94
|
|
|
95
95
|
### Option A: quick start
|
|
96
96
|
|
|
@@ -155,8 +155,8 @@ server as its child (`packages/cli/src/server-supervisor.ts`). Policy:
|
|
|
155
155
|
up (exit 1) with a log line telling you to fix the cause and re-run `halo
|
|
156
156
|
server start -d`.
|
|
157
157
|
- **2s pause between attempts**, so an instant-crash loop can't spin the CPU.
|
|
158
|
-
- Every decision lands in the daemon log as `[respawn]
|
|
159
|
-
|
|
158
|
+
- Every decision lands in the daemon log as `[respawn] …` — the stdout/stderr
|
|
159
|
+
capture `~/.halo/logs/server.log`, which `halo server logs` tails.
|
|
160
160
|
|
|
161
161
|
`halo server stop` (and `--force`) is not mistaken for a crash: it stamps the
|
|
162
162
|
server pid into a marker file `~/.halo/global/server.stop` *before* signalling,
|
|
@@ -219,6 +219,7 @@ This installs the `halo` binary on `$PATH`. Subcommands available:
|
|
|
219
219
|
| `halo tui` | Interactive TUI client |
|
|
220
220
|
| `halo cli "<prompt>"` | One-shot prompt → reply, exit |
|
|
221
221
|
| `halo agents` / `halo sessions` | List agents / sessions |
|
|
222
|
+
| `halo acp` | Stdio bridge that lets an ACP client (e.g. Claude Code) drive a halo server — see [acp-adapter.md](acp-adapter.md) |
|
|
222
223
|
|
|
223
224
|
### Upgrade flow
|
|
224
225
|
|
|
@@ -239,7 +240,7 @@ Use only for a new public release; the retained details here are not extra prepa
|
|
|
239
240
|
|
|
240
241
|
**npm token gotcha**: `npm publish` on this package needs a granular access token created with **"Bypass 2FA"** checked — scope / permission alone yields `403 Two-factor authentication or granular access token with bypass 2fa enabled is required`. `npm whoami` and `npm token list` succeed with a non-bypass token, so neither is a valid pre-flight; check `GET https://registry.npmjs.org/-/npm/v1/tokens` (with the token as bearer) and look for `"bypass_2fa": true` on the token in use before starting a release.
|
|
241
242
|
|
|
242
|
-
The published package contains a single bundled JS entry (~
|
|
243
|
+
The published package contains a single bundled JS entry (~1.4 MB), 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.
|
|
243
244
|
|
|
244
245
|
### Non-interactive (Docker / CI) details
|
|
245
246
|
|
|
@@ -247,6 +248,7 @@ The published package contains a single bundled JS entry (~620 KB), all built-in
|
|
|
247
248
|
|
|
248
249
|
- Seeds / refreshes `~/.halo/global/` from the bundled templates (per the per-category overwrite policy in `init.ts`)
|
|
249
250
|
- Generates `server.jwt_secret` if missing
|
|
251
|
+
- Binds the built-in agents to the provider named by `HALO_DEFAULT_PROVIDER`, when set (an unknown id only warns)
|
|
250
252
|
- **Does not** set a password — supply one via `HALO_PASSWORD` env
|
|
251
253
|
|
|
252
254
|
Minimal Dockerfile:
|
package/bundled-docs/dev/env.md
CHANGED
|
@@ -51,9 +51,9 @@ nohup node dist/index.js >> /dev/null 2>&1 & # HALO_PASSWORD read from env/con
|
|
|
51
51
|
## Prerequisites
|
|
52
52
|
|
|
53
53
|
- Node.js ≥ 22 (better-sqlite3 native binding is bound to v22)
|
|
54
|
-
- pnpm
|
|
54
|
+
- pnpm 11 (pinned by the root `packageManager`; `corepack enable` picks it up)
|
|
55
55
|
- AWS credentials configured (`~/.aws/credentials` or env vars) with Bedrock access
|
|
56
|
-
- bubblewrap (`sudo apt install bubblewrap`) — OS-level sandbox for non-full access levels. Without
|
|
56
|
+
- bubblewrap (`sudo apt install bubblewrap`) — OS-level sandbox for non-full access levels on Linux (macOS uses the built-in Seatbelt). Without one, only app-level path validation is active and `shell_exec` is blocked for non-full sessions
|
|
57
57
|
|
|
58
58
|
### nvm PATH
|
|
59
59
|
|
|
@@ -67,7 +67,7 @@ Your deployment scripts should also prepend this line.
|
|
|
67
67
|
|
|
68
68
|
## Runtime directories
|
|
69
69
|
|
|
70
|
-
- SQLite: `<workspace>/.halo/halo.db` (per-workspace, auto-created on first use) + global queues `~/.halo/global/evo.db`, `~/.halo/global/cron.db` and `~/.halo/global/runs.db`
|
|
70
|
+
- SQLite: `<workspace>/.halo/halo.db` (per-workspace, auto-created on first use) + global queues `~/.halo/global/evo.db`, `~/.halo/global/cron.db` and `~/.halo/global/runs.db` + channel accounts `~/.halo/secrets/channels/channels.db`
|
|
71
71
|
- Session files: `.halo/sessions/{agentId}/{sessionId}.json`
|
|
72
72
|
- Global config: `~/.halo/global/`
|
|
73
73
|
- Per-project config: `<workspace>/.halo/`
|
|
@@ -78,14 +78,14 @@ Three config file types, precedence **env vars > config.yaml / settings.yaml > c
|
|
|
78
78
|
|
|
79
79
|
| File | Scope | Contents |
|
|
80
80
|
|---|---|---|
|
|
81
|
-
| `~/.halo/secrets/config.yaml` | System | Port, password, CORS, timeouts,
|
|
82
|
-
| `~/.halo/secrets/settings.yaml` | User |
|
|
83
|
-
| `<project>/.halo/settings.yaml` | Project | Overrides of global
|
|
81
|
+
| `~/.halo/secrets/config.yaml` | System | Port, password, JWT secret, CORS, timeouts, log rotation — "infrastructure" settings |
|
|
82
|
+
| `~/.halo/secrets/settings.yaml` | User | Provider secrets, skill params, `general.*` server knobs (session, compaction, limits, sandbox, logging) — "preferences" |
|
|
83
|
+
| `<project>/.halo/settings.yaml` | Project | Overrides of provider / skill `params` and `secrets`; every `general.*` key is global-only (a workspace value is rejected by the API and ignored at runtime) |
|
|
84
84
|
|
|
85
85
|
`init.ts` seeds these on first run with a per-category policy. Refresh trigger: `halo setup` always re-runs the seed, and the server's startup check re-runs it automatically when `~/.halo/global/.template-version` is behind the bundled `TEMPLATE_VERSION` (see `init.ts:TEMPLATE_VERSION` + `index.ts` startup block).
|
|
86
86
|
|
|
87
|
-
- **Always overwritten** (platform-owned, refreshed when the template version moves): `~/.halo/global/{prompts,models,docs}/`, `INSTRUCTIONS.md`, the built-in agent ids (`default`, `executor`, `deep-executor`, `__evo_agent__`, `__score__`, `__apply_agent__`), built-in skill ids (`agent`, `skill`, `workspace`, `cron`, `acp`, `send-file`, `
|
|
88
|
-
- **Built-in agents** keep the user's `model:`
|
|
87
|
+
- **Always overwritten** (platform-owned, refreshed when the template version moves): `~/.halo/global/{prompts,models,docs}/`, `INSTRUCTIONS.md`, the built-in agent ids (`default`, `executor`, `deep-executor`, `goal`, `__evo_agent__`, `__score__`, `__apply_agent__`), built-in skill ids (`agent`, `skill`, `workspace`, `cron`, `acp`, `send-file`, `aws-knowledge`, `web-search`, `self`, `halo`, `extension`).
|
|
88
|
+
- **Built-in agents** keep the user's `model:` and `context:` blocks on overwrite — the admin UI lets users change which model an agent uses and its context limits, and that choice survives upgrades.
|
|
89
89
|
- **Optional skills** (`tavily-web-search`) install only when picked via `halo setup`; the opt-in list is `~/.halo/global/.installed-optional-skills`. Picked skills are force-overwritten alongside the always-overwritten set.
|
|
90
90
|
- **`secrets/config.yaml`** is leaf-merged: existing leaf `value`s preserved, new leaves added when a server upgrade introduces them.
|
|
91
91
|
- **`secrets/settings.yaml`** is created empty if missing and never touched again. Defaults live in `settings-schema.ts`.
|
|
@@ -99,7 +99,7 @@ Source: `packages/server/src/config.ts`
|
|
|
99
99
|
| `HALO_PORT` | `9527` | `config.yaml server.port` | Hono listen port |
|
|
100
100
|
| `HALO_PASSWORD` | (none) | `config.yaml server.password` (scrypt hash) | Plaintext login password. When set, takes precedence over the stored hash and bypasses scrypt entirely — intended for Docker / systemd / CI. It also satisfies the **startup gate**: the server refuses to boot with no password configured, and env plaintext counts as a first-class credential there, so an env-only deployment (`halo setup -y && HALO_PASSWORD=... halo server start`, no stored hash) boots fine. The hash is set by `halo setup` for interactive installs. |
|
|
101
101
|
| `HALO_CORS_ORIGINS` | empty (reflect any origin) | `config.yaml server.cors_origins` | CORS allowlist (comma-separated). Empty = reflect any incoming Origin so credentials work cross-origin. Set explicit list to enforce strict CORS. |
|
|
102
|
-
| `HALO_FRONTEND_DIR` | `packages/admin/out` | — | Static frontend dir
|
|
102
|
+
| `HALO_FRONTEND_DIR` | auto (bundled `admin-out`, else `packages/admin/out`) | — | Static frontend dir; resolved against the current working directory |
|
|
103
103
|
| `HALO_MAX_CONTEXT_TOKENS` | `272000` | — | Model max context |
|
|
104
104
|
| `HALO_SHELL_TIMEOUT` | `120000` | `config.yaml timeout.shell_exec` | Shell command timeout (ms) |
|
|
105
105
|
| `HALO_WEB_FETCH_TIMEOUT` | `10000` | `config.yaml timeout.web_fetch` | web_fetch timeout |
|
|
@@ -114,6 +114,10 @@ Source: `packages/server/src/config.ts`
|
|
|
114
114
|
| `HALO_BADGE` | (none) | — | Environment badge (e.g. `DEV`) returned by `GET /api/auth/check` on **both** the 200 and 401 branches, so the admin can brand the login screen too. The admin repaints the favicon with a badge band and prefixes the tab title, making parallel dev/prod tabs tellable apart. dev and prod serve the same static build, so this has to be a runtime signal. Unset / whitespace-only → `null` → stock branding. Because the 401 branch carries it, the value is **public to unauthenticated clients** — keep it a label (`DEV`, `staging`), never anything sensitive. |
|
|
115
115
|
| `HALO_RUNTIME_MODE` | (none) | — | `agentcore` runs the server as an Amazon Bedrock AgentCore Runtime container (see [design/agentcore.md](../design/agentcore.md)) |
|
|
116
116
|
| `HALO_WORKSPACE` | `process.cwd()` | — | Workspace root in agentcore mode |
|
|
117
|
+
| `HALO_DEFAULT_PROVIDER` | (none) | — | Read by `halo setup --non-interactive`: binds the built-in agents to this provider id (and its default model). An unknown id only prints a warning |
|
|
118
|
+
| `HALO_CLI` | `halo` (`halo.cmd` on Windows) | — | `halo` cli executable that cron runs and the evolution wrapper spawn — a dev override |
|
|
119
|
+
| `HALO_VERSION` | `dev` | — | Version string reported by `GET /api/health` and the cli; stamped by the bundle / desktop launcher, so normally not set by hand |
|
|
120
|
+
| `HALO_PROMPT` | (none) | — | `plain` makes `halo setup` use numbered-menu prompts instead of the raw-mode TUI |
|
|
117
121
|
|
|
118
122
|
settings.yaml only (no env override):
|
|
119
123
|
- `general.session.max_queue_size` (default 256)
|
|
@@ -140,7 +144,7 @@ settings.yaml only (no env override):
|
|
|
140
144
|
When a provider manifest declares a `default: <<NAME>>` for a secret, `halo setup` offers a "Use env $NAME" action. Picking it **writes the literal `<<NAME>>` placeholder into settings.yaml**, which the standard env-var interpolation ([storage.md](../design/storage.md#env-var-interpolation-env_name)) expands against `process.env` at read time. There is no separate runtime env fallback — the placeholder in the file is the only mechanism (an unset env var leaves the literal `<<NAME>>` visible in the request, failing loudly).
|
|
141
145
|
|
|
142
146
|
`<skill-id>.params.*` (referenceable from skills via `{{<skill-id>.params.<key>}}`, or short form `{{params.<key>}}` inside SKILL.md; declared in `skills/<id>/config.yaml`):
|
|
143
|
-
- `tavily-search.params.api_key` — example only; declared by whichever skill needs it
|
|
147
|
+
- `tavily-web-search.params.api_key` — example only; declared by whichever skill needs it
|
|
144
148
|
|
|
145
149
|
Hardcoded (config.ts, no override):
|
|
146
150
|
- `auth.tokenMaxAge` — 14 days
|
|
@@ -152,7 +156,8 @@ settings.yaml-driven `general.limits.*` (no env override):
|
|
|
152
156
|
- `general.limits.web_fetch_bytes` (default 50 KiB)
|
|
153
157
|
- `general.limits.grep_default_matches` (default 50)
|
|
154
158
|
- `general.limits.tool_result_render_chars` (default 8000)
|
|
155
|
-
- `general.limits.
|
|
159
|
+
- `general.limits.tool_result_ui_chars` (default 64 KiB)
|
|
160
|
+
- `general.limits.auto_report_chars` (default 8192)
|
|
156
161
|
- `general.limits.terminal_scrollback_bytes` (default 50000)
|
|
157
162
|
|
|
158
163
|
Two ways to authenticate:
|
|
@@ -6,28 +6,26 @@ File: `packages/server/src/logger.ts`
|
|
|
6
6
|
|
|
7
7
|
## Architecture
|
|
8
8
|
|
|
9
|
-
On startup the logger intercepts `console.log` / `console.error` / `console.warn` / `console.debug`.
|
|
9
|
+
On startup the logger intercepts `console.log` / `console.error` / `console.warn` / `console.debug`. A call that passes the level gate (`general.logging.level` / `HALO_LOG_LEVEL`, default `warn`) writes to:
|
|
10
10
|
1. The original console (stdout/stderr) — live monitoring
|
|
11
11
|
2. A disk log file — persistent history
|
|
12
|
+
3. The OTel logger, when observability is on (see [design/observability.md](../design/observability.md))
|
|
12
13
|
|
|
13
|
-
|
|
14
|
+
Any `?token=…` value in a message is replaced with `?token=<redacted>` before it reaches a sink.
|
|
14
15
|
|
|
15
|
-
|
|
16
|
-
|---|---|---|
|
|
17
|
-
| Default (no workspace) | `~/.halo/logs/` | `server.log` |
|
|
18
|
-
| Workspace open | `<project>/.halo/logs/` | `server.log` |
|
|
16
|
+
## Log locations
|
|
19
17
|
|
|
20
|
-
|
|
18
|
+
One file for the whole server: `~/.halo/global/logs/server.log`. A single server process serves many workspaces, so there is no per-workspace log. Cron runs and evolution runs keep their own logs under `~/.halo/global/logs/{cron,evo}/`. `halo server start -d` additionally captures the process's stdout/stderr in `~/.halo/logs/server.log` (plus the supervisor's `[respawn]` lines) — that is the file `halo server logs` tails.
|
|
21
19
|
|
|
22
20
|
## Log format
|
|
23
21
|
|
|
24
22
|
```
|
|
25
|
-
2026-04-19T10:30:00.000Z
|
|
26
|
-
2026-04-19T10:30:01.234Z
|
|
27
|
-
2026-04-19T10:30:02.567Z
|
|
23
|
+
time=2026-04-19T10:30:00.000Z level=info [Server] Hono server listening on http://localhost:9527
|
|
24
|
+
time=2026-04-19T10:30:01.234Z level=error [WS] Chat error: ThrottlingException
|
|
25
|
+
time=2026-04-19T10:30:02.567Z level=warn [SessionManager] Context overflow, auto-compacting...
|
|
28
26
|
```
|
|
29
27
|
|
|
30
|
-
Every line: `ISO timestamp
|
|
28
|
+
Every line is logfmt-flavored: `time=<ISO timestamp> level=<debug|info|warn|error> <message>`; the message keeps its `[Module]` prefix.
|
|
31
29
|
|
|
32
30
|
## Rotation
|
|
33
31
|
|
|
@@ -41,8 +39,9 @@ When the log file exceeds the cap:
|
|
|
41
39
|
|
|
42
40
|
| Config key | Default | Env var | Purpose |
|
|
43
41
|
|---|---|---|---|
|
|
44
|
-
| `logging.
|
|
45
|
-
| `logging.
|
|
42
|
+
| `logging.max_file_size` (`config.yaml`) | 10 MB | `HALO_LOG_MAX_SIZE` | Rotation threshold |
|
|
43
|
+
| `logging.max_files` (`config.yaml`) | 5 | `HALO_LOG_MAX_FILES` | Retained rotated files |
|
|
44
|
+
| `general.logging.level` (`settings.yaml`) | `warn` | `HALO_LOG_LEVEL` | Minimum level written: debug \| info \| warn \| error |
|
|
46
45
|
|
|
47
46
|
Defined in `packages/server/src/config.ts`.
|
|
48
47
|
|
|
@@ -54,18 +53,8 @@ initLogger()
|
|
|
54
53
|
```
|
|
55
54
|
|
|
56
55
|
Called once from `packages/server/src/index.ts` at startup. Steps:
|
|
57
|
-
1.
|
|
58
|
-
2.
|
|
59
|
-
3. Each interceptor calls the original method and appends to the log file
|
|
60
|
-
|
|
61
|
-
## Workspace switching
|
|
62
|
-
|
|
63
|
-
```typescript
|
|
64
|
-
import { setLogDir } from './logger.js'
|
|
65
|
-
setLogDir('/path/to/project')
|
|
66
|
-
```
|
|
67
|
-
|
|
68
|
-
Called from `packages/server/src/ws/handler.ts` when the client subscribes to a workspace.
|
|
56
|
+
1. Replace console methods with the interceptor
|
|
57
|
+
2. Each interceptor calls the original method and appends to the log file (the directory is created on first write)
|
|
69
58
|
|
|
70
59
|
## Error handling
|
|
71
60
|
|
|
@@ -78,8 +67,5 @@ Log writes use synchronous `fs.appendFileSync` and fail silently — logging mus
|
|
|
78
67
|
tail -f /tmp/server.log
|
|
79
68
|
|
|
80
69
|
# file logs (always written, regardless of nohup redirection)
|
|
81
|
-
tail -f ~/.halo/logs/server.log
|
|
82
|
-
|
|
83
|
-
# project-specific logs
|
|
84
|
-
tail -f /path/to/your-project/.halo/logs/server.log
|
|
70
|
+
tail -f ~/.halo/global/logs/server.log
|
|
85
71
|
```
|