@turmind/halo 1.5.4 → 1.5.6

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 (84) hide show
  1. package/admin-out/404.html +1 -1
  2. package/admin-out/_next/static/chunks/{133-33aa091fba853037.js → 422-cc74b1fdd3af5415.js} +1 -1
  3. package/admin-out/_next/static/chunks/989-e0faeff1c4a0b0b9.js +1 -0
  4. package/admin-out/_next/static/chunks/app/page-44f707ea8dad57da.js +1 -0
  5. package/admin-out/_next/static/chunks/{webpack-1a264efd965db7ab.js → webpack-edf248e07333566d.js} +1 -1
  6. package/admin-out/_next/static/css/b8c87b919758c52b.css +3 -0
  7. package/admin-out/index.html +1 -1
  8. package/admin-out/index.txt +5 -5
  9. package/bundled-docs/design/agent.md +10 -8
  10. package/bundled-docs/design/agentcore.md +13 -10
  11. package/bundled-docs/design/architecture.md +24 -14
  12. package/bundled-docs/design/canvas-extensions.md +19 -4
  13. package/bundled-docs/design/command.md +5 -5
  14. package/bundled-docs/design/cron.md +2 -2
  15. package/bundled-docs/design/evolution.md +34 -35
  16. package/bundled-docs/design/express-self.md +6 -4
  17. package/bundled-docs/design/feishu.md +18 -14
  18. package/bundled-docs/design/halo-city.md +5 -4
  19. package/bundled-docs/design/observability.md +9 -9
  20. package/bundled-docs/design/prompt-system.md +12 -12
  21. package/bundled-docs/design/session.md +14 -14
  22. package/bundled-docs/design/slack.md +22 -20
  23. package/bundled-docs/design/storage.md +30 -29
  24. package/bundled-docs/design/telegram.md +26 -8
  25. package/bundled-docs/design/web.md +3 -1
  26. package/bundled-docs/design/wechat.md +8 -6
  27. package/bundled-docs/design/wecom.md +23 -9
  28. package/bundled-docs/design/ws.md +6 -6
  29. package/bundled-docs/dev/acp-adapter.md +8 -8
  30. package/bundled-docs/dev/add-channel.md +12 -9
  31. package/bundled-docs/dev/add-model-provider.md +31 -20
  32. package/bundled-docs/dev/api.md +86 -57
  33. package/bundled-docs/dev/deploy.md +8 -6
  34. package/bundled-docs/dev/env.md +29 -21
  35. package/bundled-docs/dev/logging.md +15 -29
  36. package/bundled-docs/dev/previews.md +20 -11
  37. package/bundled-docs/dev/tools.md +26 -14
  38. package/bundled-docs/guide/README.md +1 -0
  39. package/bundled-docs/guide/agents.md +19 -53
  40. package/bundled-docs/guide/channels/README.md +7 -4
  41. package/bundled-docs/guide/channels/acp.md +3 -1
  42. package/bundled-docs/guide/channels/feishu.md +13 -11
  43. package/bundled-docs/guide/channels/slack.md +12 -10
  44. package/bundled-docs/guide/channels/telegram.md +21 -27
  45. package/bundled-docs/guide/channels/web.md +15 -11
  46. package/bundled-docs/guide/channels/wechat.md +15 -14
  47. package/bundled-docs/guide/channels/wecom.md +7 -2
  48. package/bundled-docs/guide/chat.md +10 -9
  49. package/bundled-docs/guide/cli.md +10 -8
  50. package/bundled-docs/guide/delegation-and-access.md +1 -1
  51. package/bundled-docs/guide/getting-started.md +3 -3
  52. package/bundled-docs/guide/secrets-and-credentials.md +3 -3
  53. package/bundled-docs/guide/sessions.md +3 -3
  54. package/bundled-docs/guide/skills.md +9 -7
  55. package/bundled-docs/guide/testing-agents-and-skills.md +14 -15
  56. package/bundled-docs/guide/workspace.md +29 -17
  57. package/bundled-docs/requirements/agent.md +19 -18
  58. package/bundled-docs/requirements/chat.md +16 -17
  59. package/bundled-docs/requirements/command.md +10 -10
  60. package/bundled-docs/requirements/editor.md +12 -10
  61. package/bundled-docs/requirements/explorer.md +10 -7
  62. package/bundled-docs/requirements/session.md +16 -17
  63. package/bundled-docs/requirements/settings.md +30 -18
  64. package/bundled-docs/requirements/skill.md +4 -4
  65. package/bundled-docs/requirements/source-control.md +4 -3
  66. package/bundled-docs/requirements/terminal.md +13 -13
  67. package/bundled-docs/requirements/workspace.md +19 -13
  68. package/dist/index.js +10587 -12088
  69. package/package.json +1 -1
  70. package/templates/agents/default/agent.yaml +1 -0
  71. package/templates/config.yaml +6 -0
  72. package/templates/skills/extension/SKILL.md +25 -14
  73. package/templates/skills/extension/config.yaml +9 -0
  74. package/templates/skills/extension/templates/ext.sh +166 -34
  75. package/templates/skills/web-search/SKILL.md +20 -6
  76. package/templates/skills/web-search/config.yaml +13 -0
  77. package/templates/skills/web-search/search.py +27 -6
  78. package/templates/skills/workspace/share.md +11 -3
  79. package/templates/skills/workspace/stage.py +85 -5
  80. package/admin-out/_next/static/chunks/989-92e9683ae8d39750.js +0 -1
  81. package/admin-out/_next/static/chunks/app/page-7a635969198fc62b.js +0 -1
  82. package/admin-out/_next/static/css/4ea5f674146e54d8.css +0 -3
  83. /package/admin-out/_next/static/{lm8Bz3ugTSORyuSXfyx56 → visjvN510yFK-Ff5zwx-0}/_buildManifest.js +0 -0
  84. /package/admin-out/_next/static/{lm8Bz3ugTSORyuSXfyx56 → visjvN510yFK-Ff5zwx-0}/_ssgManifest.js +0 -0
