@turmind/halo 1.5.11 → 1.6.0
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/README.md +2 -2
- package/admin-out/404.html +1 -1
- package/admin-out/_next/static/chunks/{941-4719e7343b54b9bd.js → 147-d4200c70445aabb6.js} +1 -1
- package/admin-out/_next/static/chunks/989-a7f752480eaaa2dc.js +1 -0
- package/admin-out/_next/static/chunks/app/{layout-5ec6c5f713a8562b.js → layout-ea161287d934ba96.js} +1 -1
- package/admin-out/_next/static/chunks/app/page-8173112f256bc483.js +1 -0
- package/admin-out/_next/static/chunks/{webpack-efd27999305b080a.js → webpack-dc73047e0279979b.js} +1 -1
- package/admin-out/_next/static/css/7b736202f0b1dbfd.css +1 -0
- package/admin-out/_next/static/css/981c9e5610ce535a.css +1 -0
- package/admin-out/index.html +1 -1
- package/admin-out/index.txt +6 -5
- package/bundled-docs/design/a2a.md +286 -0
- package/bundled-docs/design/agentcore.md +194 -122
- package/bundled-docs/design/architecture.md +2 -2
- package/bundled-docs/design/canvas-extensions.md +30 -9
- package/bundled-docs/design/observability.md +12 -4
- package/bundled-docs/design/relay.md +1 -1
- package/bundled-docs/design/session.md +3 -1
- package/bundled-docs/design/storage.md +9 -2
- package/bundled-docs/design/web.md +5 -56
- package/bundled-docs/dev/api.md +70 -4
- package/bundled-docs/dev/deploy.md +2 -1
- package/bundled-docs/dev/env.md +8 -4
- package/bundled-docs/dev/tools.md +93 -3
- package/bundled-docs/guide/channels/web.md +0 -26
- package/bundled-docs/guide/cli.md +40 -4
- package/bundled-docs/guide/delegation-and-access.md +3 -2
- package/bundled-docs/guide/skills.md +1 -1
- package/bundled-docs/requirements/agent.md +1 -1
- package/bundled-docs/requirements/chat.md +3 -1
- package/bundled-docs/requirements/editor.md +9 -3
- package/bundled-docs/requirements/workspace.md +6 -0
- package/dist/index.js +4428 -2299
- package/package.json +1 -1
- package/templates/models/aws-bedrock-claude-invoke.yaml +40 -1
- package/templates/models/aws-bedrock-openai.yaml +100 -6
- package/templates/prompts/all/RUNTIME.md +2 -1
- package/templates/skills/send-file/SKILL.md +1 -1
- package/templates/skills/send-file/a2a.md +13 -0
- package/admin-out/_next/static/chunks/989-86b0f677f69ccf3b.js +0 -1
- package/admin-out/_next/static/chunks/app/page-4792aa1c79f7e7f1.js +0 -1
- package/admin-out/_next/static/css/97f3ac570664d044.css +0 -3
- /package/admin-out/_next/static/{GO2W5JoiC46FIxLjtFAXx → _F_Fkyh9PainUNY82EEjY}/_buildManifest.js +0 -0
- /package/admin-out/_next/static/{GO2W5JoiC46FIxLjtFAXx → _F_Fkyh9PainUNY82EEjY}/_ssgManifest.js +0 -0
|
@@ -28,6 +28,7 @@ Defines the persisted-data format for every Halo surface. Format changes must re
|
|
|
28
28
|
│ ├── evo.db # Cross-workspace evolution queue (evolution_runs + evolution_applies)
|
|
29
29
|
│ ├── cron.db # Cross-workspace cron jobs + run history (cron_jobs + cron_runs)
|
|
30
30
|
│ ├── runs.db # Run ledger: (workspace, session_id) rows for server-driven runs in flight; steady state empty (running_sessions)
|
|
31
|
+
│ ├── a2a.db # A2A: inbound tasks, push configs + outbox, outbound dispatches (see design/a2a.md)
|
|
31
32
|
│ ├── extensions/<id>/ # Installed Canvas preview extensions (halo-extension.json + bundle)
|
|
32
33
|
│ ├── logs/ # Runtime logs (server.log)
|
|
33
34
|
│ │ ├── evo/ # Per-evo-run wrapper logs
|
|
@@ -37,6 +38,7 @@ Defines the persisted-data format for every Halo surface. Format changes must re
|
|
|
37
38
|
├── secrets/ # Sensitive files (not mounted into sandbox)
|
|
38
39
|
│ ├── settings.yaml # Global settings (API keys, credentials)
|
|
39
40
|
│ ├── config.yaml # System config (admin password, server settings)
|
|
41
|
+
│ ├── a2a-remotes.yaml # A2A remote list (server-wide, full-only; see design/a2a.md)
|
|
40
42
|
│ └── channels/
|
|
41
43
|
│ └── channels.db # Channel accounts DB
|
|
42
44
|
|
|
@@ -66,6 +68,9 @@ Defines the persisted-data format for every Halo surface. Format changes must re
|
|
|
66
68
|
├── assets/<channel>/inbound/<accountId>/<date>/ # Inbound media per channel (image/voice/video/file)
|
|
67
69
|
├── runtime.lock # Workspace runtime ownership marker (pid) — see below
|
|
68
70
|
├── halo.db # Per-workspace sqlite (sessions metadata, disabled-items)
|
|
71
|
+
├── a2a.db # AgentCore mode only: A2A tasks + push outbox (instead of ~/.halo/global/a2a.db) — design/agentcore.md
|
|
72
|
+
├── runs.db # AgentCore mode only: the run ledger (instead of ~/.halo/global/runs.db)
|
|
73
|
+
├── agentcore.lease # AgentCore mode only: single-writer heartbeat lease file
|
|
69
74
|
└── docs/ # Project docs (requirements/design/dev/test/plans)
|
|
70
75
|
```
|
|
71
76
|
|
|
@@ -402,8 +407,10 @@ A value of the form `<<ENV_NAME>>` is replaced at read time with `process.env.EN
|
|
|
402
407
|
| `general.limits.tool_result_ui_chars` | 65536 | Per-tool-result cap on the content **stored for UI display** (admin/web chat panel). Far larger than the LLM cap so a normal command's full output stays visible, but bounded so a multi-MB `cat` can't bloat the session file / WS payload / browser render; excess truncated with a marker pointing at `file_read` |
|
|
403
408
|
| `general.limits.terminal_scrollback_bytes` | 50000 | Off-screen scrollback bytes retained per detached persistent terminal |
|
|
404
409
|
| `general.sandbox.hidden_dirs` | `''` | Extra dirs hidden from workspace/readonly sessions — bwrap tmpfs overlay (Linux) / Seatbelt deny (macOS). **Appended** to the built-in `DEFAULT_HIDDEN_DIRS` (`tools/sandbox.ts`: `~/.halo/secrets,~/.aws,~/.ssh,~/.gnupg,~/.docker,~/.config/gh,~/.halo/global/internal-sessions,~/.halo/global/logs`), which are always included. Full sessions, the admin file explorer and the terminal are not affected |
|
|
405
|
-
| `general.sandbox.hidden_files` | `''` | Extra files hidden from workspace/readonly sessions — bwrap bind of the empty `~/.halo/.sandbox-empty`, reads as empty (Linux) / Seatbelt deny (macOS). **Appended** to the built-in `DEFAULT_HIDDEN_FILES` (`~/.npmrc,~/.bash_history,~/.gitconfig,~/.git-credentials,~/.netrc,~/.halo/global/{evo,cron,runs}.db` + `-wal` / `-shm`), which are always included. Full sessions, the admin file explorer and the terminal are not affected |
|
|
410
|
+
| `general.sandbox.hidden_files` | `''` | Extra files hidden from workspace/readonly sessions — bwrap bind of the empty `~/.halo/.sandbox-empty`, reads as empty (Linux) / Seatbelt deny (macOS). **Appended** to the built-in `DEFAULT_HIDDEN_FILES` (`~/.npmrc,~/.bash_history,~/.gitconfig,~/.git-credentials,~/.netrc,~/.halo/global/{evo,cron,runs,a2a}.db` + `-wal` / `-shm`), which are always included. Full sessions, the admin file explorer and the terminal are not affected |
|
|
406
411
|
| `general.logging.level` | warn | `debug` / `info` / `warn` / `error`. Restart required |
|
|
412
|
+
| `general.a2a.public_url` | `''` | Global-only. Origin peers use to reach this server for A2A (no trailing slash): the agent card's interface URL and the default push-webhook base for `a2a_send`. Empty = derived from the request host (no outbound dispatch without it or a remote's `push_base`). Env `HALO_A2A_PUBLIC_URL` overrides. See [a2a.md](a2a.md) |
|
|
413
|
+
| `general.a2a.url_allowlist` | `100.64.0.0/10,*.ts.net` | Global-only. CIDRs / host patterns A2A egress may reach besides public https (push webhooks, remote cards and RPC, inbound image URLs). Private / loopback only when listed; plain http only to listed hosts / ranges; link-local always refused. See [a2a.md](a2a.md#push-delivery-and-url-policy) |
|
|
407
414
|
| `general.observability.endpoint` | `''` | OTLP base URL of an OpenTelemetry collector (e.g. `http://localhost:4318`); empty = off. Restart required. See [observability.md](observability.md) |
|
|
408
415
|
| `general.observability.service_name` | `halo` | OTel resource `service.name`. Restart required |
|
|
409
416
|
| `general.observability.headers` | `''` | **Secret.** Extra OTLP request headers, comma-separated `k=v`. Restart required |
|
|
@@ -634,7 +641,7 @@ All channel types (telegram, web, wechat, slack, feishu, wecom) share one table.
|
|
|
634
641
|
### Schema change rules
|
|
635
642
|
|
|
636
643
|
- `schema.sql` / each db's `CREATE_SQL` always describes the FULL current shape, so a fresh db is complete after the CREATEs alone
|
|
637
|
-
- Every change to an already-existing db gets a numbered slot in that file's ordered migration list — `HALO_MIGRATIONS` (`db/index.ts`), `CRON_MIGRATIONS` (`db/cron-db.ts`); `channel-db.ts` / `evo-db.ts` / `runs-db.ts` currently pass an empty list. `runMigrations(sqlite, list)` (`db/migrate.ts`) runs `list[user_version..]` in order, each in its own transaction, stamping `PRAGMA user_version = i + 1` after each
|
|
644
|
+
- Every change to an already-existing db gets a numbered slot in that file's ordered migration list — `HALO_MIGRATIONS` (`db/index.ts`), `CRON_MIGRATIONS` (`db/cron-db.ts`), `A2A_MIGRATIONS` (`db/a2a-db.ts`); `channel-db.ts` / `evo-db.ts` / `runs-db.ts` currently pass an empty list. `runMigrations(sqlite, list)` (`db/migrate.ts`) runs `list[user_version..]` in order, each in its own transaction, stamping `PRAGMA user_version = i + 1` after each
|
|
638
645
|
- A fresh db is also at `user_version 0` and runs the whole list, so every slot must be a no-op against the current shape (`addColumnIfMissing`, `CREATE … IF NOT EXISTS`)
|
|
639
646
|
- Append only — never reorder or edit a shipped slot. A db opened by a newer halo (user_version > list length) logs a warning and continues
|
|
640
647
|
- Do not drop or rename existing columns
|
|
@@ -10,13 +10,12 @@ Browser-based access to Halo via token-authenticated HTTP API. Supports SSE stre
|
|
|
10
10
|
Halo server (9527) ──┤── channels/telegram/ ├── SessionManager
|
|
11
11
|
└── channels/web/ ─┘ (per workspace, via Registry)
|
|
12
12
|
↕ HTTP + SSE
|
|
13
|
-
|
|
14
|
-
or any HTTP client
|
|
13
|
+
any HTTP client (custom frontend, halo-city, ACP adapter)
|
|
15
14
|
```
|
|
16
15
|
|
|
17
16
|
Common slash commands (`/help`, `/evo`, and the object commands `/session`, `/agent`, `/skill`, `/workspace`) live in `channels/shared/commands.ts`; each channel handler is a thin adapter.
|
|
18
17
|
|
|
19
|
-
Web channel is a public HTTP API on Halo.
|
|
18
|
+
Web channel is a public HTTP API on Halo.
|
|
20
19
|
|
|
21
20
|
## Data model
|
|
22
21
|
|
|
@@ -63,7 +62,7 @@ File: `packages/server/src/routes/web.ts`
|
|
|
63
62
|
|
|
64
63
|
All public endpoints require `x-token` header (or `?token=` query param) with a valid account token. No cookie auth needed.
|
|
65
64
|
|
|
66
|
-
> These paths are listed in `PUBLIC_PATHS` (`middleware/auth.ts`) so they bypass the admin cookie gate: `/api/web/chat`, `/api/web/sessions` (POST mint + GET list), `/api/web/stop`, `/api/web/history`, `/api/web/subscribe`, `/api/web/file`, and `/api/show/state` (the [halo-city](../../../halo-city/) snapshot — see [dev/api.md](../dev/api.md#show-world-snapshot)). The server CORS allowlist includes the `x-token` header, so browser-based custom frontends (
|
|
65
|
+
> These paths are listed in `PUBLIC_PATHS` (`middleware/auth.ts`) so they bypass the admin cookie gate: `/api/web/chat`, `/api/web/sessions` (POST mint + GET list), `/api/web/stop`, `/api/web/history`, `/api/web/subscribe`, `/api/web/file`, and `/api/show/state` (the [halo-city](../../../halo-city/) snapshot — see [dev/api.md](../dev/api.md#show-world-snapshot)). The server CORS allowlist includes the `x-token` header, so browser-based custom frontends (e.g. halo-city) can call these cross-origin.
|
|
67
66
|
|
|
68
67
|
### POST `/api/web/chat`
|
|
69
68
|
|
|
@@ -96,7 +95,7 @@ data: {"type":"error","error":"..."}
|
|
|
96
95
|
|
|
97
96
|
#### SSE keepalive
|
|
98
97
|
|
|
99
|
-
`/web/chat` and `/web/subscribe` write an SSE comment line `: keepalive` every 15 s (`SSE_KEEPALIVE_MS` / `streamWithKeepalive` in `routes/web.ts`; the timer is cleared when the stream ends). A long tool call can otherwise leave the stream silent long enough for an idle reverse proxy / CDN to cut it. Comment lines aren't events — `EventSource` and any spec-following parser drop them; a hand-rolled parser must skip lines that don't start with `data
|
|
98
|
+
`/web/chat` and `/web/subscribe` write an SSE comment line `: keepalive` every 15 s (`SSE_KEEPALIVE_MS` / `streamWithKeepalive` in `routes/web.ts`; the timer is cleared when the stream ends). A long tool call can otherwise leave the stream silent long enough for an idle reverse proxy / CDN to cut it. Comment lines aren't events — `EventSource` and any spec-following parser drop them; a hand-rolled parser must skip lines that don't start with `data:`.
|
|
100
99
|
|
|
101
100
|
> **Batch-boundary `complete` (must be absorbed, never closes the stream)**: when a root session drains a queue of multiple messages, the server runs N merged turns and emits an internal `complete` with `batchBoundary: true` between rounds (see [session.md](session.md#message-queue-and-drain)). The SSE generators in `channels/web/handler.ts` (`listenSession().events()` plus the `createMediaBuffer()` event processor) flush the just-finished round's text on a `batchBoundary` complete but **do not** send a `complete` SSE frame and **do not** set `done` — the response stays open for the next round; only the **terminal** (unmarked) `complete` closes the HTTP stream. Without this guard a producer→sub-agent fan-out would truncate the web client after the first round. The `batchBoundary` flag is therefore an internal server-side event marker only — it is never serialized into the SSE payload a client sees, so a custom frontend just consumes one ordinary `complete` at the end. (ACP rides on this channel, so it inherits the same safe behavior.)
|
|
102
101
|
|
|
@@ -110,7 +109,7 @@ By default each token is bound 1:1 to the workspace its admin row configured, an
|
|
|
110
109
|
- `sessionId` (string, optional): explicit halo session id. Bypasses the account's active-session pointer entirely. **Gated by ownership**: `full` tokens may address any id; readonly / workspace tokens only ids their own account minted (`web_<accountId>_*`), plus the goal session (`goal_<ts>`) bound to one of those — goal mode routes that session's chat there and the `session` frame names it, so re-attach / history / stop must reach it. Anything else is refused with HTTP 403 `{error: "session not owned by this token"}` at the route layer (before the SSE stream opens), on `/web/chat`, `/web/stop`, `/web/history` and `/web/subscribe` alike. If the session doesn't yet exist, the server creates it with the supplied id (so callers can pre-mint stable ids and address them across reconnects).
|
|
111
110
|
- `agentId` (string, optional): only consulted when the request is creating a new session (no row yet for `sessionId`). Picks the agent profile to bootstrap with. Defaults to `default`.
|
|
112
111
|
|
|
113
|
-
|
|
112
|
+
Simple clients needn't use any of these; per-token defaults apply. Three accepted transports per request, lowest-priority first:
|
|
114
113
|
|
|
115
114
|
1. Query string: `?workspace=…&sessionId=…&agentId=…`
|
|
116
115
|
2. Headers: `x-workspace`, `x-session-id`, `x-agent-id`
|
|
@@ -188,56 +187,6 @@ Protected by cookie auth (admin panel). File: `packages/server/src/routes/web.ts
|
|
|
188
187
|
|
|
189
188
|
---
|
|
190
189
|
|
|
191
|
-
## packages/web-demo — Standalone Demo Frontend
|
|
192
|
-
|
|
193
|
-
An independent Express app that provides a browser UI for the web channel. It holds the Halo token server-side and exposes its own password-based auth.
|
|
194
|
-
|
|
195
|
-
### Deployment
|
|
196
|
-
|
|
197
|
-
```bash
|
|
198
|
-
cd packages/web-demo
|
|
199
|
-
node server.js
|
|
200
|
-
```
|
|
201
|
-
|
|
202
|
-
### Environment variables
|
|
203
|
-
|
|
204
|
-
| Variable | Required | Description |
|
|
205
|
-
|---|---|---|
|
|
206
|
-
| `HALO_API` | Yes | Halo server URL (e.g. `http://localhost:9527`) |
|
|
207
|
-
| `HALO_TOKEN` | Yes | Token from Halo admin panel (Web settings) |
|
|
208
|
-
| `HALO_WEB_DEMO_PASSWORD` | No | Password for web-demo login. Empty = open access |
|
|
209
|
-
| `PORT` | No | Listen port (default: `9528`) |
|
|
210
|
-
|
|
211
|
-
### Security model
|
|
212
|
-
|
|
213
|
-
- **Proxy mode (default)**: `HALO_TOKEN` and `HALO_API` are server-side only, never exposed to the browser; frontend uses same-origin relative paths (`/chat`, `/history`, etc.); web-demo server proxies all requests to Halo, injecting the token in `x-token` header. Session auth: HMAC token in `x-session` header + localStorage. All proxy routes including `GET /file` are gated by the auth middleware.
|
|
214
|
-
- **Session TTL: 7 days**, enforced without server-side storage — the signed payload *is* the issue timestamp, so `isValidSession` re-parses it and rejects anything older than `SESSION_TTL_MS`. A leaked token therefore stops working on its own; the frontend gets a 401 and falls back to the login view (`handleAuthFail → logout`). Previously the signature alone was checked, making every issued token valid forever.
|
|
215
|
-
- **Direct-connect mode (opt-in)**: the gear panel takes a halo server URL + web-channel token; the browser then calls `<server>/api/web/*` directly with `x-token`, bypassing the proxy (no password step). The token is stored in that browser's localStorage by explicit user choice — the UI says so next to the field. One slot; clearing it returns to proxy mode. Works because the server CORS reflects any origin and allowlists `x-token`.
|
|
216
|
-
|
|
217
|
-
### Proxy routes
|
|
218
|
-
|
|
219
|
-
| Frontend path | Upstream Halo path |
|
|
220
|
-
|---|---|
|
|
221
|
-
| `POST /chat` | `POST /api/web/chat` |
|
|
222
|
-
| `POST /stop` | `POST /api/web/stop` |
|
|
223
|
-
| `GET /history` | `GET /api/web/history` |
|
|
224
|
-
| `GET /subscribe` | `GET /api/web/subscribe` |
|
|
225
|
-
| `GET /file` | `GET /api/web/file` |
|
|
226
|
-
|
|
227
|
-
### Features
|
|
228
|
-
|
|
229
|
-
- Markdown rendering for assistant text (marked@12, gfm + breaks) with typewriter streaming; user text stays escaped plain text
|
|
230
|
-
- SSE streaming with collapsible thinking / tool-call step blocks
|
|
231
|
-
- Auto-reconnect: if session is running on page load, subscribes to live stream; history renders rebuild-from-scratch (never append)
|
|
232
|
-
- Message queue: can send while agent is running, messages queue and send sequentially
|
|
233
|
-
- Slash commands (`/help`, `/session`, `/agent`, `/skill`, `/workspace`), session/agent switcher menus
|
|
234
|
-
- Image upload + camera capture (camera button is mobile-only — the `capture` attribute is a no-op on desktop, where it would duplicate the file picker)
|
|
235
|
-
- Voice recording (webm audio saved to workspace, path given to agent)
|
|
236
|
-
- Stop/interrupt button
|
|
237
|
-
- i18n: auto-detects browser language (`navigator.language`), toggle button in header, persists to `localStorage`
|
|
238
|
-
- Mobile-first layout: 100dvh-safe, safe-area insets, ≥44px touch targets, bottom-sheet menus/settings on small widths, visual language shared with `packages/agentcore-demo`
|
|
239
|
-
- Page load needs internet for two CDN deps (tailwindcss, marked) — no longer fully self-contained
|
|
240
|
-
|
|
241
190
|
## Integration guide (custom frontend)
|
|
242
191
|
|
|
243
192
|
To build your own frontend against Halo's web channel:
|
package/bundled-docs/dev/api.md
CHANGED
|
@@ -54,10 +54,12 @@ All file operations validate the path stays inside the project root (prevents pa
|
|
|
54
54
|
|---|---|---|
|
|
55
55
|
| GET | `/api/fs/home` | Returns server `homedir()` — fallback when the frontend has no `?folder=` URL param. Returns `{ home }`. |
|
|
56
56
|
| GET | `/api/fs/exists?path=/abs` | Validates an absolute path exists and whether it's a directory. Returns `{exists, isDirectory?}`. |
|
|
57
|
-
| GET | `/api/fs/browse?path=/abs` | Lists immediate directory children (hidden ones dropped). Returns `{path, parent, entries: [{name, path}]}`. |
|
|
57
|
+
| GET | `/api/fs/browse?path=/abs` | Lists immediate directory children (hidden ones dropped). Returns `{path, parent, entries: [{name, path}]}`. With `files=1` (the extension file picker, capability `fs-read`): files too — `entries: [{name, path, type: 'file'\|'directory', size?}]`, directories first, dot-entries shown, only the file tree's noise (`.git`, `.DS_Store`, `node_modules`, `__pycache__`) skipped, symlinks counted as what they point at; `sizes=1` adds each file's `size`. A directory on the sandbox hidden lists answers 404 like a missing one, and hidden entries are left out of the listing. |
|
|
58
|
+
| GET | `/api/fs/stat?path=/abs` | `fs-read` `scope: 'system'` stat. Returns `{path, realPath, size, modifiedAt, isDirectory}` (`realPath` resolves symlinks). 400 `absolute path without .. required`; 404 for a missing path **and** for one on the sandbox hidden lists (same message); 403 on `EACCES` / `EPERM`. |
|
|
59
|
+
| GET | `/api/fs/raw?path=/abs` | `fs-read` `scope: 'system'` read: the file's bytes, streamed (`application/octet-stream` + `Content-Length`; the stream is destroyed if the client aborts). Same path rules and error shapes as `/api/fs/stat`; 400 `not a file` for a directory. |
|
|
58
60
|
| POST | `/api/fs/workspace/resolve` | Resolve `{path}` to an absolute path and run `ensureWorkspaceHalo()`. Returns `{id, path}`. Used by the workspace-picker on switch. 404 `path not found` when it doesn't exist; 400 `not a directory` for a file; 400 `filesystem root cannot be a workspace` for `/` (or a drive root) — `ensureWorkspaceHalo()` writes a whole `.halo/` scaffold, so those two shapes are refused before the write. Deeper system trees (`/usr`, `/etc`) are deliberately **not** banned. |
|
|
59
61
|
|
|
60
|
-
Absolute paths only. Purpose: Explorer's workspace picker and switching validation. `/api/fs/browse` is intentionally unrestricted (it's a read-only picker over the server's filesystem — same trust boundary as the admin cookie). Reading/writing files still goes through `/api/files/*` and remains project-sandboxed.
|
|
62
|
+
Absolute paths only. Purpose: Explorer's workspace picker and switching validation. `/api/fs/browse` is intentionally unrestricted (it's a read-only picker over the server's filesystem — same trust boundary as the admin cookie). Reading/writing files still goes through `/api/files/*` and remains project-sandboxed. The one exception is the read-only `/api/fs/stat` + `/api/fs/raw` + `/api/fs/browse?files=1` trio behind the `fs-read` extension capability: they take absolute paths (no `..` segment) and refuse the sandbox hidden lists (`~/.halo/secrets`, `~/.ssh`, `.git-credentials`, the global dbs … — `isHiddenHostPath` in `tools/sandbox.ts`, the same lists the agent sandbox masks) by **realpath**, so a symlink can't reach them. They are admin-cookie routes, never in `PUBLIC_PATHS`; `fs-read` itself is host-side UX gating (the extension iframe is same-origin and could call them directly), which is why the guard lives on the routes.
|
|
61
63
|
|
|
62
64
|
## Data Preview
|
|
63
65
|
|
|
@@ -94,6 +96,8 @@ Installed extensions are directories under `~/.halo/global/extensions/<id>/` wit
|
|
|
94
96
|
| GET | `/api/extensions/:id/:version/:token/<asset>` | **path token, no cookie** | Static file from the extension directory. 401 unless `verifyScopedToken(token,'ext')`; 404 if `version` ≠ installed version or the asset escapes the directory. Response headers: `Access-Control-Allow-Origin: *`, `Referrer-Policy: no-referrer`, `Cache-Control: public, max-age=31536000, immutable` (the version segment is the cache key) |
|
|
95
97
|
| WS | `/api/transcribe/stream?ext=<id>&lang=<auto\|xx-XX>` | cookie | Streaming transcription proxy to Amazon Transcribe (`routes/transcribe-ws.ts`; not mounted in AgentCore mode). Handshake 401 without the login cookie, 403 unless `ext` is installed and declares capability `transcribe`. Client → binary PCM s16le mono 16 kHz, text `{"type":"end"}` (flush, server closes 1000). Server → `{"type":"ready"}` (on connect; stream audio right after it), `{"type":"partial"\|"final",start,end,text,lang}` (seconds from the stream's first audio byte), `{"type":"error",code,message}` then close; `code` ∈ `credentials` \| `denied` \| `limit` \| `bad-request` \| `io`. Region / languages / credentials from the extension's `ext-<id>` settings (default chain when no keys) — see [design](../design/canvas-extensions.md#streaming-transcription-proxy) |
|
|
96
98
|
|
|
99
|
+
Extension capabilities the host understands (`ExtensionCapability`): `save`, `media`, `transcribe`, `fs-read`; an unknown value makes the manifest an error. `fs-read` adds no extension-specific route: the host serves the extension's scoped `fs` frames and `pick` file picker from the [Filesystem browse](#filesystem-browse-not-project-scoped--for-the-workspace-picker) routes above (`scope: 'system'`) and the ordinary `/api/files/*` routes (`scope: 'workspace'`) — see [design/canvas-extensions.md](../design/canvas-extensions.md#scoped-file-access-and-the-picker-fs-read).
|
|
100
|
+
|
|
97
101
|
**Why the asset route isn't cookie-authed.** The admin first hosted extensions in `<iframe sandbox="allow-scripts">` without `allow-same-origin` — an opaque origin. The document navigation still carries the cookie, but every subresource it loads (classic/module scripts, img, css, fetch, wasm, dynamic import, Worker) is a cross-site request with **no cookie**, and module/fetch/wasm need `Access-Control-Allow-Origin` too. A `?token=` query would be dropped when the document resolves `./viewer.js`-style relative URLs, so the token is a **path segment** that every relative URL inherits. The host now grants `allow-same-origin` (a cookie-auth proxy in front of halo — CloudFront + midway, oauth2-proxy — needs its *own* cookie on those requests, which only a same-origin iframe sends; see [design/canvas-extensions.md](../design/canvas-extensions.md#asset-serving-and-the-scoped-token)), so the admin cookie does travel again, but the route keeps the path token as its single auth path. `authMiddleware` lets exactly that path shape (`EXTENSION_ASSET_PATH = /^\/api\/extensions\/[^/]+\/[^/]+\/[^/]+\/./`) through without a cookie and the route verifies the token itself. The token is good for nothing else: `validateToken` (admin cookie / WS upgrade) refuses any JWT payload that carries a `scope`, so an asset token replayed as `halo_token` is a 401.
|
|
98
102
|
|
|
99
103
|
Install / uninstall don't broadcast themselves — the resulting rename / rm trips the `fs.watch` on `~/.halo/global/extensions/` (`extensions/watcher.ts`), the single notifier; both routes also call `rescanAndBroadcast()` so their response and the next `GET` already reflect the change. Changes reach every connected admin as the WS frame `extension:changed` (full snapshot — see [design/ws.md](../design/ws.md#other-server--client-messages)). All three cookie routes are "any logged-in admin" — the cookie has no access level, so install / delete cannot be restricted to full access (the agent-side `extension` skill is `requiresAccess: full`).
|
|
@@ -315,6 +319,60 @@ SSE responses (`/api/web/chat`, `/api/web/subscribe`) carry an SSE comment line
|
|
|
315
319
|
|
|
316
320
|
See [design/web.md](../design/web.md).
|
|
317
321
|
|
|
322
|
+
## A2A (Agent-to-Agent)
|
|
323
|
+
|
|
324
|
+
Files: `packages/server/src/a2a/routes.ts` (JSON-RPC handler), `exposure.ts` (path resolution, auth, card), `outbound.ts` (push receiver). A2A v1.0, JSON-RPC binding only. Mounted in normal server mode outside `/api/*` (no admin cookie) and before the static frontend; not in the CLI / TUI. AgentCore mode serves the same handler at `/` instead — see [AgentCore runtime container](#agentcore-runtime-container-halo-agentcore-only). Design: [design/a2a.md](../design/a2a.md).
|
|
325
|
+
|
|
326
|
+
| Method | Path | Purpose |
|
|
327
|
+
|---|---|---|
|
|
328
|
+
| POST | `/a2a/<rel>` (trailing `/` ok) | JSON-RPC 2.0 for the workspace `<A2A base>/<rel>` |
|
|
329
|
+
| GET | `/a2a/<rel>/.well-known/agent-card.json` | The agent card. `Cache-Control: private, max-age=300` + `ETag` (304 on `If-None-Match`); invalid `.halo/agent-card.json` → 500 |
|
|
330
|
+
| POST | `/a2a-push/:pushId` | Webhook receiver for remotes this server dispatched to (`a2a_send`) |
|
|
331
|
+
|
|
332
|
+
**Exposure**: `<rel>` is the workspace path relative to the base dir (`HALO_A2A_ROOT`, default the user's home), each segment percent-encoded. Only a workspace containing `.halo/agent-card.json` is served; empty / `.` / `..` segments, separators inside a segment, and symlinks leaving the base are refused. The card's interface URL is `general.a2a.public_url` (env `HALO_A2A_PUBLIC_URL`) + `/a2a/<rel>/`, else derived from the request host.
|
|
333
|
+
|
|
334
|
+
**Auth** (RPC and card alike): a web-channel token in `Authorization: Bearer <t>` or `x-token: <t>` — headers only, `?token=` is ignored. Same `resolveTokenAuth` core and `web-token` lockout bucket as the [Web channel](#public-endpoints-token-auth-via-x-token-header-or-token-query). Missing token → 401 `{error: "token required"}` (before any path lookup); invalid token, unknown / unexposed path, or a token bound to another workspace → the same 404 `{error: "not found"}`; lockout → 429. The token's access level becomes the session's (`readonly` / `observer` → the A2A read-only profile). A token never reaches another workspace. A GET of anything but the card path → 404.
|
|
335
|
+
|
|
336
|
+
**JSON-RPC**: header `A2A-Version: 1.0` required (missing → treated as 0.3 → `-32009`). Body ≤16 MB; batches refused. Answered HTTP 200 with a JSON-RPC body — errors included; only the auth failures above use HTTP statuses.
|
|
337
|
+
|
|
338
|
+
| Method | Params (main) | Result |
|
|
339
|
+
|---|---|---|
|
|
340
|
+
| `SendMessage` | `message { role: "ROLE_USER", parts, messageId?, contextId?, taskId?, metadata?["halo/interrupt"] }`, `configuration { returnImmediately?, taskPushNotificationConfig? }` | `{ task }`. Blocks until terminal (≤10 min, then the WORKING task) unless `returnImmediately: true` |
|
|
341
|
+
| `SendStreamingMessage` | as SendMessage | SSE: `{task}` snapshot → `statusUpdate` (interims) / `artifactUpdate` (`progress` live text) → terminal `{task}`, then close; `: keepalive` every 15 s |
|
|
342
|
+
| `GetTask` | `id`, `historyLength?` (ignored) | Task: `{ id, contextId, status { state, message?, timestamp }, artifacts, metadata?["halo/errorKind"] }` |
|
|
343
|
+
| `ListTasks` | `contextId?`, `status?`, `statusTimestampAfter?` (ISO-8601), `pageSize?` (1–100, default 50), `pageToken?`, `includeArtifacts?` | `{ tasks, nextPageToken, pageSize, totalSize }`, newest `updated_at` first; `nextPageToken` `""` on the last page; artifacts only with `includeArtifacts`, file bytes never (counted in `metadata["halo/omittedFiles"]`) |
|
|
344
|
+
| `CancelTask` | `id` | The CANCELED task (already canceled → returned as is) |
|
|
345
|
+
| `SubscribeToTask` | `id` | SSE as above, for a live task |
|
|
346
|
+
| `CreateTaskPushNotificationConfig` | `taskId`, `url`, `id?`, `token?`, `authentication? { scheme, credentials }` | The config. URL policy applies |
|
|
347
|
+
| `GetTaskPushNotificationConfig` | `taskId`, `id` | The config |
|
|
348
|
+
| `ListTaskPushNotificationConfigs` | `taskId` | `{ configs, nextPageToken: "" }` |
|
|
349
|
+
| `DeleteTaskPushNotificationConfig` | `taskId`, `id` | `null` (idempotent) |
|
|
350
|
+
| `GetExtendedAgentCard` | — | `-32004` |
|
|
351
|
+
|
|
352
|
+
Parts: `{ text }`, and images `{ raw: <base64>, mediaType, filename? }` / `{ url, mediaType? }` — png / jpeg / gif / webp, ≤5 MB each, ≤10 MB per message. States: `TASK_STATE_WORKING` → `TASK_STATE_COMPLETED` (artifact `result`) / `TASK_STATE_FAILED` / `TASK_STATE_CANCELED` (artifact `partial` when there is output). A `contextId` is always server-minted (`a2a_<accountId>_…`, from an earlier response).
|
|
353
|
+
|
|
354
|
+
| Code | When |
|
|
355
|
+
|---|---|
|
|
356
|
+
| `-32700` | Body is not JSON |
|
|
357
|
+
| `-32600` | Not a JSON-RPC 2.0 request, a batch array, or a body over 16 MB |
|
|
358
|
+
| `-32601` | Unknown method |
|
|
359
|
+
| `-32602` | Invalid params: empty message, bad `role`, unknown or foreign `contextId`, `taskId` not in `contextId`, image fetch / limit failure, refused push URL (`push url not allowed: <reason>`), bad `pageSize` / `pageToken` / `status` / timestamp |
|
|
360
|
+
| `-32603` | Internal error; the workspace couldn't be opened |
|
|
361
|
+
| `-32001` | Task (or push config) not found — includes another account's task |
|
|
362
|
+
| `-32002` | CancelTask on a COMPLETED / FAILED task |
|
|
363
|
+
| `-32004` | Unsupported: `taskId` of a terminal task, SubscribeToTask / push-config create on a terminal task, `GetExtendedAgentCard` |
|
|
364
|
+
| `-32005` | A part that is neither text nor a supported image (incl. `data` parts) |
|
|
365
|
+
| `-32009` | `A2A-Version` missing or not `1.0` |
|
|
366
|
+
|
|
367
|
+
**Push delivery** (to a caller's webhook): `POST <url>` with `Content-Type: application/a2a+json`, `A2A-Version: 1.0`, `X-A2A-Notification-Token: <token>` when the config has one, `Authorization: <scheme> <credentials>` when it has `authentication`. Body `{ task }` on a terminal state, `{ statusUpdate }` for an interim. The pushed task carries no file bytes: text and status stay, file parts are counted in `metadata['halo/omittedFiles']`, and GetTask returns them. Each event is sent once per config; non-2xx retries with backoff (4xx other than 408 / 429 gives up at once). The URL must pass `general.a2a.url_allowlist`.
|
|
368
|
+
|
|
369
|
+
**`POST /a2a-push/:pushId`** (receiver): token in `X-A2A-Notification-Token` (or `Authorization: Bearer`), compared constant-time with the dispatch's; unknown id or wrong token → 404 `{error: "not found"}`. Body `{ task }` or `{ statusUpdate }` whose task id must belong to that dispatch (else 404 `{error: "unknown task"}`); invalid JSON → 400.
|
|
370
|
+
|
|
371
|
+
The push is a doorbell: only the task id is read from the body. The receiver then runs its own authed GetTask on the remote, with a 10 s deadline:
|
|
372
|
+
- **GetTask succeeds:** answers `{ ok: true }` and injects the fetched Task into the calling session asynchronously — the report when it is terminal, an interim when it is WORKING with a status message.
|
|
373
|
+
- **GetTask fails** (network, timeout, HTTP or JSON-RPC error): answers **503** `{error: "task fetch failed, retry later"}` and delivers nothing, so the sender retries.
|
|
374
|
+
- **Dispatch already reported:** answers `{ ok: true }` without a fetch.
|
|
375
|
+
|
|
318
376
|
## Show (world snapshot)
|
|
319
377
|
|
|
320
378
|
File: `packages/server/src/routes/halo-city.ts`. Token auth (same `x-token` as
|
|
@@ -427,9 +485,17 @@ File: `packages/server/src/routes/evolution.ts`. Surfaces the global `evolution_
|
|
|
427
485
|
| DELETE | `/api/evolution/runs/:id` | Delete a finished run: its on-disk artifacts (run dir + archive zip + wrapper log) **and** the DB row, plus every apply that references it (artifacts + row, `evolution:apply_changed` `kind:'deleted'`). Rejected with 409 for in-flight states (`pending` / `running` / `approved`) so a live wrapper / queued apply isn't pulled out from under. Broadcasts `evolution:run_changed` with `kind:'deleted'`. |
|
|
428
486
|
| GET | `/api/evolution/applies` | List in-flight apply rows (`pending` / `running` / `syncing`, newest 200; used for status badges). Returns `{applies}`. |
|
|
429
487
|
|
|
430
|
-
## AgentCore
|
|
488
|
+
## AgentCore runtime container (`halo agentcore` only)
|
|
489
|
+
|
|
490
|
+
File: `packages/server/src/a2a/agentcore.ts`. Served **only** when `HALO_RUNTIME_MODE=agentcore-a2a` (set by `halo agentcore`; see [design/agentcore.md](../design/agentcore.md)), on port 9000, with no cookie / token auth — AgentCore verifies the caller's signed request before forwarding. Not present in normal server mode.
|
|
491
|
+
|
|
492
|
+
| Method | Path | Purpose |
|
|
493
|
+
|---|---|---|
|
|
494
|
+
| GET | `/ping` | `{"status":"HealthyBusy"}` while the workspace lease is held and a session is running or a push is pending, else `{"status":"Healthy"}` |
|
|
495
|
+
| POST | `/` | A2A JSON-RPC. Without the workspace lease: error `-32603` "workspace is in use by another runtime session…" |
|
|
496
|
+
| GET | `/.well-known/agent-card.json` | The agent card (`<ws>/.halo/agent-card.json`, seeded if missing); interface URL = `HALO_A2A_PUBLIC_URL` + `/`; no `securitySchemes` |
|
|
431
497
|
|
|
432
|
-
|
|
498
|
+
Any other path is 404. The former HTTP-protocol routes (`POST /invocations`, WS `/ws`) were removed 2026-10-09.
|
|
433
499
|
|
|
434
500
|
---
|
|
435
501
|
|
|
@@ -63,7 +63,7 @@ pnpm --filter @turmind/halo-admin build # next build + copy-monaco; never a ba
|
|
|
63
63
|
|
|
64
64
|
## 2. Runtime data locations
|
|
65
65
|
|
|
66
|
-
No directory needs to be created by hand. SQLite databases are created automatically on first use: per-workspace state at `<workspace>/.halo/halo.db`, plus global queues at `~/.halo/global/evo.db`, `~/.halo/global/cron.db` and `~/.halo/global/
|
|
66
|
+
No directory needs to be created by hand. SQLite databases are created automatically on first use: per-workspace state at `<workspace>/.halo/halo.db`, plus global queues at `~/.halo/global/evo.db`, `~/.halo/global/cron.db`, `~/.halo/global/runs.db` and `~/.halo/global/a2a.db`, and channel accounts at `~/.halo/secrets/channels/channels.db`.
|
|
67
67
|
|
|
68
68
|
## 3. Run `halo setup`
|
|
69
69
|
|
|
@@ -220,6 +220,7 @@ This installs the `halo` binary on `$PATH`. Subcommands available:
|
|
|
220
220
|
| `halo cli "<prompt>"` | One-shot prompt → reply, exit |
|
|
221
221
|
| `halo agents` / `halo sessions` | List agents / sessions |
|
|
222
222
|
| `halo acp` | Stdio bridge that lets an ACP client (e.g. Zed / JetBrains) drive a halo server — see [acp-adapter.md](acp-adapter.md) |
|
|
223
|
+
| `halo agentcore --workspace <path>` | Foreground A2A server for an Amazon Bedrock AgentCore Runtime container — see [design/agentcore.md](../design/agentcore.md) |
|
|
223
224
|
|
|
224
225
|
### Upgrade flow
|
|
225
226
|
|
package/bundled-docs/dev/env.md
CHANGED
|
@@ -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/
|
|
70
|
+
- SQLite: `<workspace>/.halo/halo.db` (per-workspace, auto-created on first use) + global queues `~/.halo/global/evo.db`, `~/.halo/global/cron.db`, `~/.halo/global/runs.db` and `~/.halo/global/a2a.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/`
|
|
@@ -113,8 +113,11 @@ Source: `packages/server/src/config.ts`
|
|
|
113
113
|
| `HALO_LOG_LEVEL` | `warn` | `settings.yaml general.logging.level` | Log level: debug \| info \| warn \| error |
|
|
114
114
|
| `HALO_JWT_SECRET` | (none) | `config.yaml server.jwt_secret` | JWT signing secret (generated by `halo setup` if missing; stripped from child-process env) |
|
|
115
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). |
|
|
116
|
-
| `HALO_RUNTIME_MODE` | (none) | — | `agentcore` runs the server as an Amazon Bedrock AgentCore Runtime container (see [design/agentcore.md](../design/agentcore.md)) |
|
|
117
|
-
| `HALO_WORKSPACE` | `process.cwd()` | — |
|
|
116
|
+
| `HALO_RUNTIME_MODE` | (none) | — | `agentcore-a2a` runs the server as an Amazon Bedrock AgentCore Runtime container speaking A2A — set by `halo agentcore`, not by hand (see [design/agentcore.md](../design/agentcore.md)) |
|
|
117
|
+
| `HALO_WORKSPACE` | `process.cwd()` | — | The one workspace served in agentcore-a2a mode (`halo agentcore -w` overrides it; required there) |
|
|
118
|
+
| `HALO_A2A_ROOT` | the user's home | — | Base dir A2A exposes workspaces under: a workspace `<base>/A/B` with `.halo/agent-card.json` is served at `/a2a/A/B` (see [design/a2a.md](../design/a2a.md)); workspaces outside it can't be exposed |
|
|
119
|
+
| `HALO_A2A_PUBLIC_URL` | (none) | `settings.yaml general.a2a.public_url` | This server's A2A origin as peers see it (no trailing slash), used for the agent card's interface URL and as the default push-webhook base. In agentcore-a2a mode: the runtime's invoke URL (`…/runtimes/<escaped ARN>/invocations`) |
|
|
120
|
+
| `HALO_A2A_ACCESS` | `workspace` | — | agentcore-a2a mode only: access level of every A2A session — `workspace` \| `full` \| `readonly`; anything else is a boot error |
|
|
118
121
|
| `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
122
|
| `HALO_CLI` | `halo` (`halo.cmd` on Windows) | — | `halo` cli executable that cron runs and the evolution wrapper spawn — a dev override |
|
|
120
123
|
| `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 |
|
|
@@ -127,10 +130,11 @@ settings.yaml only (no env override):
|
|
|
127
130
|
- `general.compact.keep_messages` (default 5) — recent messages to keep uncompacted, restart required
|
|
128
131
|
- `general.compact.max_summary_input` (default 15000) — local compaction fallback input cap, restart required
|
|
129
132
|
- `general.compact.max_message_slice` (default 800) — local compaction per-message cap, restart required
|
|
133
|
+
- `general.a2a.url_allowlist` (default `100.64.0.0/10,*.ts.net`) — comma-separated CIDRs / host patterns A2A egress may reach besides public https (push webhooks, remote cards and RPC, inbound image URLs); private / loopback ranges only when listed, plain http only to listed hosts / ranges, link-local always refused; scope: global. See [design/a2a.md](../design/a2a.md#push-delivery-and-url-policy)
|
|
130
134
|
- `general.server.trust_proxy` (default `false`) — trust `x-forwarded-for` for client IP resolution, scope: global (enable only behind a reverse proxy you control)
|
|
131
135
|
- `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
|
|
132
136
|
- `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
|
|
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
|
|
137
|
+
- `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,a2a}.db` + their `-wal`/`-shm` files); Full sessions, the admin file explorer and the terminal are not affected, scope: global
|
|
134
138
|
- `general.logging.level` (default `warn`) — log level: debug | info | warn | error, restart required
|
|
135
139
|
- `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)
|
|
136
140
|
- `general.observability.service_name` (default `halo`) — OTel resource `service.name`, scope: global, restart required
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Agent Tool Reference
|
|
2
2
|
|
|
3
|
-
Agents have two tool categories: workspace tools (files, shell, search) and session tools (managing other sessions). Workspace tools are enabled by name in `agent.yaml`'s `tools` list; the session-tool bundle is granted automatically by a non-empty `team` (see [Session tools](#session-tools)).
|
|
3
|
+
Agents have two tool categories: workspace tools (files, shell, search) and session tools (managing other sessions). Workspace tools are enabled by name in `agent.yaml`'s `tools` list; the session-tool bundle is granted automatically by a non-empty `team` (see [Session tools](#session-tools)). Two opt-in sets reach beyond the workspace: [Relay tools](#relay-tools) (sessions in *other* workspaces on the same server) and [A2A tools](#a2a-tools) (agents on *other* servers).
|
|
4
4
|
|
|
5
5
|
## Workspace tools
|
|
6
6
|
|
|
@@ -180,7 +180,7 @@ Sensitive directories and files are hidden from workspace/readonly sessions via
|
|
|
180
180
|
| Setting | Built-in (always included) | Method |
|
|
181
181
|
|---|---|---|
|
|
182
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 |
|
|
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
|
+
| `hidden_files` | `~/.npmrc,~/.bash_history,~/.gitconfig,~/.git-credentials,~/.netrc,~/.halo/global/{evo,cron,runs,a2a}.db` + their `-wal`/`-shm` files | bwrap `--ro-bind ~/.halo/.sandbox-empty` (reads as empty); Seatbelt `literal` deny |
|
|
184
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 |
|
|
185
185
|
|
|
186
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.
|
|
@@ -443,6 +443,96 @@ List a workspace's **root** sessions — most recently active first, capped at 1
|
|
|
443
443
|
|
|
444
444
|
Returns `{ "code": 0, "workspace": "<realpath>", "sessions": [{ id, agentId, agentName, title, status, createdAt, updatedAt }], "count": N }`. `title` falls back to `description` like `session_list`; `status` follows the list semantics (`running` when the root itself or any live child is mid-turn, `stopped` when the row is stamped, else `idle`).
|
|
445
445
|
|
|
446
|
+
## A2A tools
|
|
447
|
+
|
|
448
|
+
File: `packages/server/src/a2a/outbound.ts` (`buildA2ATools`). Design notes in [design/a2a.md](../design/a2a.md).
|
|
449
|
+
|
|
450
|
+
Cross-**server** dispatch over A2A v1.0: the agent sends a message to a remote A2A agent (normally another halo server's exposed workspace) and the result is pushed back into this session when the remote is done, no polling. Remotes are configured once per server in `~/.halo/secrets/a2a-remotes.yaml` (no per-workspace file; `<ws>/.halo/a2a-remotes.yaml` is not read):
|
|
451
|
+
|
|
452
|
+
```yaml
|
|
453
|
+
remotes:
|
|
454
|
+
halo-test:
|
|
455
|
+
card: http://myhost.tailnet.ts.net:9527/a2a/halo-test/.well-known/agent-card.json
|
|
456
|
+
auth: bearer # bearer (default) | sigv4 (an AgentCore runtime)
|
|
457
|
+
# push_base: http://172.31.7.121:9527 # optional webhook base for this remote
|
|
458
|
+
```
|
|
459
|
+
|
|
460
|
+
A `bearer` remote's token is the settings secret `a2a.secrets.<remote>` (workspace `settings.yaml` overrides global; `<<ENV>>` works), resolved at call time and never shown to the model. When non-full sessions use the tools, put it in the global `~/.halo/secrets/settings.yaml` — a workspace's `.halo/settings.yaml` is readable to them, `~/.halo/secrets` is not. The list itself is full-only: `~/.halo/secrets` is hidden from workspace / readonly sessions, so they can use the remotes but can't repoint one's `card:` at a host of their choosing and collect its token. The remote's webhook goes to `push_base`, else `general.a2a.public_url`; with neither set `a2a_send` fails. Every remote URL passes the A2A URL policy (`general.a2a.url_allowlist`).
|
|
461
|
+
|
|
462
|
+
**Enabling**: opt-in by the single name `a2a_send` in `agent.yaml`'s `tools:` — it brings `a2a_stop` / `a2a_read` / `a2a_list`; the other three names are not recognised on their own. Available at **every access level** (full, workspace, readonly, any channel) **except the A2A read-only profile** — an inbound A2A session at readonly never gets them, whatever its yaml lists, so a remote's readonly token can't make this server call third parties. The admin tool picker shows one `a2a_send` chip naming the set.
|
|
463
|
+
|
|
464
|
+
**Server only**: in the CLI / TUI — and every cron run, which is a `halo cli` child — there is no push receiver; every A2A tool returns `{"code": 1, "error": "a2a is not available in the CLI / TUI — it only runs inside `halo server`, which receives the remote agent's push reports. This is permanent for this runtime, not a temporary outage: do not retry. …"}`. The tools are still listed there when the agent names `a2a_send`.
|
|
465
|
+
|
|
466
|
+
**Retries**: each request has a 30 s timeout. An AgentCore `-32054` "… please retry" answer is retried after 0.5 / 1 / 2 / 4 / 8 s, and a halo AgentCore container's lease-busy answer (`-32603` "workspace is in use by another runtime session …") after 5 / 10 / 15 / 20 s; then the last answer is returned as the error. Stopping the turn ends the wait at once — for the RPC and for the agent-card fetch before it.
|
|
467
|
+
|
|
468
|
+
Errors are `{ "code": 1, "error": "…" }`; a remote JSON-RPC error reads `<Method>: <message> (<code>)`.
|
|
469
|
+
|
|
470
|
+
### a2a_send
|
|
471
|
+
|
|
472
|
+
Send a message to a remote. Omit `context_id` to start a new conversation; pass the `context_id` from an earlier call for a follow-up — a follow-up to a busy remote is queued and softly interrupts its current step, `interrupt: true` aborts that step instead. Sent as SendMessage with `returnImmediately: true` plus a per-dispatch webhook (a follow-up on a context with a pending dispatch reuses its webhook, so the report comes once). Returns immediately.
|
|
473
|
+
|
|
474
|
+
| Arg | Type | Required | Description |
|
|
475
|
+
|---|---|---|---|
|
|
476
|
+
| `remote` | string | yes | Remote name from `~/.halo/secrets/a2a-remotes.yaml` (see `a2a_list`) |
|
|
477
|
+
| `message` | string | yes | The message to send |
|
|
478
|
+
| `context_id` | string | no | Conversation to continue (from an earlier `a2a_send`); omit to start one |
|
|
479
|
+
| `interrupt` | boolean | no | Follow-up only: abort the remote's current step instead of waiting for it |
|
|
480
|
+
| `files` | string[] | no | Absolute paths of images to attach |
|
|
481
|
+
|
|
482
|
+
**`files`**: png / jpeg / gif / webp only (by extension, and the bytes must agree), ≤5 MB each, ≤10 MB total, each sent as a `{ raw, mediaType, filename }` part after the text. A non-full session may only attach files under the workspace or the OS temp dir — the path and its realpath, so a symlink pointing outside is refused; full access has no path limit. Any violation fails the whole call (`files: <path>: <reason> — nothing was sent`).
|
|
483
|
+
|
|
484
|
+
Returns `{ "code": 0, "remote", "context_id", "task_id", "state": "working" | …, "follow_up": boolean }`.
|
|
485
|
+
|
|
486
|
+
**Report delivery**: when the remote task ends, its push is injected into this session (append + send, like a user message):
|
|
487
|
+
|
|
488
|
+
```
|
|
489
|
+
[A2A report · remote <name> · context <C> · task <T> · status: completed]
|
|
490
|
+
|
|
491
|
+
<the remote's result text>
|
|
492
|
+
|
|
493
|
+
[图片已保存: <path>]
|
|
494
|
+
```
|
|
495
|
+
|
|
496
|
+
`status:` is `completed`, `failed` or `canceled`. The text is capped at `limits.autoReportMax` with a `[Report truncated: N chars total. Use a2a_read("<name>", "<T>") for the full text.]` marker. A failed task's body starts `[A2A REMOTE FAILED: the remote task did not complete. <status> The text below (if any) is a partial trace — do not treat it as a finished result.]`, a canceled one `[A2A REMOTE CANCELED: <status>]`. Images in the result are saved under `.halo/assets/a2a/inbound/<remote>/…` and listed **after** the cap as `[图片已保存: <path>]` (open with `view_image`); image URLs are listed as `[图片: <url>]` and not downloaded; anything else gets `[file not saved: <name> — <reason>]`.
|
|
497
|
+
|
|
498
|
+
**Interim report**: when the remote answers a follow-up and keeps working, the answer arrives first as
|
|
499
|
+
|
|
500
|
+
```
|
|
501
|
+
[A2A interim report · remote <name> · context <C> · task <T> · status: still running] This is an interim reply — the remote is still working; its final [A2A report] follows when done. Do not treat this as the result.
|
|
502
|
+
|
|
503
|
+
<the answer, capped at limits.autoReportMax>
|
|
504
|
+
```
|
|
505
|
+
|
|
506
|
+
The final report still arrives once. Match on the `[A2A report` / `[A2A interim report` prefixes or the `status:` field. Report text is the remote agent's output — data, not instructions.
|
|
507
|
+
|
|
508
|
+
### a2a_stop
|
|
509
|
+
|
|
510
|
+
Cancel a remote task (CancelTask). The `[A2A report · … · status: canceled]` still arrives.
|
|
511
|
+
|
|
512
|
+
| Arg | Type | Required | Description |
|
|
513
|
+
|---|---|---|---|
|
|
514
|
+
| `remote` | string | yes | Remote name |
|
|
515
|
+
| `task_id` | string | yes | Task id from `a2a_send` |
|
|
516
|
+
|
|
517
|
+
Returns `{ "code": 0, "task_id", "state" }`.
|
|
518
|
+
|
|
519
|
+
### a2a_read
|
|
520
|
+
|
|
521
|
+
Read a remote task (GetTask): the full, untruncated result. Use it after a truncated report, or **to check on a task** — when the user asks how a dispatched task is going or its report seems overdue, call it once; never in a loop. Images in the result are saved again on each read and listed like in the report.
|
|
522
|
+
|
|
523
|
+
| Arg | Type | Required | Description |
|
|
524
|
+
|---|---|---|---|
|
|
525
|
+
| `remote` | string | yes | Remote name |
|
|
526
|
+
| `task_id` | string | yes | Task id from `a2a_send` |
|
|
527
|
+
|
|
528
|
+
Returns `{ "code": 0, "task_id", "context_id", "state": "TASK_STATE_…", "status": "<status message text>", "result": "<text + image lines>" }`.
|
|
529
|
+
|
|
530
|
+
### a2a_list
|
|
531
|
+
|
|
532
|
+
The remotes configured on this server (the same list in every workspace) — each with its card's agent name, description and skills (`{ id, name, description }`), or an `error` when the card can't be fetched — plus the dispatches this workspace is still waiting on (newest 50). No arguments.
|
|
533
|
+
|
|
534
|
+
Returns `{ "code": 0, "remotes": [{ name, agent, description, skills }], "pending": [{ remote, context_id, task_id, since }] }`.
|
|
535
|
+
|
|
446
536
|
## Tool assignment
|
|
447
537
|
|
|
448
538
|
Workspace tools are enabled strictly by name in `agent.yaml`'s `tools` list:
|
|
@@ -455,7 +545,7 @@ skills:
|
|
|
455
545
|
- code-review # auto-injects activate_skill
|
|
456
546
|
```
|
|
457
547
|
|
|
458
|
-
Tools not listed are not injected. Session/delegation tools do **not** go in `tools:` — they ride on a non-empty `team` (see [Session tools](#session-tools) above). `activate_skill` is auto-injected whenever the YAML lists `skills` (no need to put it in `tools`), and `continue_task` is auto-injected for **every** agent unconditionally (see [continue_task](#continue_task)). The relay
|
|
548
|
+
Tools not listed are not injected. Session/delegation tools do **not** go in `tools:` — they ride on a non-empty `team` (see [Session tools](#session-tools) above). `activate_skill` is auto-injected whenever the YAML lists `skills` (no need to put it in `tools`), and `continue_task` is auto-injected for **every** agent unconditionally (see [continue_task](#continue_task)). The relay and A2A sets are the two name-gated bundles: listing `relay_send` alone brings `relay_interrupt` / `relay_stop` / `relay_read` / `relay_list` with it, full-access sessions only (see [Relay tools](#relay-tools)); listing `a2a_send` alone brings `a2a_stop` / `a2a_read` / `a2a_list`, at every access level except the A2A read-only profile (see [A2A tools](#a2a-tools)).
|
|
459
549
|
|
|
460
550
|
There is **no implicit default tool set**: `filterTools()` (in `agent-loader.ts`) returns only the tools whose names appear in `agent.yaml`'s `tools:` list. If the field is absent or empty, the agent has zero workspace tools. The admin UI's "Create agent" form scaffolds a fresh agent with an empty `tools: []` for the same reason — fill it in deliberately. The `default` agent's bundled `agent.yaml` lists the common set (`file_read` / `file_write` / `file_edit` / `view_image` / `file_list` / `shell_exec` / `grep` / `glob` / `web_fetch`) that most agents will want; copy that line if you're starting from scratch.
|
|
461
551
|
|
|
@@ -2,8 +2,6 @@
|
|
|
2
2
|
|
|
3
3
|
Talk to a halo agent over plain HTTP from any client you control — a browser, a curl script, a custom frontend, your own mobile app. The Web channel is the "build your own UI" channel: it gives you a token, you give the token to whatever client you wrote.
|
|
4
4
|
|
|
5
|
-
> If you want a ready-made browser frontend without writing code, halo ships `packages/web-demo` — a tiny Express app that wraps the Web channel with its own password gate. See "Standalone web-demo frontend" at the bottom.
|
|
6
|
-
|
|
7
5
|
## What you'll end up with
|
|
8
6
|
|
|
9
7
|
- A 24-byte random token (base64url-encoded) bound to one workspace
|
|
@@ -92,8 +90,6 @@ For browser apps you almost never want these — leave them off and use the per-
|
|
|
92
90
|
|
|
93
91
|
Images go to the LLM as multimodal content. Any other `mimeType` (audio, PDF, …) in the same array is not sent to the model: the server saves it to `<workspace>/.halo/assets/web/inbound/<accountId>/<date>/` and appends a `[语音已保存: <path>]` line to your message so the agent can open it with its file tools.
|
|
94
92
|
|
|
95
|
-
The standalone web-demo handles voice + arbitrary files for you; if you want that, use it as a reference implementation.
|
|
96
|
-
|
|
97
93
|
## Slash commands
|
|
98
94
|
|
|
99
95
|
Slash commands are intercepted before they reach the agent — same set as every other channel:
|
|
@@ -120,28 +116,6 @@ Send a slash command exactly like a normal message — the server detects the le
|
|
|
120
116
|
| Token leaked accidentally | Delete the account in admin, create a new one. The old token is invalidated immediately |
|
|
121
117
|
| Want to share one token across multiple users | Don't — every request would land on the same active session. Create one account per user / app |
|
|
122
118
|
|
|
123
|
-
## Standalone web-demo frontend
|
|
124
|
-
|
|
125
|
-
If you want a browser UI with login, image upload, voice recording, and multi-language support without writing it yourself:
|
|
126
|
-
|
|
127
|
-
```bash
|
|
128
|
-
cd packages/web-demo
|
|
129
|
-
node server.js
|
|
130
|
-
```
|
|
131
|
-
|
|
132
|
-
Environment variables:
|
|
133
|
-
|
|
134
|
-
| Variable | Required | Description |
|
|
135
|
-
|---|---|---|
|
|
136
|
-
| `HALO_API` | yes | Halo server URL, e.g. `http://localhost:9527` |
|
|
137
|
-
| `HALO_TOKEN` | yes | The web-channel token from Step 1 |
|
|
138
|
-
| `HALO_WEB_DEMO_PASSWORD` | no | Password gate; empty means open access |
|
|
139
|
-
| `PORT` | no | Listen port; default `9528` |
|
|
140
|
-
|
|
141
|
-
The demo holds your token server-side and exposes its own password-based session — the browser never sees the halo token. Open `http://localhost:9528` to use it.
|
|
142
|
-
|
|
143
|
-
Alternatively, the gear icon (header / login screen) opens **direct-connect mode**: enter any halo server's URL + a web-channel token and the browser talks to that server's `/api/web/*` directly, skipping the proxy and its password. Handy for pointing one deployed web-demo at several halo servers. The token is kept in that browser's localStorage — only use it on devices you trust; clear it in the same panel to return to proxy mode.
|
|
144
|
-
|
|
145
119
|
## Security notes
|
|
146
120
|
|
|
147
121
|
- **Tokens are unhashed** in `~/.halo/secrets/channels/channels.db`. Restrict that file to the user that runs halo
|
|
@@ -210,6 +210,16 @@ Attach files or images to your message with `@file` and `@image`, or pull a dire
|
|
|
210
210
|
- **Images**: skipped if larger than 5MB
|
|
211
211
|
- If the current model does not support vision, a warning is shown and images are ignored
|
|
212
212
|
|
|
213
|
+
## OpenTelemetry export
|
|
214
|
+
|
|
215
|
+
`halo cli` and `halo tui` export traces, metrics and logs the same way the server does: when the global `general.observability.endpoint` setting is non-empty, each run registers the OTLP exporters at start and the agent loop emits the same GenAI spans (`invoke_agent` / `chat` / `execute_tool`). With no endpoint nothing is loaded and startup is unaffected. The scheduled runs of [Cron Tasks](../design/cron.md) and the evolution pipeline are spawned `halo cli` children, so they show up in the collector too (`session.id` `cron-<jobId>` or the internal agent's name).
|
|
216
|
+
|
|
217
|
+
- **Telling processes apart**: they all share `service.name`; the `halo.process` resource attribute is `server`, `cli` or `tui`, and cron / evolution runs report `cli`.
|
|
218
|
+
- **Flushed on exit**: the batched telemetry is sent before the process ends — on normal completion, TUI quit, an error, and on SIGINT / SIGTERM (after the graceful stop, still exiting 130 / 143). The flush is capped at 3 s, so an unreachable collector delays the exit by at most that and never hangs it.
|
|
219
|
+
- `halo agents` and `halo sessions` only list and don't export.
|
|
220
|
+
|
|
221
|
+
Settings and the span / metric model: [design/observability.md](../design/observability.md).
|
|
222
|
+
|
|
213
223
|
## Model provider configs (`halo models`)
|
|
214
224
|
|
|
215
225
|
```bash
|
|
@@ -231,6 +241,31 @@ recognized as intentional and never respawns; `halo server stop|restart|status`
|
|
|
231
241
|
still target the server's own pid. Full policy in
|
|
232
242
|
[dev/deploy.md](../dev/deploy.md#crash-semantics--why-restarton-failure-is-load-bearing).
|
|
233
243
|
|
|
244
|
+
## AgentCore runtime (`halo agentcore`)
|
|
245
|
+
|
|
246
|
+
```bash
|
|
247
|
+
halo agentcore --workspace /mnt/efs/halo [-p 9000]
|
|
248
|
+
```
|
|
249
|
+
|
|
250
|
+
Runs the server in the foreground as an Amazon Bedrock AgentCore Runtime
|
|
251
|
+
container speaking the A2A protocol (`HALO_RUNTIME_MODE=agentcore-a2a`): `GET
|
|
252
|
+
/ping`, `POST /` (A2A JSON-RPC) and `GET /.well-known/agent-card.json` on one
|
|
253
|
+
fixed workspace. No admin UI, password, channels, cron or evolution — AgentCore
|
|
254
|
+
verifies the caller's signed request before it reaches the container. It always
|
|
255
|
+
runs in the foreground (no `-d`) — meant as the container's entry command.
|
|
256
|
+
|
|
257
|
+
| Flag / env | Meaning |
|
|
258
|
+
|---|---|
|
|
259
|
+
| `-w, --workspace <path>` / `HALO_WORKSPACE` | Workspace to serve — required, must exist. Seeded with `.halo/agent-card.json` if missing. |
|
|
260
|
+
| `-p, --port <n>` | Listen port, default `9000` (the AgentCore contract) |
|
|
261
|
+
| `HALO_A2A_PUBLIC_URL` | Interface URL advertised in the card — the runtime's invoke URL (`…/runtimes/<escaped ARN>/invocations`) |
|
|
262
|
+
| `HALO_A2A_ACCESS` | Session access level: `workspace` (default) \| `full` \| `readonly` |
|
|
263
|
+
|
|
264
|
+
One runtime session id owns the workspace at a time (`.halo/agentcore.lease`);
|
|
265
|
+
requests on another id are refused until the holder has been gone ~45 s. Design,
|
|
266
|
+
the caller-side `auth: sigv4` remote and ops notes:
|
|
267
|
+
[design/agentcore.md](../design/agentcore.md).
|
|
268
|
+
|
|
234
269
|
## Architecture
|
|
235
270
|
|
|
236
271
|
```
|
|
@@ -260,7 +295,8 @@ Session prefix: `cli_`. Sessions are persisted to `<workspace>/.halo/sessions/`
|
|
|
260
295
|
|
|
261
296
|
`tui` / `cli` / `agents` / `sessions` refuse to run ("~/.halo/global/ not initialized. Run halo setup first.") until `halo setup` has seeded `~/.halo/global/`. Then the CLI replicates the server's init sequence (`initRuntime()` in `harness.ts`):
|
|
262
297
|
1. `refreshTemplatesIfOutdated()` — the server's template startup check: when `~/.halo/global/.template-version` is behind the bundled `TEMPLATE_VERSION` (typically right after `halo upgrade`), re-seed `~/.halo/global/` before anything reads the models registry; logs `[CLI] Templates outdated (vX → vY), refreshing ~/.halo/global/` to stderr. Runs once per upgrade — afterwards the stamp matches and it's a single file read
|
|
263
|
-
2. `
|
|
264
|
-
3. `
|
|
265
|
-
4. `
|
|
266
|
-
5. `
|
|
298
|
+
2. `initObservability()` — `tui` / `cli` only (the list-only `agents` / `sessions` skip it): when `general.observability.endpoint` is set, register OTel providers so this run's traces, metrics and logs reach the collector, tagged `halo.process=cli` or `tui`; a no-op otherwise. Cron jobs and evolution runs are spawned `halo cli` children, so they export too. See [OpenTelemetry export](#opentelemetry-export)
|
|
299
|
+
3. `initLogger()` — redirect console to stderr + file logger
|
|
300
|
+
4. `initBwrapCheck()` — probe sandbox availability
|
|
301
|
+
5. `setSandboxHiddenPaths()` — configure sandbox paths
|
|
302
|
+
6. `new SessionManager(workspace)` — create agent session manager (the workspace's `.halo/` is seeded via `ensureWorkspaceHalo`)
|
|
@@ -103,7 +103,7 @@ Extra rules:
|
|
|
103
103
|
- A readonly `shell_exec` gets no secret substitution — `{{…params…}}` / `<<ENV>>` placeholders stay literal.
|
|
104
104
|
- `view_image` also needs a vision-capable model.
|
|
105
105
|
- A skill with `requiresAccess` above the session level is hidden (no metadata, no `activate_skill`).
|
|
106
|
-
- Relay tools are only built for `full` sessions.
|
|
106
|
+
- Relay tools are only built for `full` sessions. A2A tools (`a2a_send` in `tools:`) are built at every level except the A2A read-only profile; a non-full session's `files` must sit in the workspace or the temp dir ([dev/tools.md → A2A tools](../dev/tools.md#a2a-tools)).
|
|
107
107
|
- Hidden from `workspace` / `readonly` sessions only (Full sessions, the admin file explorer and the terminal see everything): a built-in list that is always included (`~/.halo/secrets`, `~/.aws`, `~/.ssh`, `~/.gnupg`, `~/.docker`, `~/.config/gh`, `~/.gitconfig`, `~/.git-credentials`, `~/.npmrc`, `~/.netrc`, the global evo/cron/runs databases, internal-session transcripts, logs, …), any extra paths added in `general.sandbox.hidden_dirs` / `hidden_files` (they add to the built-in list, never replace it), plus the workspace's own `.halo/sessions`, `.halo/logs`, `.halo/evo`, `.halo/halo.db*` (fixed in code). The rest of `.halo/` stays readable.
|
|
108
108
|
|
|
109
109
|
### Where a session's level comes from
|
|
@@ -117,7 +117,8 @@ Extra rules:
|
|
|
117
117
|
| Cron job | Full (the `halo cli` default) | Keeps its stored level (passed through as `--access`) |
|
|
118
118
|
| `halo cli` / TUI | `--access`, default `full` | `--access` value (a different level is persisted) |
|
|
119
119
|
| `relay_send` into another workspace | Full | Keeps its stored level |
|
|
120
|
-
|
|
|
120
|
+
| A2A inbound (`/a2a/<path>`, a web-channel token bound to that workspace) | The token's `accessLevel`, mapped like a channel account. A `readonly` / `observer` token gets the fixed A2A read-only profile: `file_read` / `view_image` / `file_list` / `grep` / `glob` + `continue_task`, even with an OS sandbox — no shell, write or fetch, no delegation, no relay / a2a tools | Re-applied on every inbound message |
|
|
121
|
+
| AgentCore runtime (`halo agentcore`) | `HALO_A2A_ACCESS`: `workspace` (default) \| `full` \| `readonly` — every A2A session gets it | Re-applied on every inbound message |
|
|
121
122
|
|
|
122
123
|
Consequences:
|
|
123
124
|
- A sub-agent can never have more access than its parent had when it was spawned. A readonly channel user can't escalate by delegating.
|