@turmind/halo 1.3.4 → 1.4.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/admin-out/404.html +1 -1
- package/admin-out/_next/static/chunks/989-6a0ec35b8ae2cb39.js +1 -0
- package/admin-out/_next/static/chunks/app/page-bf9c0b21ae431c92.js +1 -0
- package/admin-out/_next/static/css/c0ea31cd787f7aa7.css +3 -0
- package/admin-out/index.html +1 -1
- package/admin-out/index.txt +5 -5
- package/bundled-docs/design/agent.md +12 -6
- package/bundled-docs/design/cron.md +19 -11
- package/bundled-docs/design/express-self.md +1 -1
- package/bundled-docs/design/storage.md +7 -3
- package/bundled-docs/design/wechat.md +1 -1
- package/bundled-docs/design/ws.md +2 -1
- package/bundled-docs/dev/add-model-provider.md +1 -1
- package/bundled-docs/dev/api.md +6 -6
- package/bundled-docs/dev/deploy.md +1 -1
- package/bundled-docs/dev/desktop-packaging.md +1 -1
- package/bundled-docs/dev/env.md +4 -4
- package/bundled-docs/dev/tools.md +39 -17
- package/bundled-docs/guide/chat.md +10 -0
- package/bundled-docs/guide/cli.md +8 -3
- package/bundled-docs/requirements/chat.md +8 -0
- package/dist/index.js +872 -442
- package/package.json +1 -1
- package/templates/agents/__apply_agent__/agent.yaml +1 -1
- package/templates/agents/__evo_agent__/agent.yaml +1 -1
- package/templates/agents/__score__/agent.yaml +1 -1
- package/templates/agents/deep-executor/agent.yaml +1 -1
- package/templates/agents/default/agent.yaml +1 -1
- package/templates/agents/goal/agent.yaml +1 -1
- package/templates/models/mimo-token-plan-china.yaml +13 -6
- package/templates/skills/agent/SKILL.md +2 -2
- package/templates/skills/cron/SKILL.md +15 -0
- package/templates/skills/cron/manage-cron.py +28 -3
- package/templates/skills/send-file/SKILL.md +1 -0
- package/admin-out/_next/static/chunks/989-e660d0dd799950f2.js +0 -1
- package/admin-out/_next/static/chunks/app/page-18ab83dbdcc3897c.js +0 -1
- package/admin-out/_next/static/css/59402ac06b2b1a10.css +0 -3
- /package/admin-out/_next/static/{D2iJsfaULMDk0HYCHji6W → 9_eFiojb_-MS8CJ2oJg8_}/_buildManifest.js +0 -0
- /package/admin-out/_next/static/{D2iJsfaULMDk0HYCHji6W → 9_eFiojb_-MS8CJ2oJg8_}/_ssgManifest.js +0 -0
package/bundled-docs/dev/api.md
CHANGED
|
@@ -12,7 +12,7 @@ Compression: every response negotiates gzip/deflate via `hono/compress` (`thresh
|
|
|
12
12
|
|
|
13
13
|
| Method | Path | Purpose |
|
|
14
14
|
|---|---|---|
|
|
15
|
-
| GET | `/api/health` | Health check — returns `{status, timestamp, uptime, engine, version, gitSha}` (`gitSha`: short sha on source builds, `-dirty` suffix if tree modified, `null` on published bundles) |
|
|
15
|
+
| GET | `/api/health` | Health check — returns `{status, timestamp, uptime, engine, version, gitSha, sandbox}` (`gitSha`: short sha on source builds, `-dirty` suffix if tree modified, `null` on published bundles; `sandbox`: OS sandbox backing non-full access levels — `'bwrap'` / `'seatbelt'` / `null`; the admin locks its access-level selector to Full on `null`) |
|
|
16
16
|
|
|
17
17
|
## Authentication
|
|
18
18
|
|
|
@@ -86,7 +86,7 @@ Unified session log API — list + read session files across all agents.
|
|
|
86
86
|
|
|
87
87
|
| Method | Path | Purpose |
|
|
88
88
|
|---|---|---|
|
|
89
|
-
| GET | `/api/sessions/logs?projectId=` | List session metadata, keyset-paginated. Default returns each top-level row + all descendants (sidebar tree); `rootOnly=1` returns roots only (chat-header dropdown) |
|
|
89
|
+
| GET | `/api/sessions/logs?projectId=` | List session metadata, keyset-paginated. Default returns each top-level row + all descendants (sidebar tree); `rootOnly=1` returns roots only (chat-header dropdown). A `projectId` with no `.halo/` (plain or missing directory) returns `{sessions: [], nextCursor: null}` without scaffolding it — a list never turns a directory into a workspace |
|
|
90
90
|
| GET | `/api/sessions/logs/:id?projectId=` | Full session log (scans across agent dirs) — the **active** file only; archived history is a separate call |
|
|
91
91
|
| GET | `/api/sessions/logs/:id/archive/:n?projectId=` | One archived UI-log segment — see [detail](#get-apisessionslogsidarchivenprojectidabs) |
|
|
92
92
|
| DELETE | `/api/sessions/logs/:id?projectId=` | Delete the session log (and all of its archive segments) |
|
|
@@ -184,9 +184,9 @@ File: `packages/server/src/routes/cron.ts`. Admin cookie auth. REST CRUD over `c
|
|
|
184
184
|
|
|
185
185
|
| Method | Path | Purpose |
|
|
186
186
|
|---|---|---|
|
|
187
|
-
| GET | `/api/cron/jobs?limit=&before=` | Cursor-paginated newest-first by `createdAt`. Default `limit` 20, max 100; `before` is a `createdAt` cursor. Returns `{jobs, hasMore, nextCursor}`. Each job has `targets` decoded
|
|
188
|
-
| POST | `/api/cron/jobs` | Create a job. Body: `{label?, workspacePath, agentId, userPrompt, schedule, runAt?, timezone?, timeoutSec?, targets?, enabled?}`. Exactly one of `schedule` (5-field cron) or `runAt` (epoch ms, future) is required; mutually exclusive. `timeoutSec` must be an integer 60–21600 (else 400); unset = runner default 3600. Returns `{ok, id}` |
|
|
189
|
-
| PUT | `/api/cron/jobs/:id` | Partial update over the same fields. Re-validates `schedule` / `runAt` and enforces trigger-mode exclusivity on the **merged** row: both in one body → 400 `schedule and runAt are mutually exclusive`; supplying one side is a mode switch that implicitly clears the other; a body that clears the active side without supplying a replacement → 400 `schedule or runAt required`. `runAt` must be a finite future epoch-ms number or `null`. `timeoutSec` follows the same partial-body contract: omitted = unchanged, `null` = clear back to the default, a number is range-validated (60–21600) same as create. Flips schedule/unschedule on `enabled`. Returns `{ok}` |
|
|
187
|
+
| GET | `/api/cron/jobs?limit=&before=` | Cursor-paginated newest-first by `createdAt`. Default `limit` 20, max 100; `before` is a `createdAt` cursor. Returns `{jobs, hasMore, nextCursor}`. Each job has `targets` decoded, a live `nextRunAt` (computed from schedule/runAt), and `sessionId` (`null` = runs in `cron-<jobId>`) |
|
|
188
|
+
| POST | `/api/cron/jobs` | Create a job. Body: `{label?, workspacePath, agentId, userPrompt, schedule, runAt?, timezone?, timeoutSec?, sessionId?, targets?, enabled?}`. Exactly one of `schedule` (5-field cron) or `runAt` (epoch ms, future) is required; mutually exclusive. `timeoutSec` must be an integer 60–21600 (else 400); unset = runner default 3600. `sessionId` picks an existing (or to-be-created) **root** session of `workspacePath` for the cli to run in — `[A-Za-z0-9_:-]{1,200}`, no `>` sub-session path (else 400); unset / `null` / `''` = the job's own `cron-<jobId>` session. A picked session that already exists keeps its own agent and access level (`agentId` only applies when the cli creates it); see [design/cron.md](../design/cron.md#execution-runjob) for the shared-session guards. Returns `{ok, id}` |
|
|
189
|
+
| PUT | `/api/cron/jobs/:id` | Partial update over the same fields. Re-validates `schedule` / `runAt` and enforces trigger-mode exclusivity on the **merged** row: both in one body → 400 `schedule and runAt are mutually exclusive`; supplying one side is a mode switch that implicitly clears the other; a body that clears the active side without supplying a replacement → 400 `schedule or runAt required`. `runAt` must be a finite future epoch-ms number or `null`. `timeoutSec` follows the same partial-body contract: omitted = unchanged, `null` = clear back to the default, a number is range-validated (60–21600) same as create. `sessionId` likewise: omitted = unchanged, `null` / `''` = back to `cron-<jobId>`, a string is validated same as create. Flips schedule/unschedule on `enabled`. Returns `{ok}` |
|
|
190
190
|
| DELETE | `/api/cron/jobs/:id` | Unschedule, delete every `cron_runs` row for the job, then delete the job. Returns `{ok}` |
|
|
191
191
|
| POST | `/api/cron/jobs/:id/run-now` | Fire the job immediately (fire-and-forget). Returns `{ok}` |
|
|
192
192
|
| POST | `/api/cron/reload` | Re-read every job from db and rebuild schedules. Returns `{ok}` |
|
|
@@ -317,7 +317,7 @@ File: `packages/server/src/routes/agent-configs.ts`
|
|
|
317
317
|
|
|
318
318
|
| Method | Path | Purpose |
|
|
319
319
|
|---|---|---|
|
|
320
|
-
| GET | `/api/agent-configs?projectId=` | List agents (global + workspace merged) |
|
|
320
|
+
| GET | `/api/agent-configs?projectId=` | List agents (global + workspace merged). A `projectId` with no `.halo/` yields the global agents only and is not scaffolded |
|
|
321
321
|
| GET | `/api/agent-configs/tools` | Available workspace tools |
|
|
322
322
|
| GET | `/api/agent-configs/models` | Available model providers and models |
|
|
323
323
|
| POST | `/api/agent-configs` | Create a new agent |
|
|
@@ -202,7 +202,7 @@ This installs the `halo` binary on `$PATH`. Subcommands available:
|
|
|
202
202
|
2. **Update `CHANGELOG.md`**: rename `[Unreleased]` → `[x.y.z] - YYYY-MM-DD`, add a fresh empty `[Unreleased]` section above it, and roll the link references at the bottom (add `[x.y.z]: compare/v<prev>...vx.y.z`, repoint `[Unreleased]` to `compare/vx.y.z...HEAD`).
|
|
203
203
|
3. **Bump `TEMPLATE_VERSION`** in `packages/server/src/init.ts` if any file under `templates/` was touched. `build-bundle.mjs` enforces this: it diffs `packages/server/templates` against the previous release tag and exits 1 when files changed but the number didn't move (so `pnpm bundle` and every desktop `dist:*` refuse to package a silent template edit).
|
|
204
204
|
4. **Build admin**: `pnpm --filter @turmind/halo-admin build` — verify `admin/out/monaco/vs/loader.js` exists.
|
|
205
|
-
5. Commit, tag `vx.y.z`, push, then `npm publish
|
|
205
|
+
5. Commit, tag `vx.y.z`, push, then `HALO_RELEASE=1 pnpm --filter @turmind/halo-cli bundle` and `npm publish` from `packages/cli/dist-pub/` — **exactly once**. Confirm with `npm view @turmind/halo version` (the registry replicates with a ~2–3 min lag, so wait and re-`view`; never "retry" the publish to see what happens). `dist-pub/` is only written in release mode; desktop `dist:*` builds stage into `dist-dev/` instead, so a stale or sha-suffixed bundle can't end up there — but `npm publish` is still not idempotent (v1.3.4 shipped a `1.3.4-<sha>` prerelease to npm from a second publish and it became `latest` for ~90s).
|
|
206
206
|
6. **GitHub release**: `gh release create vx.y.z --notes-from-tag` (or paste the CHANGELOG section).
|
|
207
207
|
7. **Windows exe** (the step both v1.3.0 and v1.3.1 skipped): `HALO_STAGE_FULL=1 CI=true pnpm dist:win`, then `gh release upload vx.y.z "packages/desktop/dist/Halo Setup x.y.z.exe"`. The macOS dmg is built on a Mac and uploaded the same way.
|
|
208
208
|
8. **Post-check** (whoever ran the release does NOT get to skip this): `gh release view vx.y.z --json assets` must list the exe, and `npm view @turmind/halo version` must print `x.y.z`. A release without the exe attached is not done.
|
|
@@ -121,7 +121,7 @@ dependency work. Three levers, in order of how often they help:
|
|
|
121
121
|
|
|
122
122
|
1. **Auto-fast (automatic).** A full `pnpm dist:arm64` now fingerprints the
|
|
123
123
|
dependency inputs (`packages/server/package.json`, `core/package.json`, the
|
|
124
|
-
cli bundle's `dist-
|
|
124
|
+
cli bundle's `dist-dev/package.json`, `pnpm-lock.yaml`, + target/node
|
|
125
125
|
version) and records it in `resources/.stage-meta`. On the next run, if a
|
|
126
126
|
prior full stage exists for the same target **and the fingerprint is
|
|
127
127
|
unchanged**, staging auto-takes the fast path: it re-syncs only the compiled
|
package/bundled-docs/dev/env.md
CHANGED
|
@@ -124,9 +124,9 @@ settings.yaml only (no env override):
|
|
|
124
124
|
- `general.compact.max_message_slice` (default 800) — local compaction per-message cap
|
|
125
125
|
- `general.compact.summarize_timeout_sec` (default 300) — self-compact timeout
|
|
126
126
|
- `general.server.trust_proxy` (default `false`) — trust `x-forwarded-for` for client IP resolution, scope: global (enable only behind a reverse proxy you control)
|
|
127
|
-
- `general.sandbox.hidden_dirs` (default `~/.halo/secrets,~/.aws,~/.ssh,~/.gnupg,~/.docker,~/.config/gh,~/.halo/global/internal-sessions,~/.halo/global/logs`) — bwrap tmpfs overlays, scope: global
|
|
128
|
-
- `general.sandbox.writable_dirs` (default empty) — dirs
|
|
129
|
-
- `general.sandbox.hidden_files` (default `~/.npmrc,~/.bash_history,~/.gitconfig,~/.git-credentials,~/.netrc,~/.halo/global/{evo,cron,runs}.db` + their `-wal`/`-shm` files) —
|
|
127
|
+
- `general.sandbox.hidden_dirs` (default `~/.halo/secrets,~/.aws,~/.ssh,~/.gnupg,~/.docker,~/.config/gh,~/.halo/global/internal-sessions,~/.halo/global/logs`) — hidden from workspace/readonly sessions (bwrap tmpfs overlays on Linux, Seatbelt deny on macOS), scope: global
|
|
128
|
+
- `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
|
|
129
|
+
- `general.sandbox.hidden_files` (default `~/.npmrc,~/.bash_history,~/.gitconfig,~/.git-credentials,~/.netrc,~/.halo/global/{evo,cron,runs}.db` + their `-wal`/`-shm` files) — hidden from workspace/readonly sessions, read as empty files (bwrap bind of `~/.halo/.sandbox-empty` on Linux, Seatbelt deny on macOS), scope: global
|
|
130
130
|
- `general.logging.level` (default `warn`) — log level: debug | info | warn | error
|
|
131
131
|
- `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)
|
|
132
132
|
- `general.observability.service_name` (default `halo`) — OTel resource `service.name`, scope: global, restart required
|
|
@@ -180,4 +180,4 @@ curl -s -o /dev/null -w "%{http_code}" http://localhost:9527
|
|
|
180
180
|
- React functional components; styling via Tailwind only; UI prefers shadcn/ui
|
|
181
181
|
- Log format `[Module] message`
|
|
182
182
|
- Bedrock model ID: `global.anthropic.claude-sonnet-4-6`, default region `us-east-1` (configured per agent via `agent.yaml model.endpoint`)
|
|
183
|
-
- File operations are sandboxed by bwrap
|
|
183
|
+
- File operations are sandboxed for non-full sessions by the OS sandbox (bwrap on Linux; Seatbelt for `shell_exec` on macOS) + `assertPathAllowed` (in-process file tools); `shell_exec` also runs an rm guard at every level — see [tools.md](tools.md#access-level-per-session-dynamic)
|
|
@@ -112,24 +112,46 @@ Timeout 10 s. Max body 50 KB (truncated if larger). Returns status + content-typ
|
|
|
112
112
|
|
|
113
113
|
### Access level (per-session, dynamic)
|
|
114
114
|
|
|
115
|
-
Each session carries `accessLevel: 'readonly' | 'workspace' | null` (persisted in `agent_sessions.access_level`). Sub-sessions inherit their parent's access level. Access level is re-evaluated on every user message — if the channel account's access level has changed, the agent instance is rebuilt with the new tool set and sandbox config.
|
|
115
|
+
Each session carries `accessLevel: 'readonly' | 'workspace' | null` (persisted in `agent_sessions.access_level`). Sub-sessions inherit their parent's access level. Access level is re-evaluated on every user message — if the channel account's access level has changed, the agent instance is rebuilt with the new tool set and sandbox config. In the admin, the level comes from the chat-input selector: the `chat` WS frame carries `accessLevel`, and `handleChat` applies it on the idle path (`'full'`, or any level on a host without an OS sandbox → `null`; a message queued behind a running turn keeps the level that turn was built with).
|
|
116
116
|
|
|
117
|
-
| Level | DB value | Tools (with
|
|
117
|
+
| Level | DB value | Tools (with OS sandbox) | Tools (without) | Sandbox |
|
|
118
118
|
|---|---|---|---|---|
|
|
119
|
-
| `null` (full) | `full` | All 9 tools | All 9 tools | None |
|
|
120
|
-
| `workspace` | `workspace` | All 9 tools | All 9 tools |
|
|
121
|
-
| `readonly` | `readonly` | All 9 tools | file_read, view_image, file_list, grep, glob (5 tools) |
|
|
119
|
+
| `null` (full) | `full` | All 9 tools | All 9 tools | None (rm guard still applies) |
|
|
120
|
+
| `workspace` | `workspace` | All 9 tools | All 9 tools | workspace rw, sensitive paths + workspace runtime state hidden |
|
|
121
|
+
| `readonly` | `readonly` | All 9 tools | file_read, view_image, file_list, grep, glob (5 tools) | workspace ro, sensitive paths + workspace runtime state hidden |
|
|
122
122
|
|
|
123
123
|
`view_image` is additionally gated on `capabilities.image` (see its section above). Models without vision support get the same lists minus `view_image` — so a non-vision model on `readonly` ends up with 4 tools, not 5.
|
|
124
124
|
|
|
125
|
+
OS sandbox backend per platform — `getSandboxBackend()` in `sandbox.ts` returns `'bwrap' | 'seatbelt' | null` after the boot probe (`initBwrapCheck()`), logged at startup as `[Server] OS sandbox: …` and exposed as `sandbox` on `GET /api/health`:
|
|
126
|
+
|
|
127
|
+
| Platform | `shell_exec` | File tools | Dependency |
|
|
128
|
+
|---|---|---|---|
|
|
129
|
+
| Linux | bwrap | bwrap | `bubblewrap` (`apt install bubblewrap`); the probe runs a real sandboxed no-op, so an install that can't create namespaces (Ubuntu 24.04 AppArmor userns restriction) counts as unavailable |
|
|
130
|
+
| macOS | Seatbelt (`/usr/bin/sandbox-exec -p <profile>`) | in-process, behind `assertPathAllowed` | none — ships with macOS (Apple marks it deprecated, but it's present on every release). The probe runs `sandbox-exec -p '(version 1)(allow default)' /usr/bin/true` |
|
|
131
|
+
| Windows | none | none | — see below |
|
|
132
|
+
|
|
125
133
|
Enforcement layers:
|
|
126
|
-
1. **OS sandbox
|
|
127
|
-
|
|
128
|
-
|
|
134
|
+
1. **OS sandbox**
|
|
135
|
+
- **bwrap (Linux)**: `--ro-bind / /` mounts the entire filesystem read-only, then configurable overlays hide sensitive paths (`--tmpfs` for directories; files are covered by `--ro-bind ~/.halo/.sandbox-empty <file>`, a zero-byte file created on demand — **not** `/dev/null`, whose bind reads as EACCES inside bwrap and makes git abort on `~/.gitconfig`). Workspace level adds `--bind` (rw) for the workspace directory. `--tmpfs /tmp` provides isolated writable temp per invocation. `--clearenv` + a minimal `PATH` / `HOME` / `TERM`.
|
|
136
|
+
- **Seatbelt (macOS)**: `buildSeatbeltProfile()` emits `(allow default)` → `(deny file-write*)` → `(allow file-write*)` for the workspace + `writable_dirs` + `/private/tmp`, `/private/var/folders`, `/dev` (readonly: temp dirs and `/dev` only) → `(deny file-read* file-write*)` on every hidden path (global lists + workspace-relative set; `subpath` for dirs, `literal` for files). Later rules win in SBPL, so the final hidden deny overrides the write allow. Paths are realpath'd (Seatbelt matches `/private/tmp`, not `/tmp`). The child gets a minimal env with `GIT_CONFIG_GLOBAL=/dev/null` and `NPM_CONFIG_USERCONFIG=/dev/null` so git / npm don't trip over the denied `~/.gitconfig` / `~/.npmrc`. Unlike bwrap there's no private `/tmp` — writes there persist.
|
|
137
|
+
- Both backends pass the host git identity (`git config --global user.name/user.email`, read once at boot) as `GIT_AUTHOR_*` / `GIT_COMMITTER_*`, so `git commit` works with `~/.gitconfig` hidden. Nothing else from the host git config crosses over.
|
|
138
|
+
- Tool execution uses `execFileAsync` / `spawn` (not shell) to prevent escape. Error messages are sanitized to strip sandbox internals — the agent never sees bwrap flags or mount details.
|
|
139
|
+
- When a non-full `shell_exec` fails with a write-denial (`Read-only file system` / `Operation not permitted` / `Permission denied` / `EROFS` / `EPERM`), the tool result gets a `[Sandbox] This session runs at "<level>" access: …` hint telling the agent to ask the user to switch to Full in the chat input box, rather than retrying variations.
|
|
140
|
+
2. **Tool filtering (no OS sandbox only)**: when `getSandboxBackend()` is `null`, `createWorkspaceTools()` returns a reduced 5-tool set for readonly (no file_write, file_edit, shell_exec, web_fetch). Workspace retains all tools.
|
|
141
|
+
3. **In-process path validation**: `assertPathAllowed()` gates every file-tool path whenever the file tools don't run inside bwrap (macOS always; Linux without bwrap). Same rules as the OS sandbox: **read** anywhere except the hidden sets (global + workspace-relative), **write** only to the workspace (minus the hidden set) and `writable_dirs`, readonly never writes. Paths are realpath'd first, so a symlink is judged by its target. Without any OS sandbox, `shell_exec` is blocked entirely for non-full sessions.
|
|
142
|
+
|
|
143
|
+
On Linux without bwrap only layers 2 + 3 are active.
|
|
144
|
+
|
|
145
|
+
#### rm guard
|
|
146
|
+
|
|
147
|
+
`assertRmSafe(command, workspaceRoot)` runs before every `shell_exec` at **every** access level (full included) on every platform except Windows — the OS sandbox limits where writes land, but not a mistyped `rm -r` inside the writable area. It refuses an `rm` / `rmdir` whose target resolves to:
|
|
148
|
+
- `/`, `$HOME`, `~/.halo`, the workspace root, or any parent of those
|
|
149
|
+
- a system directory or its direct child (`/etc`, `/usr`, `/var`, `/home`, `/opt`, … plus macOS `/Applications`, `/Library`, `/System`, `/Users`, `/Volumes`, `/private`)
|
|
150
|
+
- `/tmp`, `/private/tmp`, `/root` themselves (their children are ordinary deletes — `/root` is `$HOME` in most containers)
|
|
129
151
|
|
|
130
|
-
|
|
152
|
+
Parsing: the command is split into simple commands on unquoted `;` `&` `|` newline, quotes stripped, heredoc bodies skipped (they're data being written, not commands). Leading wrappers / keywords (`sudo`, `env`, `xargs`, `nohup`, `time`, `if`/`then`/`do`/`while`, `VAR=…` assignments) are skipped to find the real command; `cd` earlier in the line moves the base for relative targets (default base = workspace root). `~`, `$HOME`, `$PWD` / `$(pwd)` expand; any other variable or substitution expands to empty — its value when unset, which is how `rm -rf "$DIR/"` goes wrong. A glob meaning "everything in X" (`*`, `.*`, `**`) is judged by X; narrower globs (`*.log`) pass. A block throws `[Sandbox] rm blocked: "<word>" resolves to <path>, which is <reason>. Name the specific files or subdirectories to delete instead.` It's a heuristic against mistakes, not a parser — `eval`, `bash -c "…"`, scripts and command substitution aren't followed.
|
|
131
153
|
|
|
132
|
-
**Windows has no sandbox.** There is no bwrap equivalent, so `sandbox.ts` (`normalizeOptsForPlatform`) promotes every non-full call to `full` before it reaches layer 1 or layer 3 — `assertPathAllowed()` returns immediately without checking the workspace boundary or the hidden-path lists. Only layer 2 (tool filtering) survives, because `
|
|
154
|
+
**Windows has no sandbox.** There is no bwrap / Seatbelt equivalent, so `sandbox.ts` (`normalizeOptsForPlatform`) promotes every non-full call to `full` before it reaches layer 1 or layer 3 — `assertPathAllowed()` returns immediately without checking the workspace boundary or the hidden-path lists, and the rm guard is skipped (cmd syntax, no `rm`). Only layer 2 (tool filtering) survives, because `getSandboxBackend()` is always `null` there. The admin's access-level selector reads `sandbox: null` from `/api/health` and locks itself to Full. Net effect on Windows:
|
|
133
155
|
|
|
134
156
|
| Level | Effective behavior on Windows |
|
|
135
157
|
|---|---|
|
|
@@ -141,15 +163,15 @@ This is a known, unfixed gap (not a bug in a specific route): don't hand out `wo
|
|
|
141
163
|
|
|
142
164
|
### Sandbox hidden paths
|
|
143
165
|
|
|
144
|
-
Sensitive directories and files are hidden from workspace/readonly sessions via bwrap overlays (and the same lists gate `assertPathAllowed`
|
|
166
|
+
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:
|
|
145
167
|
|
|
146
168
|
**Global lists (configurable)** — credentials and cross-workspace state, configured in `settings.yaml` under `general.sandbox`:
|
|
147
169
|
|
|
148
170
|
| Setting | Default | Method |
|
|
149
171
|
|---|---|---|
|
|
150
|
-
| `hidden_dirs` | `~/.halo/secrets,~/.aws,~/.ssh,~/.gnupg,~/.docker,~/.config/gh,~/.halo/global/internal-sessions,~/.halo/global/logs` | `--tmpfs` overlay (empty directory) |
|
|
151
|
-
| `hidden_files` | `~/.npmrc,~/.bash_history,~/.gitconfig,~/.git-credentials,~/.netrc,~/.halo/global/{evo,cron,runs}.db` + their `-wal`/`-shm` files | `--ro-bind
|
|
152
|
-
| `writable_dirs` | (empty) | `--bind` read-write — for external CLIs that keep local state (e.g. `~/.kiro`); not applied to readonly sessions |
|
|
172
|
+
| `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 |
|
|
173
|
+
| `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 |
|
|
174
|
+
| `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 |
|
|
153
175
|
|
|
154
176
|
Changes take effect immediately — `config.ts` reads settings.yaml via an mtime-watched lazy cache, so the next `shell_exec` reads the latest values. These keys are `globalOnly` in the schema — a workspace `settings.yaml` cannot override them, since they define the security boundary agents run inside.
|
|
155
177
|
|
|
@@ -157,12 +179,12 @@ Changes take effect immediately — `config.ts` reads settings.yaml via an mtime
|
|
|
157
179
|
|
|
158
180
|
| Constant | Entries | Method |
|
|
159
181
|
|---|---|---|
|
|
160
|
-
| `WORKSPACE_HIDDEN_DIRS` | `.halo/sessions` (session transcripts), `.halo/logs`, `.halo/evo` (run dirs contain full source-session snapshots) | `--tmpfs` overlay |
|
|
161
|
-
| `WORKSPACE_HIDDEN_FILES` | `.halo/halo.db` + `-wal`/`-shm` (sqlite `agent_sessions` rows) | `--ro-bind /
|
|
182
|
+
| `WORKSPACE_HIDDEN_DIRS` | `.halo/sessions` (session transcripts), `.halo/logs`, `.halo/evo` (run dirs contain full source-session snapshots) | `--tmpfs` overlay / Seatbelt `subpath` deny |
|
|
183
|
+
| `WORKSPACE_HIDDEN_FILES` | `.halo/halo.db` + `-wal`/`-shm` (sqlite `agent_sessions` rows) | `--ro-bind ~/.halo/.sandbox-empty` / Seatbelt `literal` deny |
|
|
162
184
|
|
|
163
185
|
The rest of `.halo/` (INSTRUCTIONS.md, INDEX.md, docs/, memory/, skills/, agents/, prompts/, tmp/, canvas/, goal/, settings.yaml) stays readable — it's workspace knowledge agents need to work. `full` sessions bypass the sandbox entirely and see everything.
|
|
164
186
|
|
|
165
|
-
`/tmp` is not in the hidden list — it receives a standalone `--tmpfs` mount for process isolation (each
|
|
187
|
+
`/tmp` is not in the hidden list — under bwrap it receives a standalone `--tmpfs` mount for process isolation (each invocation gets its own empty `/tmp`), not for hiding secrets. Seatbelt has no mount namespace, so on macOS `/tmp` is the real one and writes to it persist.
|
|
166
188
|
|
|
167
189
|
Per-channel defaults:
|
|
168
190
|
- **Web** — inherits account's `access_level` (default `full`)
|
|
@@ -10,6 +10,16 @@ The dropdown to the left of the input lists every agent (global + workspace).
|
|
|
10
10
|
- Once you manually pick an agent, that choice is remembered across `clear` / new sessions until you pick something else.
|
|
11
11
|
- **A conversation is locked to one agent**: once you start chatting, the dropdown locks. To change agent, start a new session (`/session new`).
|
|
12
12
|
|
|
13
|
+
## Access level
|
|
14
|
+
|
|
15
|
+
The leftmost button of the input toolbar sets what the agent is allowed to change:
|
|
16
|
+
|
|
17
|
+
- **Full** — no restrictions.
|
|
18
|
+
- **Workspace** — the agent can read anywhere but write only inside this workspace. Credential files such as `~/.gitconfig` and `~/.git-credentials` are hidden from it. `git commit` still works, using your git name and email.
|
|
19
|
+
- **Readonly** — the agent can't write anything.
|
|
20
|
+
|
|
21
|
+
A change applies from the next message you send, and the level is saved per session. If a command fails because of the level, the agent will ask you to switch to Full instead of retrying. The button is locked to Full when the host can't enforce the other levels (Windows, or Linux without bubblewrap). On macOS the built-in `sandbox-exec` is used.
|
|
22
|
+
|
|
13
23
|
## Sending messages
|
|
14
24
|
|
|
15
25
|
- Enter to send
|
|
@@ -18,8 +18,11 @@ pnpm --filter @turmind/halo-cli link --global
|
|
|
18
18
|
|
|
19
19
|
The published package is `@turmind/halo` — a self-contained esbuild bundle
|
|
20
20
|
(CLI + server + admin-out + templates), staged under `packages/cli/dist-pub/`
|
|
21
|
-
by `scripts/build-bundle.mjs
|
|
22
|
-
|
|
21
|
+
by `scripts/build-bundle.mjs` when run with `HALO_RELEASE=1`. Without that flag
|
|
22
|
+
(desktop packaging, local bundle tests) the script writes a sha-suffixed build to
|
|
23
|
+
`packages/cli/dist-dev/` instead and never touches `dist-pub/`. The workspace
|
|
24
|
+
`@turmind/halo-cli` can't be published directly (it has `workspace:*` deps); the
|
|
25
|
+
bundle flattens them.
|
|
23
26
|
|
|
24
27
|
```bash
|
|
25
28
|
# 1. bump packages/cli/package.json "version" (e.g. 0.1.2)
|
|
@@ -30,11 +33,13 @@ cd packages/admin && npx next build --no-lint && node scripts/copy-monaco.mjs &&
|
|
|
30
33
|
# 3. stage the release bundle — HALO_RELEASE=1 stamps the bare version
|
|
31
34
|
# (0.1.2), NOT the default <version>-<sha>: a `-<sha>` suffix is a semver
|
|
32
35
|
# prerelease that `npm install` skips and never tags as `latest`.
|
|
36
|
+
# It also selects dist-pub/ as the output dir (dev builds go to dist-dev/).
|
|
33
37
|
HALO_RELEASE=1 node packages/cli/scripts/build-bundle.mjs
|
|
34
|
-
# 4. preview + publish
|
|
38
|
+
# 4. preview + publish — publish ONCE, then confirm with `npm view`
|
|
35
39
|
cd packages/cli/dist-pub
|
|
36
40
|
npm pack --dry-run # inspect tarball contents
|
|
37
41
|
npm publish # @turmind/halo@<version>, tag latest, public
|
|
42
|
+
npm view @turmind/halo version # may lag 2-3 min; re-view, don't re-publish
|
|
38
43
|
```
|
|
39
44
|
|
|
40
45
|
npm versions are immutable — a published version can never be overwritten,
|
|
@@ -11,6 +11,14 @@ The primary surface for talking to an agent.
|
|
|
11
11
|
- **Locked during an active session** — the agent is bound to the session; to change, start a new session (/session new)
|
|
12
12
|
- The Agents panel's "Test" button can also preselect an agent
|
|
13
13
|
|
|
14
|
+
### Access level
|
|
15
|
+
- Leftmost control of the input toolbar (just left of the attach button), fixed width so switching doesn't shift the row; colored badge per level — Full (amber) / Workspace (blue) / Readonly (emerald). The dropdown shows each level with a one-line description
|
|
16
|
+
- **Full** — no restrictions; **Workspace** — writes limited to this workspace; **Readonly** — read only, no writes (OS-sandbox semantics in [dev/tools.md](../dev/tools.md#access-level-per-session-dynamic))
|
|
17
|
+
- Applies to the **next message** sent while the session is idle; disabled while a response is streaming (a message queued mid-turn runs at the level already in effect)
|
|
18
|
+
- Opening an existing session shows its stored level; a new session starts at Full, and a level picked before the first send sticks
|
|
19
|
+
- **Locked to Full** when the host has no OS sandbox (`/api/health` `sandbox: null` — e.g. Windows, or Linux without bubblewrap); the tooltip says why
|
|
20
|
+
- When a command fails because of the sandbox, the agent is told to ask the user to switch to Full here rather than retrying
|
|
21
|
+
|
|
14
22
|
### Message rendering
|
|
15
23
|
- Markdown + code-block highlighting
|
|
16
24
|
- Tool-call card: expandable, shows tool name / input / output
|