@@ -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 ≥ 9
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 it, only app-level path validation is active and `shell_exec` is blocked for non-full sessions
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,15 +78,16 @@ 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, limits, logging — "infrastructure" settings |
82
- | `~/.halo/secrets/settings.yaml` | User | Model, region, session behaviour — "preferences" |
83
- | `<project>/.halo/settings.yaml` | Project | Overrides of global settings |
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`, `self`, `aws-knowledge`, `nova-web-search`, `halo`).
88
- - **Built-in agents** keep the user's `model:` block on overwrite — the admin UI lets users change which model an agent uses, and that choice survives upgrades.
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
+ - **Skill params in `halo setup`**: a newly picked optional skill walks all its declared params; for built-in skills setup asks only the `secret: true` ones (`listRequiredSkillsWithSecrets`, `setup-providers.ts`) — plain params such as `extension.params.hub_repo` / `web-search.params.fast_region` are set in Settings → Skills only.
90
91
  - **`secrets/config.yaml`** is leaf-merged: existing leaf `value`s preserved, new leaves added when a server upgrade introduces them.
91
92
  - **`secrets/settings.yaml`** is created empty if missing and never touched again. Defaults live in `settings-schema.ts`.
92
93
 
@@ -99,7 +100,7 @@ Source: `packages/server/src/config.ts`
99
100
  | `HALO_PORT` | `9527` | `config.yaml server.port` | Hono listen port |
100
101
  | `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
