# Changelog ## Unreleased - **Fixed** a session-wedge where strict providers (notably MiniMax, `400 invalid params (2013)`) rejected every turn of a long-lived session forever. Before each LLM call the request payload is now normalized to strictly alternate user/assistant turns (`normalizeMessageAlternation`): consecutive same-role messages are coalesced. This unwedges two production failure modes — (1) failed turns whose empty error-placeholder was stripped left orphan user messages that accumulated into a non-alternating run, and (2) an assistant text turn landing between a `toolCall` and its `toolResult`. The transform is request-only; persisted history is untouched, and `toolResult` messages (parallel results) are never merged. - **Changed** Supabase attachment storage to prefer a new-style Supabase secret API key (`sb_secret_…`). The key now resolves from `SUPABASE_SERVICE_ROLE_KEY` or the new `OPENHERMIT_ATTACHMENT_SUPABASE_SECRET_KEY`. `OPENHERMIT_ATTACHMENT_SUPABASE_SERVICE_KEY` is **deprecated** — it still works as a fallback but logs a warning; rename it to `OPENHERMIT_ATTACHMENT_SUPABASE_SECRET_KEY`. The legacy JWT service-role key continues to work. ## SDK 0.5.1 — 2026-05-18 - **Added** `AgentLocalClient.uploadAttachment` / `listAttachments` / `getAttachment` — the same per-session attachment surface that `GatewayClient` already exposed, but on the per-agent client, so callers holding a channel-mode token (not an admin token) can upload directly under `/api/agents/:id/sessions/:sid/attachments`. Server-side auth is unchanged: same `requireAuth` → `enforceSessionNamespace` → `requireSessionAccessHttp` chain that `postMessage` uses, so any token that can `postMessage` on a session can also `uploadAttachment` to it. - **Added** `SessionAttachment` and `AttachmentMaterializationState` to the package's top-level exports so downstream consumers no longer need to mirror them locally or recover them via `Awaited>`. - **Added** `agentLocalRoutes.sessionAttachments(sessionId)` / `sessionAttachmentById(sessionId, attachmentId)` (plus their `*Pattern` siblings) in `@openhermit/protocol`, mirroring the existing `gatewayRoutes.agentSessionAttachments*` entries. ## 0.8.0 — 2026-05-18 ### File attachments end-to-end Agents now have first-class file attachments. Users upload files in the web composer, attachments are persisted by a pluggable `AttachmentStorage` backend, and the bytes are materialized into the agent's sandbox so tools can `file_read` them by path. - **Added** `session_attachments` table + `DbAttachmentStore` with `originalName`, `safeName`, `mimeType`, `sizeBytes`, `sha256`, `materializationState` (`pending` / `copied` / `failed`), and sandbox path (#110). - **Added** `AttachmentStorage` interface and `LocalAttachmentStorage` provider (#110). S3 and Supabase providers followed in (#116), gated by optional peer deps (`@aws-sdk/client-s3`, `@supabase/supabase-js`). - **Added** `POST/GET /api/agents/:id/sessions/:sid/attachments` endpoints with multipart upload, MIME sniffing via magic bytes, and 25 MB default cap (#115). Every successful upload is materialized into the sandbox at `~/.openhermit/attachments///`. - **Added** `attachment_fetch` tool, multimodal user messages, web composer drag/drop+paste support, and SDK upload helpers (#117). - **Changed** materialization policy to always-on with lazy self-heal (#118). When the sandbox is down at upload time the row is marked `failed`; on first `attachment_fetch` the tool re-materializes from storage. The previous `sandboxCopyMaxBytes` threshold and `'skipped'` state were removed. ### Gateway config UI: Settings + JSON tabs The admin Gateway Config panel grew a JSON tab alongside Settings. The JSON tab shows the full DB-stored config as editable JSON so fields the form doesn't expose (e.g. `attachments.storage`) can be edited from the UI without shell access. Switching tabs re-derives the destination view from the last applied config, so unsaved edits don't silently leak across tabs. ### Breaking: env vars are for secrets only Non-secret attachment env vars were dropped — provider selection and non-secret resource pointers must now live in the DB-backed gateway config under `attachments.storage`. - **Removed** `OPENHERMIT_ATTACHMENT_PROVIDER`, `OPENHERMIT_ATTACHMENT_S3_BUCKET` / `_REGION` / `_PREFIX` / `_ENDPOINT`, `OPENHERMIT_ATTACHMENT_SUPABASE_URL` / `_BUCKET` / `_PREFIX`, `OPENHERMIT_ATTACHMENT_ROOT`, `OPENHERMIT_ATTACHMENT_MAX_BYTES`. - **Kept (env-only)** AWS default chain (`AWS_ACCESS_KEY_ID`/etc.), `SUPABASE_URL` (project URL embeds the project ID — treated as part of the credential bundle), `SUPABASE_SERVICE_ROLE_KEY`. - Deployments that relied on the env fallbacks must add an `attachments.storage` block to gateway config before upgrading or fall back to local-disk storage on restart. ### Fleet usage analytics Token usage and cost views surfaced in the Fleet and Stats panels (#100). The per-session Usage modal split into Overview / By model / Daily tabs (#105). Partial index on `session_events` added so the aggregation query stays cheap as event volume grows (#103). Fixes for the aggregation SQL (#101, #102) and time-tab scope (#104). ### Channels - **Added** `hermit channel install / uninstall / list` for managing channel plugin packages (#95). - **Added** public gateway URL config, channel auto-start, and persisted runtime errors so a misconfigured channel doesn't take down boot (#107). - **Added** runtime error reporting from bots back to the channel layer (#108). - **Fixed** Telegram realtime approval review now passes `channelUserId` so the gate resolves the acting user (#106). ### Fixes - **agent:** promote thinking to text when `stopReason=toolUse` with no tool_use blocks (#111). - **agent:** don't duplicate the new user message on the first turn after resume (#109). --- ## 0.7.0 — 2026-05-15 ### Channel plugin contract Channels are now plugins. Each channel — built-in or external — provides a `ChannelManifest` describing its config schema, secret keys, capabilities, and setup steps. The gateway loads built-in channels through the manifest registry on boot, and external channel packages can be loaded by listing them under `channelPackages` in the gateway config. - **Added** `ChannelManifest` + `ChannelManifestRegistry` in `@openhermit/protocol` (#87). - **Refactored** built-in channels (Telegram / Slack / Discord) to register via the manifest registry (#88). - **Added** interactive setup contract for multi-step channel flows (QR scan, OAuth) so plugin channels can drive setup from the admin UI without hardcoded per-channel code (#90). - **Added** `channelPackages` gateway config: each entry is an npm package name, dynamic-imported at boot, whose default export must be a `ChannelManifest`. External packages may override a built-in by matching its key (#94). ### @openhermit/channel-wechat (text-only v0) First external channel plugin published to npm (#91, #93). Connects to WeChat via iLink (browser scan + persistent session), text-only for v0. Install with `npm install @openhermit/channel-wechat`, then add the package name to `channelPackages` in gateway config and restart the gateway. ### Docs - New user-facing manual (#84, #85) covering identity / soul / rules sections and the everyday agent workflow. --- ## 0.6.8 — 2026-05-09 ### Breaking: drop `channel_message_sent` Proactive sends from `session_send` were recorded as a synthetic `channel_message_sent` log entry whose `text` field made the row invisible to LLM history replay (which keys off `content`). The wire `OutboundEvent.channel_message_sent` type existed in the protocol but was never published. Both have been removed. - **Removed** `channel_message_sent` from `OutboundEventBody` (wire). Zero publishers, zero subscribers — pure dead code. - **Changed** `session_send` to record deliveries as a normal assistant log entry: `{ role: 'assistant', content: text, metadata: { source: 'session_send', fromSession, channel, to, messageId } }`. The receiver session's LLM history replay now sees proactive sends as ordinary assistant turns. - **Added** explicit `metadata?: Record` slot on `SessionLogEntry`. Existing `[key: string]: unknown` permits it; this just documents the convention so future derivative fields land in `metadata` instead of scattering at the entry root. - Migration `0021_drop_channel_message_sent.sql` rewrites existing rows in place: `event_type` becomes `'assistant'`, `text` is folded into `content`, delivery details move under `payload.metadata`. Released as a patch since the SDK is still in 0.x. --- ## 0.6.7 — 2026-05-09 ### Breaking: unified approval event naming Approval-related events used a mix of legacy tool-specific and new resource-based names. Cleaned up to a single set: - **Removed** `tool_approval_required` (wire). Consumers must listen for `approval_requested` with `mode === 'realtime'` and read the tool name from `resourceKey` (instead of `toolName`). - **Added** `approval_resolved` (wire) — fires when a request is resolved, in both realtime (after the gate decides) and async (after the owner runs `approval_review`) modes. Carries `decision`, optional `resolution`, and `reviewerId`. - `approval_pending` is now `mode: 'async'` only — realtime mode no longer emits a redundant pending event (requester is the same as the reviewer). - DB `session_events` rows previously written as `tool_approval_requested` / `tool_approval_resolved` are now written as `approval_requested` / `approval_resolved`. Migration `0020_approval_event_rename.sql` renames existing rows. The DB payload is also generic (`resourceType`, `resourceKey`, optional `toolCallId`) instead of tool-specific. - Async non-tool resources (file/exec/etc.) now also persist `approval_requested` / `approval_resolved` to the requester's `session_events` for audit symmetry. Previously only the tool-specific path wrote DB rows. - `approval_pending` continues to be wire-only — its lifecycle is captured by the `approval_requests` table. Released as a patch since the SDK is still in 0.x. --- ## 0.6.6 — 2026-05-09 ### Outbound `agent_start` event Added `agent_start` to `OutboundEvent` so SDK consumers see an explicit turn-start signal that mirrors the `agent_start` log entry already written to the message store. Carries `correlationId` (the inbound user-message id, same as the rest of the turn's events). Additive — non-breaking. --- ## 0.6.5 — 2026-05-09 ### Breaking: outbound event identifiers (#51) The `messageId` field on outbound events `text_delta`, `text_final`, `thinking_delta`, `thinking_final`, `tool_call`, and `tool_result` was misnamed: every event in a turn carried the same value (the inbound user-message id), so consumers persisting events by `messageId` hit unique-key collisions. - **Renamed** `messageId` → `correlationId` on those six events. Same semantics: the inbound user-message id that triggered the turn. - **Added** `eventId: string` to every outbound event. Per-event unique (UUID) — minted by the runtime. Use this for persistence dedup. - SDK consumers reading the event stream must update field references (`event.messageId` → `event.correlationId`, plus optionally use `event.eventId`). Released as a patch since the SDK is still in 0.x and the misnamed field shipped only one patch ago in 0.3.3. `SessionMessage.messageId` (inbound), `ChannelOutboundResult.messageId`, and `channel_message_sent.messageId` are unchanged — those are unrelated identifiers. --- ## 0.6.2 — 2026-05-07 ### Lazy hydration of agents (#23, #24, #25, #26, #30) The gateway no longer eagerly starts every agent at boot. Agents are now hydrated on first access, gated by a per-agent `hydrating` map that fences `start()` against double-start races. A central cron scheduler (Phase 2) keeps scheduled work running for cold agents, an LRU eviction policy (Phase 3) reclaims idle runners, and the legacy `autoStartAgents` config has been removed in favour of fully lazy hydration. ### Channel bridge connection pool (#29) Per-agent channel bridges (Slack/Discord/Telegram) are hoisted into a shared connection pool, so multiple agents on the same workspace share a single upstream connection instead of each opening their own. ### Async approval callback persistence (#28) Channel approval requests now persist their callback ID via `approval_requests.short_id`, so async approvals survive gateway restarts. ### Fixes - **agent:** auto-ensure sandbox in the e2b and daytona file backends so file ops don't fail against a cold sandbox (#27). - **web:** show DB status on the agent picker and block clicks on disabled agents (#31). ### Performance - **mcp:** connect MCP servers asynchronously so a slow server no longer blocks agent boot (#22). --- ## 0.6.1 — 2026-05-07 ### Gateway config moved to the database Gateway-level configuration (CORS, auto-start agents, sandbox presets, auto-provision target) is now stored in the `meta` table instead of `~/.openhermit/gateway/gateway.json`. On first boot, an existing `gateway.json` is migrated into the DB and renamed to `gateway.json.imported`. New `hermit gateway config get/set/show` commands and a **Config** tab in the admin UI let operators edit the live config; changes require a gateway restart to apply. ### Security hardening (#14) - Admin bearer-token comparison now uses `crypto.timingSafeEqual` to prevent timing attacks. - `jwtVerify` is pinned to `HS256` to block algorithm-confusion attacks. - Markdown rendered in chat messages is now passed through DOMPurify before `dangerouslySetInnerHTML`. ### Performance (#15) - Eliminated N+1 user lookups in fleet/agent endpoints (batched `UserStore` methods). - Boot is parallelized: agent runners and channel starts kick off concurrently, and sandbox `listAll` is bucketed across providers. - Added a GIN index on `sessions.user_ids` (migration `0015_sessions_user_ids_gin.sql`). - Web UI bundle is code-split via `manualChunks` + `React.lazy` on `ChatShell` / `ManagePanel`; initial JS dropped from ~363 KB to ~207 KB. ### Web UI: self-service token + restore flow (#18) - New **Show access tokens** panel on the agent picker exposes the JWT bearer token and the device key (with copy buttons and warning) — no more digging through devtools. - New **Restore from key** mode on the welcome screen accepts an exported device key JSON to recover access on a fresh browser. - Renamed the per-agent join field from "Access Token" to "Agent invite token" with help text to stop the JWT/invite mix-up. ### SDK (#16) - `GatewayClient.registerMcpServer()` wrapper around `POST /api/admin/mcp-servers`. ### UI fixes (#19) - `/admin/config` panel: replaced undefined `.form-row` classes with the existing `.field` pattern so fields stack vertically. - Added spacing between **Show access tokens** and **Sign out** on the agent picker. --- ## 0.6.0 — 2026-05-06 ### Policy v2: unified effect model + approval flow The access policy system now supports three effects per policy row: `allow`, `deny`, and `require_approval`. This replaces the previous `autonomy_level` / `require_approval_for` fields in SecurityPolicy with a single, composable mechanism. **Effect evaluation** follows deny > require_approval > allow precedence (AWS IAM style). When policy rows exist for a resource but none match the calling principal's grants, access is denied — no silent fallthrough. **Approval flow** supports two modes: - **Real-time approval**: when the owner is in an interactive session, a UI prompt appears inline (ApprovalGate). The owner approves or rejects without leaving the conversation. - **Async approval**: when the requester is on a different channel (e.g. Telegram guest), an `ApprovalRequest` is persisted and the owner is notified on their configured channel with approve/reject buttons. The agent tells the user to wait for owner approval and stops attempting workarounds. Approved requests are cached with a configurable TTL (default 60 minutes). Owners can choose `persistent` resolution to auto-create a permanent allow policy row for the requester. **ToolPolicy simplified**: the previous `{ kind: 'fixed', grants } | { kind: 'configurable', defaultGrants }` union is replaced by a single `{ defaultGrants: Grant[] }` interface. All tools are now overridable via DB policy rows — there is no longer a "fixed" category that ignores the policy table. **Default grants tightened**: | Tool | Old default | New default | |------|------------|-------------| | `exec` | owner + user | owner only | | `file_write`, `file_edit`, `file_delete` | owner + user | owner only | | `schedule_create/update/delete/trigger` | owner + user | owner only | | `file_read`, `file_list`, `file_stat` | owner + user | owner + user (unchanged) | **File and exec policy scopes** are now populated at creation time with structured fields (`{ sandbox, mode, path }` for files, `{ sandbox, command, cwd }` for exec) instead of relying on fallback matching at evaluation time. **Channel identity forwarding**: channel-authenticated API calls (e.g. Telegram bridge reviewing an approval) can now pass `x-channel-user-id` header so the gateway resolves the acting user's identity for authorization. ### Breaking changes - `ToolPolicy` type changed from a discriminated union to `{ defaultGrants: Grant[] }`. All tool definitions updated. - `evaluateAccess` returns `'deny'` (not the default decision) when policy rows exist but no grant matches the principal. Previously this allowed guests to bypass owner-only tools. - `autonomy_level` and `require_approval_for` in SecurityPolicy are deprecated. They still work as syntactic sugar (synthesized into virtual policy rows at runtime) but new deployments should use policy rows directly. --- ## 0.5.2 — 2026-05-05 ### Fixes - `hermit --version` now reads from `package.json` instead of a hardcoded string. --- ## 0.5.1 — 2026-05-05 ### First-class filesystem tools Six new tools (`file_read`, `file_write`, `file_edit`, `file_list`, `file_stat`, `file_delete`) give agents direct filesystem access inside their sandbox, replacing the previous pattern of shelling out via `exec`. `file_read` supports line ranges (`offset`/`limit`), and `file_edit` provides find-and-replace semantics. All three sandbox backends (host/docker bind-mount, E2B, Daytona) are supported. ### Cross-channel identity link Users can now link their identities across channels (Telegram, CLI, web, Discord) via `identity_link_request` / `identity_link_confirm` tools. A token generated on one channel can be confirmed on another to merge the accounts. Ghost users created during the link flow are absorbed into the confirmed identity. ### Security: symlink escape detection `HostFileBackend` (used by the `host` and `docker` backends) now resolves all paths through `realpath` before performing I/O, then re-checks the resolved path falls within the sandbox root. This prevents agents from escaping the workspace via symlinks. ### Fixes - Identity tools now register for guest users on Telegram (channel info is threaded through `refreshAgentConfiguration`). - Identity link tool descriptions improved for discoverability with weaker models. - `file_write` overwrite mode no longer uses atomic temp+rename (which broke on cross-device mounts). - System prompt now directs agents to prefer file tools over exec for file operations. - Discord channel turn serialization fixed (#12), runtime dependency bundling fixed (#11). ### Refactor - `exec-backend.ts` (1385 lines) split into `core/backends/` folder: `docker.ts`, `host.ts`, `e2b.ts`, `daytona.ts`, `file-backend.ts`, `shared.ts`. ### Docs - `tools.md` and `fs-tools.md` updated with all new tools. - Access policy proposal and sandbox model docs refreshed. --- ## 0.5.0 — 2026-05-04 ### Sandboxes are first-class Sandboxes used to live inside each agent's config as `exec.backends[]`. They are now stored as rows in a `sandboxes` table, with their own lifecycle (`pending` → `provisioned` → `deleted`), per-row `runtime_state` for cross-restart reconnection, and a per-agent `(agent_id, alias)` partial-unique index that allows re-using an alias after soft-delete. The runtime constructs each agent's `ExecBackendManager` from these rows; the legacy `exec.backends[]` path remains as a fallback when no rows exist (mid-backfill or sandbox store unavailable). ### Sandbox presets in `gateway.json` `autoProvisionSandbox` is no longer an inline `{ enabled, type, config }` object — it now references a named preset: ```json { "sandboxPresets": { "docker-ubuntu": { "type": "docker", "config": { "image": "ubuntu:24.04", "username": "root", "agent_home": "/root" } }, "e2b-default": { "type": "e2b", "config": { "template": "base" } }, "daytona-default": { "type": "daytona", "config": {} } }, "autoProvisionSandbox": "docker-ubuntu" } ``` **Breaking** — gateways carrying the legacy shape will refuse to start with a clear migration message. Move the inline config into `sandboxPresets[]` and set `autoProvisionSandbox: ""`. `POST /api/agents` (and the admin UI's create-agent dialog) accept a new `sandbox` field: - omitted → use the gateway's `autoProvisionSandbox` - `""` → provision that preset - `null` → skip sandbox provisioning entirely `GET /api/sandbox-presets` returns the registry to authenticated users so frontends can populate dropdowns. ### Daytona backend New `daytona` backend type alongside `host` / `docker` / `e2b`. Set `DAYTONA_API_KEY` in the gateway env, then pick `daytona` as a preset type or pass `--type daytona` to `hermit sandbox add`. Archived sandboxes (idle 7d+) are recovered transparently on `ensure()` via `start()`. ### Access policy enforced end-to-end The `access` field on the agent's security policy (`public` / `protected` / `private`) is now enforced at session-open time: - A sender with no membership row on a non-public agent is rejected (404) **before** any message is processed. - Globally-known users (registered via another agent on the gateway) no longer auto-claim a guest role on `private` / `protected` agents — they must be added explicitly via `/members`. - The create-agent dialog gained an Access dropdown so operators can pick the level at create time. ### Sandboxes admin tab The admin UI's `Containers` tab is now `Sandboxes` and reads from the `sandboxes` table directly, overlaying live `docker ps` runtime info for docker rows (`—` when the container isn't on this host). ### Misc - `host` backend now enforces single-instance-per-gateway at the API layer (was previously enforced inside the backend). - Soft-deleted sandbox aliases can be reused immediately (partial unique index migration `0009`). - New CLI flags: `hermit agents create --sandbox ` / `--no-sandbox`. --- Earlier history: see git tag list (v0.4.16 and prior).