@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.
- 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-e0faeff1c4a0b0b9.js +1 -0
- package/admin-out/_next/static/chunks/app/page-44f707ea8dad57da.js +1 -0
- package/admin-out/_next/static/chunks/{webpack-1a264efd965db7ab.js → webpack-edf248e07333566d.js} +1 -1
- package/admin-out/_next/static/css/b8c87b919758c52b.css +3 -0
- package/admin-out/index.html +1 -1
- package/admin-out/index.txt +5 -5
- package/bundled-docs/design/agent.md +10 -8
- package/bundled-docs/design/agentcore.md +13 -10
- package/bundled-docs/design/architecture.md +24 -14
- package/bundled-docs/design/canvas-extensions.md +19 -4
- 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 +14 -14
- package/bundled-docs/design/slack.md +22 -20
- package/bundled-docs/design/storage.md +30 -29
- 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 +86 -57
- package/bundled-docs/dev/deploy.md +8 -6
- package/bundled-docs/dev/env.md +29 -21
- package/bundled-docs/dev/logging.md +15 -29
- package/bundled-docs/dev/previews.md +20 -11
- package/bundled-docs/dev/tools.md +26 -14
- 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 +10 -9
- package/bundled-docs/guide/cli.md +10 -8
- package/bundled-docs/guide/delegation-and-access.md +1 -1
- 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 +9 -7
- package/bundled-docs/guide/testing-agents-and-skills.md +14 -15
- package/bundled-docs/guide/workspace.md +29 -17
- package/bundled-docs/requirements/agent.md +19 -18
- package/bundled-docs/requirements/chat.md +16 -17
- package/bundled-docs/requirements/command.md +10 -10
- package/bundled-docs/requirements/editor.md +12 -10
- package/bundled-docs/requirements/explorer.md +10 -7
- package/bundled-docs/requirements/session.md +16 -17
- package/bundled-docs/requirements/settings.md +30 -18
- 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 +10587 -12088
- package/package.json +1 -1
- package/templates/agents/default/agent.yaml +1 -0
- package/templates/config.yaml +6 -0
- package/templates/skills/extension/SKILL.md +25 -14
- package/templates/skills/extension/config.yaml +9 -0
- package/templates/skills/extension/templates/ext.sh +166 -34
- package/templates/skills/web-search/SKILL.md +20 -6
- package/templates/skills/web-search/config.yaml +13 -0
- package/templates/skills/web-search/search.py +27 -6
- package/templates/skills/workspace/share.md +11 -3
- package/templates/skills/workspace/stage.py +85 -5
- package/admin-out/_next/static/chunks/989-92e9683ae8d39750.js +0 -1
- package/admin-out/_next/static/chunks/app/page-7a635969198fc62b.js +0 -1
- package/admin-out/_next/static/css/4ea5f674146e54d8.css +0 -3
- /package/admin-out/_next/static/{lm8Bz3ugTSORyuSXfyx56 → visjvN510yFK-Ff5zwx-0}/_buildManifest.js +0 -0
- /package/admin-out/_next/static/{lm8Bz3ugTSORyuSXfyx56 → visjvN510yFK-Ff5zwx-0}/_ssgManifest.js +0 -0
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,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,
|
|
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
|
+
- **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
|
|
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`)
|
|
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)
|
|
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.
|
|
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`.
|
|
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
|
```
|
|
@@ -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.
|
|
12
|
-
│ renders <Suspense><Component/></Suspense
|
|
13
|
-
|
|
14
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
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**:
|
|
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) | `
|
|
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
|
|
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 |
|
|
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`
|
|
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
|
-
"
|
|
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
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
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
|
|
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
|
|
15
|
+
Click the `🤖 Agents` icon in the Activity Bar.
|
|
16
16
|
|
|
17
17
|
```
|
|
18
18
|
┌─────────────────┬──────────────────────────┐
|
|
19
|
-
│ Agents │ Form /
|
|
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
|
-
|
|
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.
|
|
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 —
|
|
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
|
|
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
|
|
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
|
-
###
|
|
56
|
-
Edit
|
|
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
|
-
|
|
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**:
|
|
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 #
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|