102
  | `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 (resolved as absolute path from project root) |
103
+ | `HALO_FRONTEND_DIR` | auto (bundled `admin-out`, else `packages/admin/out`) | — | Static frontend dir; resolved against the current working directory |
103
104
  | `HALO_MAX_CONTEXT_TOKENS` | `272000` | — | Model max context |
104
105
  | `HALO_SHELL_TIMEOUT` | `120000` | `config.yaml timeout.shell_exec` | Shell command timeout (ms) |
105
106
  | `HALO_WEB_FETCH_TIMEOUT` | `10000` | `config.yaml timeout.web_fetch` | web_fetch timeout |
@@ -111,22 +112,26 @@ Source: `packages/server/src/config.ts`
111
112
  | `HALO_LOG_MAX_FILES` | `5` | `config.yaml logging.max_files` | Retained rotated logs |
112
113
  | `HALO_LOG_LEVEL` | `warn` | `settings.yaml general.logging.level` | Log level: debug \| info \| warn \| error |
113
114
  | `HALO_JWT_SECRET` | (none) | `config.yaml server.jwt_secret` | JWT signing secret (generated by `halo setup` if missing; stripped from child-process env) |
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
+ | `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. `DEV` (trimmed, any case) also makes the server a **non-owner of workspace runtimes**: it never claims `<workspace>/.halo/runtime.lock` and skips boot cleanup (orphan reconcile, goal / run-ledger sweeps), leaving both to the prod server that shares those workspaces. Any other value → normal owner. See [storage.md](../design/storage.md#workspace-runtime-lock). |
115
116
  | `HALO_RUNTIME_MODE` | (none) | — | `agentcore` runs the server as an Amazon Bedrock AgentCore Runtime container (see [design/agentcore.md](../design/agentcore.md)) |
116
117
  | `HALO_WORKSPACE` | `process.cwd()` | — | Workspace root in agentcore mode |
118
+ | `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 |
119
+ | `HALO_CLI` | `halo` (`halo.cmd` on Windows) | — | `halo` cli executable that cron runs and the evolution wrapper spawn — a dev override |
120
+ | `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 |
121
+ | `HALO_PROMPT` | (none) | — | `plain` makes `halo setup` use numbered-menu prompts instead of the raw-mode TUI |
117
122
 
118
123
  settings.yaml only (no env override):
119
- - `general.session.max_queue_size` (default 256)
120
- - `general.session.max_nesting_depth` (default 16)
121
- - `general.agent.max_retries` (default 5)
122
- - `general.compact.keep_messages` (default 5) — recent messages to keep uncompacted
123
- - `general.compact.max_summary_input` (default 15000) — local compaction fallback input cap
124
- - `general.compact.max_message_slice` (default 800) — local compaction per-message cap
124
+ - `general.session.max_queue_size` (default 256), restart required
125
+ - `general.session.max_nesting_depth` (default 16), restart required
126
+ - `general.agent.max_retries` (default 5), restart required
127
+ - `general.compact.keep_messages` (default 5) — recent messages to keep uncompacted, restart required
128
+ - `general.compact.max_summary_input` (default 15000) — local compaction fallback input cap, restart required
129
+ - `general.compact.max_message_slice` (default 800) — local compaction per-message cap, restart required
125
130
  - `general.server.trust_proxy` (default `false`) — trust `x-forwarded-for` for client IP resolution, scope: global (enable only behind a reverse proxy you control)
126
- - `general.sandbox.hidden_dirs` (default `~/.halo/secrets,~/.aws,~/.ssh,~/.gnupg,~/.docker,~/.config/gh,~/.halo/global/internal-sessions,~/.halo/global/logs`) — hidden from workspace/readonly sessions (bwrap tmpfs overlays on Linux, Seatbelt deny on macOS), scope: global
131
+ - `general.sandbox.hidden_dirs` (default empty) — extra dirs hidden from workspace/readonly sessions (bwrap tmpfs overlays on Linux, Seatbelt deny on macOS), **appended** to the built-in list, which is always included (`~/.halo/secrets,~/.aws,~/.ssh,~/.gnupg,~/.docker,~/.config/gh,~/.halo/global/internal-sessions,~/.halo/global/logs`); Full sessions, the admin file explorer and the terminal are not affected, scope: global
127
132
  - `general.sandbox.writable_dirs` (default empty) — dirs writable inside the sandbox besides the workspace (Linux / macOS), for external CLIs that keep local state (e.g. `~/.kiro`); not applied to readonly sessions, scope: global
128
- - `general.sandbox.hidden_files` (default `~/.npmrc,~/.bash_history,~/.gitconfig,~/.git-credentials,~/.netrc,~/.halo/global/{evo,cron,runs}.db` + their `-wal`/`-shm` files) — hidden from workspace/readonly sessions, read as empty files (bwrap bind of `~/.halo/.sandbox-empty` on Linux, Seatbelt deny on macOS), scope: global
129
- - `general.logging.level` (default `warn`) — log level: debug | info | warn | error
133
+ - `general.sandbox.hidden_files` (default empty) — extra files hidden from workspace/readonly sessions, read as empty files (bwrap bind of `~/.halo/.sandbox-empty` on Linux, Seatbelt deny on macOS), **appended** to the built-in list, which is always included (`~/.npmrc,~/.bash_history,~/.gitconfig,~/.git-credentials,~/.netrc,~/.halo/global/{evo,cron,runs}.db` + their `-wal`/`-shm` files); Full sessions, the admin file explorer and the terminal are not affected, scope: global
134
+ - `general.logging.level` (default `warn`) — log level: debug | info | warn | error, restart required
130
135
  - `general.observability.endpoint` (default `''`) — OTLP collector base URL (e.g. `http://localhost:4318`); empty = off, scope: global, restart required. See [design/observability.md](../design/observability.md)
131
136
  - `general.observability.service_name` (default `halo`) — OTel resource `service.name`, scope: global, restart required
132
137
  - `general.observability.headers` (default `''`, secret) — extra OTLP request headers, comma-separated `k=v`, scope: global, restart required
@@ -140,7 +145,9 @@ settings.yaml only (no env override):
140
145
  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
146
 
142
147
  `<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
148
+ - `tavily-web-search.params.api_key` — example only; declared by whichever skill needs it
149
+ - `web-search.params.fast_region` / `web-search.params.deep_region` — AWS regions for web-search's fast gear (Nova grounding, US only; empty = us-east-1) and deep gear (Bedrock Mantle; empty = us-west-2 for luna, us-east-1 for sol, and sol set to a region it isn't offered in falls back to us-east-1)
150
+ - `extension.params.hub_repo` — hub `/extension install <id>` pulls from (empty = `https://github.com/turmind/halo-hub`); the skill's `ext.sh` also reads the optional `GITHUB_TOKEN` / `GITLAB_TOKEN` / `GITEA_TOKEN` from the server environment and sends them to that hub's release API only. See [design/canvas-extensions.md](../design/canvas-extensions.md#extension-skill-and-halo-hub)
144
151
 
145
152
  Hardcoded (config.ts, no override):
146
153
  - `auth.tokenMaxAge` — 14 days
@@ -152,7 +159,8 @@ settings.yaml-driven `general.limits.*` (no env override):
152
159
  - `general.limits.web_fetch_bytes` (default 50 KiB)
153
160
  - `general.limits.grep_default_matches` (default 50)
154
161
  - `general.limits.tool_result_render_chars` (default 8000)
155
- - `general.limits.ws_event_buffer` (default 5000)
162
+ - `general.limits.tool_result_ui_chars` (default 64 KiB)
163
+ - `general.limits.auto_report_chars` (default 8192)
156
164
  - `general.limits.terminal_scrollback_bytes` (default 50000)
157
165
 
158
166
  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`. Every call writes twice:
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
- ## Log locations
14
+ Any `?token=…` value in a message is replaced with `?token=<redacted>` before it reaches a sink.
14
15
 
15
- | Scenario | Directory | File |
16
- |---|---|---|
17
- | Default (no workspace) | `~/.halo/logs/` | `server.log` |
18
- | Workspace open | `<project>/.halo/logs/` | `server.log` |
16
+ ## Log locations
19
17
 
20
- When the WS handler receives a `subscribe` or `chat` with `projectId`, it calls `setLogDir()` to switch to the project's log directory.
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 INFO [Server] Hono server listening on http://localhost:9527
26
- 2026-04-19T10:30:01.234Z ERROR [WS] Chat error: ThrottlingException
27
- 2026-04-19T10:30:02.567Z WARN [Orchestrator] Context overflow, auto-compacting...
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` + `level (DEBUG/INFO/WARN/ERROR)` + `message`.
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.maxFileSize` | 10 MB | `HALO_LOG_MAX_SIZE` | Rotation threshold |
45
- | `logging.maxFiles` | 5 | `HALO_LOG_MAX_FILES` | Retained rotated files |
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. Create the log directory
58
- 2. Replace console methods with the interceptor
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
  ```
@@ -8,12 +8,18 @@ Location: [`packages/admin/src/features/editor/previews/`](../../../packages/adm
8
8
 
9
9
  ```
10
10
  previews/
11
- ├── FilePreview.tsx Public entry. Looks up plugin by extension,
12
- │ renders <Suspense><Component/></Suspense>.
13
- ├── types.ts PreviewPlugin, PreviewProps
14
- ├── registry.ts register() / getPlugin() / registeredExtensions() / isHeavyPreview() / loadExtensions()
11
+ ├── FilePreview.tsx Public entry. Asks the registry for the candidates for the
12
+ │ extension, renders the chosen one (<Suspense><Component/></Suspense>
13
+ │ for a built-in, the iframe host for an extension); too-large and
14
+ │ no-viewer placeholders.
15
+ ├── types.ts PreviewPlugin, PreviewProps, Resolved
16
+ ├── registry.ts register() / setExtensions() / loadExtensions() / resolve() / canPreview() / isHeavyPreview()
17
+ │ + subscribe() / useRegistryVersion() so open tabs re-resolve on change
18
+ ├── extension-host.tsx Iframe host for installed preview extensions (+ extension-host-logic.ts,
19
+ │ extension-token.ts) — see design/canvas-extensions.md
15
20
  ├── ui/
16
- │ ├── preview-shell.tsx Standard header (filename + Open-as-Text + Download + extraToolbar)
21
+ │ ├── preview-shell.tsx Standard header (filename + extraToolbar + Open with + Open-as-Text + Download)
22
+ │ ├── open-with-menu.tsx "Open with" picker, shown only when a file has more than one viewer
17
23
  │ ├── use-preview-fetch.ts Hook: fetch + AbortController + parse, returns {data, error, loading}
18
24
  │ ├── use-data-fetch.ts JSON-endpoint sibling: abortable fetcher → {data, error, loading},
19
25
  │ │ for previews that parse server-side instead of raw bytes
@@ -60,6 +66,8 @@ interface PreviewProps {
60
66
  viewUrl: string // for inline viewing, supports HTTP Range
61
67
  downloadUrl: string // for forced download (used by the shell's Download button)
62
68
  onOpenAsText?: () => void // set when the file can also be force-opened as text
69
+ tooLarge?: boolean // file over the editor's 10MB read cap; FilePreview shows a placeholder instead of the plugin
70
+ size?: number // on-disk size in bytes, shown by that placeholder
63
71
  }
64
72
  ```
65
73
 
@@ -121,7 +129,7 @@ register(fooPlugin)
121
129
 
122
130
  Done. The Canvas panel will:
123
131
  - Treat `.foo` / `.foobar` as non-text (routes to preview instead of Monaco)
124
- - Mount `FooPreview` inside an MRU cache (up to 5 concurrent plugins cached)
132
+ - Mount `FooPreview` inside an MRU cache (up to 5 concurrent previews cached)
125
133
 
126
134
  ### Routing API: `canPreview()` vs `loadExtensions()`
127
135
 
@@ -129,6 +137,7 @@ Done. The Canvas panel will:
129
137
 
130
138
  - **One-shot routing decisions** (`isBinaryExtension` in `editor-panel.tsx`: click-open, tab restore after reload, open-to-side) must `await loadExtensions()` before concluding "text", because an extension may claim an extension-only type (`.glb`) or a text suffix (`.json`). A preview verdict needs no wait — extensions only add viewers ahead of text, and `FilePreview` re-picks the viewer on a registry version bump.
131
139
  - The wait is capped at `INITIAL_LOAD_WAIT_MS` (3 s, `registry.ts`), counted from the first call; on timeout callers proceed as if nothing were installed. The request isn't cancelled — a late result still lands via `setExtensions`.
140
+ - Candidates come from `resolve(ext)`, best first: `default` extensions → the built-in plugin → `option` extensions → Open as Text (always last). More than one candidate puts an **Open with** menu in the header.
132
141
  - It never rejects and is never re-issued: a failed fetch leaves the extension layer empty without delaying later opens. The WS-reconnect re-fetch and `extension:changed` frames fill it in.
133
142
 
134
143
  ## PreviewShell — the standard header
@@ -211,21 +220,21 @@ const { data } = usePreviewFetch(viewUrl, (buf, signal) =>
211
220
 
212
221
  ## `heavy: true` — when to use it
213
222
 
214
- Set `heavy: true` when the preview:
223
+ Set `heavy: true` on a built-in when the preview:
215
224
  - Needs DOM access (can't run in a Worker — e.g. `pptx-preview` draws to canvas)
216
225
  - Is the dominant memory cost of the page (large canvas, many cached elements)
217
226
 
218
227
  Effect: the MRU cache skips this plugin. Only the **active** instance mounts; switching away unmounts the component (releases memory and stops any in-flight work). Switching back re-fetches/re-renders.
219
228
 
220
- Only pptx currently uses this. Don't set it by default — the MRU cache gives much faster switches.
229
+ Only pptx currently uses this among built-ins (read-only preview extensions are also treated as heavy, judged by `isHeavyPreview()`). Don't set it by default — the MRU cache gives much faster switches.
221
230
 
222
231
  ## `onOpenAsText`
223
232
 
224
- When the user right-clicks a previewable file in Explorer and picks "Open as Text", Canvas closes the preview tab and opens the raw file in Monaco. The preview plugin receives this as `props.onOpenAsText` — pass it straight through to `PreviewShell` and the button appears automatically.
233
+ The editor panel passes `props.onOpenAsText` to every preview tab. Calling it closes the preview tab and opens the raw file in Monaco. Pass it straight through to `PreviewShell` and the button appears automatically (the **Open with** menu also lists Open as text).
225
234
 
226
235
  ## Two plugin patterns: raw-bytes vs. server-parsed
227
236
 
228
- Most built-in plugins (pdf, docx, xlsx, pptx, media) are **raw-bytes**: `usePreviewFetch` downloads the whole file as an ArrayBuffer via `GET /api/files/download?inline=1`, then parsing happens client-side (a Worker for the heavy formats). This is the pattern documented above.
237
+ Most built-in plugins (pdf, docx, xlsx, pptx, media) are **raw-bytes**: they load the file from `viewUrl` (`GET /api/files/download?inline=1`). docx and xlsx use `usePreviewFetch` to download it as an ArrayBuffer and parse client-side in a Worker; pptx fetches it itself and renders on the main thread; pdf and media just hand `viewUrl` to the browser (`<iframe>` / `<img>` / `<video>` / `<audio>`). This is the pattern documented above.
229
238
 
230
239
  Parquet, SQLite and CSV/TSV are **server-parsed** instead: the server does the parsing and hands back one page of JSON rows at a time via `GET /api/data-preview/*` (route details in [`dev/api.md`](api.md#data-preview)), so a multi-GB file never has to reach the browser.
231
240
 
@@ -288,4 +297,4 @@ See [`dev/api.md`](api.md) for the route details.
288
297
  - Open-as-Text button works (closes preview, opens in Monaco)
289
298
  - Closing the preview tab cleanly aborts any in-flight fetch/parse
290
299
  - Opening a second file of the same type reuses the worker (no second worker instance — check DevTools → Application → Service Workers / the process graph)
291
- - Switching between up to 5 preview tabs is instant (MRU cache); the 6th oldest unmounts
300
+ - Switching between up to 5 preview tabs is instant (MRU cache); the 6th oldest unmounts (a dirty extension tab is never evicted)
@@ -31,13 +31,14 @@ Write a file (creates parent dirs on demand).
31
31
 
32
32
  ### file_edit
33
33
 
34
- Replace a string in a file (exact match).
34
+ Replace a string in a file (exact match). Fails when `old_string` is empty, identical to `new_string`, not found, or (without `replace_all`) found more than once.
35
35
 
36
36
  | Arg | Type | Required | Description |
37
37
  |---|---|---|---|
38
38
  | path | string | yes | File path |
39
39
  | old_string | string | yes | The exact text to find |
40
40
  | new_string | string | yes | The replacement |
41
+ | replace_all | boolean | no | Replace every occurrence (default: false) |
41
42
 
42
43
  ### view_image
43
44
 
@@ -58,7 +59,7 @@ Processing before the bytes go out — only the payload changes, the file on dis
58
59
 
59
60
  How many images history can hold in total is capped separately at the session level — see [design/session.md → History image budget](../design/session.md#history-image-budget).
60
61
 
61
- **Vision gating**: this tool is only injected into the agent's tool list when the underlying model declares `capabilities.image: true` in its provider manifest. For text-only models (DeepSeek and others), `view_image` is silently dropped at `createWorkspaceTools()` time so the model never sees it — calling it would otherwise produce a 400 from the provider.
62
+ **Vision gating**: this tool is only injected into the agent's tool list when the underlying model declares `capabilities.image: true` in its provider manifest (or the agent's `model.image` in `agent.yaml` overrides it). For text-only models (DeepSeek and others), `view_image` is silently dropped at `createWorkspaceTools()` time so the model never sees it — calling it would otherwise produce a 400 from the provider.
62
63
 
63
64
  ### file_list
64
65
 
@@ -103,7 +104,7 @@ Find files by glob.
103
104
  | pattern | string | yes | Glob, e.g. `**/*.ts`, `src/**/*.tsx` |
104
105
  | path | string | no | Starting dir (default: workspace root) |
105
106
 
106
- Returns paths relative to workspace root, alphabetically. Same skip list as grep.
107
+ Returns paths relative to workspace root, alphabetically; stops collecting at 5,000 matches. Same skip list as grep.
107
108
 
108
109
  ### web_fetch
109
110
 
@@ -125,7 +126,7 @@ Each session carries `accessLevel: 'readonly' | 'workspace' | null` (persisted i
125
126
 
126
127
  | Level | DB value | Tools (with OS sandbox) | Tools (without) | Sandbox |
127
128
  |---|---|---|---|---|
128
- | `null` (full) | `full` | All 9 tools | All 9 tools | None (rm guard still applies) |
129
+ | `null` (full) | `NULL` | All 9 tools | All 9 tools | None (rm guard still applies) |
129
130
  | `workspace` | `workspace` | All 9 tools | All 9 tools | workspace rw, sensitive paths + workspace runtime state hidden |
130
131
  | `readonly` | `readonly` | All 9 tools | file_read, view_image, file_list, grep, glob (5 tools) | workspace ro, sensitive paths + workspace runtime state hidden |
131
132
 
@@ -174,15 +175,15 @@ This is a known, unfixed gap (not a bug in a specific route): don't hand out `wo
174
175
 
175
176
  Sensitive directories and files are hidden from workspace/readonly sessions via bwrap overlays / Seatbelt deny rules (and the same lists gate `assertPathAllowed` for in-process file tools). Under bwrap a hidden file reads as empty and a hidden dir as an empty directory; under Seatbelt both are denied outright. Paths that don't exist on the filesystem are silently skipped. Two categories coexist:
176
177
 
177
- **Global lists (configurable)** — credentials and cross-workspace state, configured in `settings.yaml` under `general.sandbox`:
178
+ **Global lists (built-in + configurable extras)** — credentials and cross-workspace state. The built-in lists are code constants in `sandbox.ts` (`DEFAULT_HIDDEN_DIRS` / `DEFAULT_HIDDEN_FILES`, the single source) and are always included in the effective lists (which apply to workspace/readonly sessions only — Full sessions skip the sandbox entirely); `settings.yaml` `general.sandbox.hidden_dirs` / `hidden_files` hold extra entries that are **appended** (deduped) — they never replace the built-ins, so a built-in entry cannot be removed from the list in settings. `config.ts` `resolveSandboxPaths()` builds the effective lists for both the server and the CLI:
178
179
 
179
- | Setting | Default | Method |
180
+ | Setting | Built-in (always included) | Method |
180
181
  |---|---|---|
181
182
  | `hidden_dirs` | `~/.halo/secrets,~/.aws,~/.ssh,~/.gnupg,~/.docker,~/.config/gh,~/.halo/global/internal-sessions,~/.halo/global/logs` | bwrap `--tmpfs` overlay (empty directory); Seatbelt `subpath` deny |
182
183
  | `hidden_files` | `~/.npmrc,~/.bash_history,~/.gitconfig,~/.git-credentials,~/.netrc,~/.halo/global/{evo,cron,runs}.db` + their `-wal`/`-shm` files | bwrap `--ro-bind ~/.halo/.sandbox-empty` (reads as empty); Seatbelt `literal` deny |
183
184
  | `writable_dirs` | (empty) | bwrap `--bind` read-write / Seatbelt write allow — for external CLIs that keep local state (e.g. `~/.kiro`); not applied to readonly sessions |
184
185
 
185
- Changes saved through the settings API (admin Settings page, `PUT` / `PATCH /api/settings`) take effect immediately — the save fires `onSettingsChange`, which re-reads the lists into `sandbox.ts` (`setSandboxHiddenPaths`), so the next tool call uses them. A hand edit of `settings.yaml` is not watched for these keys: it applies at the next restart or the next API save. These keys are `globalOnly` in the schema — a workspace `settings.yaml` cannot override them, since they define the security boundary agents run inside.
186
+ Changes saved through the settings API (admin Settings page, `PUT` / `PATCH` / `DELETE /api/settings`) take effect immediately — the save fires `onSettingsChange`, which re-reads the lists into `sandbox.ts` (`setSandboxHiddenPaths`), so the next tool call uses them. A hand edit of `settings.yaml` is not watched for these keys: it applies at the next restart or the next API save. These keys are `globalOnly` in the schema — a workspace `settings.yaml` cannot override them, since they define the security boundary agents run inside.
186
187
 
187
188
  **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.
188
189
 
@@ -198,7 +199,7 @@ The rest of `.halo/` (INSTRUCTIONS.md, INDEX.md, docs/, memory/, skills/, agents
198
199
  Per-channel defaults: every channel session (Web, Telegram, Slack, Feishu, WeCom, WeChat) inherits its account's `access_level` — `full` → full, `workspace` → workspace, `readonly` / `observer` → readonly — and new accounts default to `readonly` (DB column default, `insertAccount` fallback and the admin create forms). Level sources for every entry point (admin, cron, cli, relay, goal, sub-agents): [guide/delegation-and-access.md](../guide/delegation-and-access.md#where-a-sessions-level-comes-from).
199
200
 
200
201
  ### Binary file detection
201
- `grep` and `glob` read the first 512 bytes looking for a null byte and skip binaries.
202
+ `grep` reads the first 512 bytes of each file looking for a null byte and skips binaries (`glob` matches paths only and never opens files).
202
203
 
203
204
  ### Tool result budget
204
205
  The orchestrator truncates tool results over 8000 chars and appends a `[Content truncated]` hint telling the agent to use `grep` for a targeted search. `activate_skill` results are exempt — a SKILL.md body is instructions, not data, and the built-in acp / cron / self skills exceed 8K.
@@ -227,6 +228,7 @@ Start a sub-agent session asynchronously. When the sub-agent finishes, its **wra
227
228
 
228
229
  - Success: `{"code": 0, "session_id": "<childSessionId>"}`
229
230
  - Unknown agent: `{"code": 1, "error": "agent \"<id>\" not found..."}`
231
+ - Agent outside the caller's `team`: `{"code": 1, "error": "agent \"<id>\" is not in your team..."}`
230
232
  - Depth exceeded: `{"code": 1, "error": "Maximum nesting depth (N) reached..."}`
231
233
  - Working dir invalid: `{"code": 1, "error": "working_dir \"...\" is outside the workspace"}` (or does-not-exist / not-a-directory)
232
234
 
@@ -244,12 +246,21 @@ No arguments. Returns JSON:
244
246
  "sessions": [
245
247
  {
246
248
  "id": "root>sid_xxx",
249
+ "parentId": "root",
247
250
  "agentId": "coder",
248
251
  "agentName": "Coder",
249
- "title": "Implement login page",
250
252
  "description": "Implement login page",
251
253
  "status": "running",
252
- "createdAt": 1714000000000
254
+ "accessLevel": null,
255
+ "goalSessionId": null,
256
+ "createdAt": 1714000000000,
257
+ "updatedAt": 1714000005000,
258
+ "stoppedAt": null,
259
+ "archivedAt": null,
260
+ "title": "Implement login page",
261
+ "exchangeCount": 1,
262
+ "contextTokens": 12345,
263
+ "totalOutputTokens": 678
253
264
  }
254
265
  ],
255
266
  "count": 1
@@ -258,10 +269,11 @@ No arguments. Returns JSON:
258
269
 
259
270
  `status`: `running` / `idle` / `stopped`. Archived sessions are excluded.
260
271
 
261
- `title` is the human-assigned label (set by renaming the session in the admin
262
- sidebar), read from the per-session jsonl log so it matches what the UI shows.
263
- It falls back to `description` (the `start_session` task summary) when no title
264
- was set, so the field is never empty — lets a caller dispatch work by title.
272
+ Each entry is the session's `SessionInfo` row. `title` is the human-assigned
273
+ label (set by renaming the session in the admin sidebar) mirrored onto the row
274
+ from the session file, so it matches what the UI shows. It falls back to
275
+ `description` (the `start_session` task summary) when no title was set, so the
276
+ field is never empty — lets a caller dispatch work by title.
265
277
 
266
278
  ### query_session
267
279
 
@@ -20,6 +20,7 @@ Aimed at Halo end-users. Pick the doc that matches your situation.
20
20
  - [Telegram](channels/telegram.md) — BotFather token, long-poll
21
21
  - [Slack](channels/slack.md) — Socket Mode (no public webhook needed)
22
22
  - [Feishu / Lark](channels/feishu.md) — long-connect with appId + appSecret
23
+ - [WeCom](channels/wecom.md) — 企业微信 intelligent bot, long-connect with Bot ID + Secret
23
24
  - [WeChat](channels/wechat.md) — QR-scan iLink bot
24
25
  - [ACP](channels/acp.md) — Claude Code or other ACP clients driving a remote halo
25
26
 
@@ -6,17 +6,17 @@ The agent is Halo's core abstraction: personality (AGENT.md) + config (agent.yam
6
6
 
7
7
  | Kind | Description |
8
8
  |---|---|
9
- | Built-in | Server-shipped agents (`default`, `executor`, `deep-executor` + the three internal `__evo_agent__` / `__score__` / `__apply_agent__`). Live under `~/.halo/global/agents/<id>/`. **Force-overwritten on every server startup** — local edits to these files will be lost on the next start. To customize: copy into the workspace scope (workspace replaces global) and edit there. |
9
+ | Built-in | Server-shipped agents (`default`, `executor`, `deep-executor` + the internal `goal` / `__evo_agent__` / `__score__` / `__apply_agent__`; `goal` is seeded like the rest but only usable when `general.goal_mode_enabled` is set — off by default, no UI). Live under `~/.halo/global/agents/<id>/`. **Force-overwritten on every server startup** — local edits to these files will be lost on the next start. To customize: copy into the workspace scope (workspace replaces global) and edit there. |
10
10
  | Global | Any other agent under `~/.halo/global/agents/<id>/`, e.g. one you created via the admin UI. Shared across projects. Never overwritten by the server. |
11
11
  | Workspace | `<project>/.halo/agents/<id>/`, private to the current project; same-id workspace wins over global. Override is **whole-folder**: the workspace agent folder replaces the global one entirely, so copy *both* `agent.yaml` and `AGENT.md` when customizing — a folder with only `AGENT.md` has no model config and won't load. |
12
12
 
13
13
  ## Open the Agents panel
14
14
 
15
- Click the `👥 Agents` icon in the Activity Bar.
15
+ Click the `🤖 Agents` icon in the Activity Bar.
16
16
 
17
17
  ```
18
18
  ┌─────────────────┬──────────────────────────┐
19
- │ Agents │ Form / YAML / MD Editor │
19
+ │ Agents │ Form / Edit │
20
20
  │─────────────────│ │
21
21
  │ ▼ Global (2) │ │
22
22
  │ 🤖 Default │ │
@@ -28,38 +28,32 @@ Click the `👥 Agents` icon in the Activity Bar.
28
28
 
29
29
  ## Create an agent
30
30
 
31
- Top-of-sidebar `+` button; modal asks for:
31
+ Click the `+` on the **Global** or **Workspace** group header in the sidebar (the group you click decides the scope; Workspace needs an open workspace). Two prompts follow:
32
32
  - **Name**: display name (e.g. "Coder")
33
- - **Description**: one-line description
34
- - **Scope**: Global / Workspace
33
+ - **Description**: one-line description (optional)
35
34
 
36
35
  The backend calls `defaultAgentYaml(name, description)` to generate a minimal `agent.yaml`.
37
36
 
38
37
  ## Edit an agent
39
38
 
40
- Click an agent in the sidebar to open the right-hand editor. Three views:
39
+ Click an agent in the sidebar to open the right-hand editor. Two views:
41
40
 
42
41
  ### Form view
43
- Data-driven form for name / description / model / tools / skills / thinking — click Save.
42
+ Data-driven form for name / description / model / tools / skills / thinking. Changes auto-save about half a second after you stop editing — there's no Save button. The agent's AGENT.md is shown below the form as a read-only preview.
44
43
 
45
44
  **Model section** has four fields:
46
45
  - `provider` — dropdown populated from `~/.halo/global/models/*.yaml`
47
- - `id` — combobox (input + datalist); preset options filtered by current provider, also accepts manual input of any Bedrock model ID
46
+ - `id` — combobox (input + datalist); preset options filtered by current provider, also accepts manual input of any model ID
48
47
  - `endpoint` — combobox (input + datalist); full endpoint URL (e.g. `https://bedrock-runtime.ap-northeast-1.amazonaws.com`), supports custom proxy URLs
49
48
  - `maxTokens` — optional; defaults to `maxOutputTokens` in the provider yaml
50
49
 
51
- Switching provider auto-resets the model id to the first model of the new provider.
50
+ Switching provider resets the model id to the new provider's default model (its `defaultModelId`), keeping the current id when the new provider lists it too.
52
51
 
53
52
  **Capability buttons** (Prompt Caching / Thinking) appear when the selected model matches a registry entry with declared capabilities. For manually entered model IDs not in the registry, default presets are shown (5min/1hour for caching, Low/Medium/High/Max for thinking).
54
53
 
55
- ### YAML view
56
- Edit `agent.yaml` directly (Monaco). Use this for advanced settings (like `context.maxTokens`, `promptCaching`).
57
-
58
- ### MD view
59
- Edit `AGENT.md` (personality) and `INSTRUCTIONS.md` (preferences).
54
+ ### Edit view
55
+ The **Edit** button (top right) opens the agent's folder in a mini workspace (file tree + Monaco); **Back** returns to the form. Edit `agent.yaml` directly for advanced settings (like `context.maxTokens`, `promptCaching`) and `AGENT.md` for the personality.
60
56
  - `AGENT.md` takes precedence over YAML's `system_prompt`
61
- - `INSTRUCTIONS.md` is injected into every agent, not just this one
62
- - When a workspace is open, the top-right toggles **Global / Workspace**; both MDs can be edited independently of which scope the current agent lives in. Switching to a non-existent file shows `(new)`; saving auto-creates the dir/file.
63
57
  - AGENT.md supports `{{var}}` placeholders (`{{<skill-id>.params.<key>}}` / `<<ENV>>` / built-ins) rendered on agent start — see [skills.md placeholder section](skills.md#placeholders-template-variables)
64
58
 
65
59
  ## Tool configuration
@@ -75,7 +69,7 @@ team: # a non-empty team is what enables delegation
75
69
  - executor # the agent ids this one may spawn
76
70
  ```
77
71
 
78
- **Workspace tools**: `file_read / file_write / file_edit / file_list / shell_exec / grep / glob / web_fetch` — listed by name under `tools`.
72
+ **Workspace tools**: `file_read / view_image / file_write / file_edit / file_list / shell_exec / grep / glob / web_fetch` — listed by name under `tools`.
79
73
 
80
74
  **Session tools**: `start_session / session_list / query_session / interrupt_session / stop_session / archive_session / get_session_output / query_agent` — **not** listed under `tools`. The whole bundle is granted automatically the moment an agent declares a non-empty `team`; an empty/absent `team` means no delegation (no session tools, no roster). To let a sub-agent delegate further, give it its own `team`. Add the agent's own id to its `team` to enable parallel self-spawn.
81
75
 
@@ -123,13 +117,14 @@ Internal agents (`internal: true`, e.g. self-evolution agents) have no Test butt
123
117
 
124
118
  ## Delete
125
119
 
126
- Right-click → Delete. Constraints:
120
+ Hover a **workspace** agent in the sidebar and click its trash icon. Global agents show a crown instead (no delete button there). Constraints:
127
121
  - The last global agent cannot be deleted (server-enforced)
128
122
  - Workspace agents delete freely
123
+ - `/agent delete <name>` (full access) removes either kind
129
124
 
130
125
  ## Common workflows
131
126
 
132
- **Copy a global agent into workspace for customisation**: edit it in Form view, switch scope to Workspace, Save — creates an independent copy under `<project>/.halo/agents/<id>/`.
127
+ **Copy a global agent into workspace for customisation**: copy the whole folder `~/.halo/global/agents/<id>/` to `<project>/.halo/agents/<id>/` and edit there. (Clicking `+` on Workspace with the same name creates a fresh scaffold under the same id, not a copy.)
133
128
 
134
129
  **Multi-agent collaboration**: the Default agent sees its teammates in the prompt roster, starts them with `start_session`, and the sub-agent auto-reports on completion. See [sessions.md](sessions.md).
135
130
 
@@ -146,7 +141,7 @@ priority: 0
146
141
 
147
142
  model:
148
143
  provider: aws-bedrock-claude-invoke # required, matches ~/.halo/global/models/<provider>.yaml
149
- id: global.anthropic.claude-sonnet-4-6 # default from settings.yaml params.model.default
144
+ id: global.anthropic.claude-sonnet-4-6 # the provider yaml's defaultModelId
150
145
  endpoint: https://bedrock-runtime.us-west-2.amazonaws.com # full endpoint URL; supports custom proxy
151
146
  maxTokens: 16384 # optional, max output tokens (default from provider yaml)
152
147
 
@@ -226,41 +221,12 @@ nano-banana:
226
221
  api_key: <<NANO_BANANA_KEY>>
227
222
  ```
228
223
 
229
- …where the schema lives at `extensions/skills/nano-banana/config.yaml`.
224
+ …where the schema lives in the skill's own `config.yaml` (`skills/nano-banana/config.yaml`).
230
225
 
231
226
  With `export NANO_BANANA_KEY=sk-xxx` in the env, the agent's system prompt gets the real key on start.
232
227
 
233
228
  ## Model registry — adding a new provider
234
229
 
235
- Halo ships with `aws-bedrock-claude-invoke` only. Hooking up OpenAI / direct Anthropic / another Bedrock variant takes two steps:
236
-
237
- ### 1. Declare the provider
238
-
239
- Create `~/.halo/global/models/<providerId>.yaml`:
240
-
241
- ```yaml
242
- id: openai # required; matches the filename
243
- displayName: OpenAI
244
- description: OpenAI Chat Completions
245
- models:
246
- - id: gpt-4o
247
- displayName: GPT-4o
248
- maxOutputTokens: 16384
249
- capabilities:
250
- promptCaching:
251
- ttlPresets:
252
- - { value: 5m, label: 5min }
253
- # No thinking block → UI automatically hides the Thinking button
254
- ```
255
-
256
- ### 2. Implement the runtime
257
-
258
- Add a case in the switch inside [packages/server/src/agents/model-runtime.ts](../../../packages/server/src/agents/model-runtime.ts) returning a class that implements `ModelRuntime` (`messages` + `run()`). Interface detail in [design/agent.md](../design/agent.md#agent-instance).
259
-
260
- ### Usage
261
-
262
- Set `model.provider: openai` in your agent.yaml; the Provider dropdown in Form view lists it automatically.
263
-
264
- ### Current state
230
+ Halo ships 13 providers (`aws-bedrock-claude-invoke`, `aws-bedrock-mantle`, `aws-bedrock-openai`, `anthropic`, `openai`, `kimi`, `deepseek`, `minimax`, `qwen`, `hunyuan`, `doubao`, `zhipu`, `mimo-token-plan-china`); each one is a yaml under `~/.halo/global/models/` plus a matching case in [packages/server/src/agents/model-runtime.ts](../../../packages/server/src/agents/model-runtime.ts). For any OpenAI- or Anthropic-compatible endpoint you don't need new code: pick `openai` or `anthropic` as the provider and fill in the endpoint and model id in Form view.
265
231
 
266
- Only `aws-bedrock-claude-invoke` ships an implementation. If you drop a provider yaml without a runtime implementation, session spawn fails with `Unknown provider "..."`.
232
+ A truly new provider takes a yaml manifest plus a runtime case; the step-by-step is in [dev/add-model-provider.md](../dev/add-model-provider.md). If you drop a provider yaml without a runtime case, session spawn fails with `Unknown provider "..."`.
@@ -28,7 +28,7 @@ This folder has one onboarding guide per channel. Pick the one you want and foll
28
28
  SessionManager (per workspace)
29
29
  ```
30
30
 
31
- Each channel handler is a thin adapter between its native protocol and halo's `SessionManager`. Slash commands (`/session` `/agent` `/skill` `/workspace` `/cron` `/acp` `/evo` `/help`) are shared across all of them — see `channels/shared/commands.ts`.
31
+ Each channel handler is a thin adapter between its native protocol and halo's `SessionManager`. Slash commands (`/session` `/agent` `/skill` `/workspace` `/cron` `/acp` `/evo` `/extension` `/help`) are shared across all of them — see `channels/shared/commands.ts`.
32
32
 
33
33
  The ACP adapter is **not** a channel — it's a stdio bridge that translates ACP JSON-RPC into the web channel's HTTP+SSE. Counted here only because users go through the same "set up an account, get a token" flow.
34
34
 
@@ -42,16 +42,19 @@ What the admin UI actually asks you for. Auto-filled fields (botUsername, botUse
42
42
  | Telegram | Bot token, workspace path | Label, access level, language, allowed users | BotFather `/newbot` |
43
43
  | Slack | Bot token (`xoxb-`), App token (`xapp-`), workspace path | Label, access level, language | api.slack.com → Create App → install + Socket Mode |
44
44
  | Feishu | App ID, App Secret, workspace path | Verification token, encrypt key, label, access level, language | open.feishu.cn → Create App + add bot capability |
45
- | WeCom | Bot ID, Secret, workspace path | Label, access level, language, allowed users | work.weixin.qq.com → 智能机器人 → create bot, copy Bot ID + Secret |
45
+ | WeCom | Bot ID, Secret, workspace path | Label, access level, language | work.weixin.qq.com → 智能机器人 → create bot, copy Bot ID + Secret |
46
46
  | WeChat | Workspace path (set before scanning) | Label, access level, language | QR scan in admin UI; bot token comes back from the scan |
47
47
 
48
+ In the admin account list every row shows an access-level badge next to its name, and a red **(path does not exist)** after the workspace path when the bound folder is missing on disk (the account is skipped at startup until you re-bind it). Secret fields in the Add forms (Telegram bot token, Slack bot / app tokens, Feishu App Secret and Encrypt Key, WeCom Secret) are password inputs — pasted values are masked.
49
+
48
50
  ## Access level
49
51
 
50
52
  Every channel account carries an `accessLevel`:
51
53
 
52
- - `readonly` (default) — sandboxed, workspace mounted read-only, secrets dir not mounted, only read-only tools (`file_read`, `view_image`, `file_list`, `grep`, `glob`) injected
54
+ - `readonly` (default) — sandboxed, workspace mounted read-only, secrets dir not mounted. With an OS sandbox the agent gets all its tools (writes fail); without one only the read-only tools (`file_read`, `view_image`, `file_list`, `grep`, `glob`)
53
55
  - `workspace` — sandboxed, workspace mounted read-write, secrets dir not mounted
54
- - `full` — no sandbox, all tools, can `/workspace <path>` to another workspace
56
+ - `full` — no sandbox, all tools, can `/workspace switch <path>` to another workspace
57
+ - `observer` — Web accounts only: global read-only, meant for dashboards / metrics (Halo City, `/api/metrics`); chat runs at `readonly`. A chat-channel account that still holds `observer` from older data shows an Observer badge, and its edit form keeps that level unless you pick another one
55
58
 
56
59
  When in doubt start at `readonly` and raise it later. The setting is on the **account row**, so a single Slack workspace can have one `readonly` bot account in #general and a separate `full` account in #ops.
57
60