# Changelog All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). ## [Unreleased] ## [1.3.5] - 2026-11-02 ### Added - Models: OpenAI's `gpt-6.1-terra` ($1 / $5 per million tokens, reasoning can be switched off) and `gpt-6.1-sol` ($2 / $10, always reasons). - Fast mode. An agent can run the fast variant of a model whose provider offers one: Claude Opus 5.5 on Anthropic, and OpenAI GPT-5.6, GPT-6 and GPT-6.1. The model select and the model catalog list each such model a second time as "· Fast", with its prices; the provider bills those calls at 2× every rate. Choosing the variant sets the agent's `fastMode`, which `PATCH /api/instances/:slug` accepts and export and import carry (migration `0097`). `GET /api/instances/models` returns the price multiple as `fastMultiplier`. A call is billed at the fast rate only when the provider reports that it ran fast. - Text written alongside tools. A model can write text in a step that also calls a tool; until now that text was dropped on WhatsApp and HTTP, shown on a live chat and spoken on voice, and the model was never told. Each agent can now choose, in Settings, to discard it (only the text after the last tool call is the reply, on every channel), show it as progress (shown on a live chat while the agent works, not kept in the reply) or make it part of the reply (every step's text, in order). A set policy adds a "Reply delivery" section to the prompt; unset keeps the previous behaviour and prompt. The agent's `intermediateText` (`discard`, `progress`, `reply` or `null`) is accepted by `PATCH /api/instances/:slug` and carried by export and import (migration `0098`). - The analytics dashboard compares every KPI with the previous period (the previous figure when the percentage would be meaningless), draws each KPI's daily shape, overlays the previous period's cost, and shows when people write as a weekday × hour heatmap. Tool usage and tool latency become one Tools table with calls, success rate and p95. The API gains `overview.previousPeriod`, `dailyTrend[].promptTokens` and `cachedInputTokens`, `previousDailyCost`, `latency.previousOverview` and `latency.toolTimings`. - The generated description under a conversation's title stops at two lines, with Show more / Hide when it is longer. ### Changed - The OpenAI `standard` tier resolves to `gpt-6.1-terra` instead of `gpt-6-sol`, at half the price. Agents with no model of their own, and the `spawnTask` sub-agents of those agents, change model. - On WhatsApp and Telegram, a message fragment that arrives after the running turn has started a tool no longer cancels it: the turn finishes and replies, and the fragment gets a turn of its own. A turn that has not called a tool yet is still cancelled and restarted on the joined text. A tool that a cancelled turn asks for does not run and returns `{ error, notRun: true }`. - The response time KPI is the pipeline median (p50), as in the response time chart, which drops p99. The model, tier and channel donuts become ranked bars, and the tier breakdown moves into the model card. - `GET` analytics returns `hourlyDistribution` as 168 rows, one per weekday and hour, each with `weekday` (0 = Monday), instead of 24. - Webhook-started turns and rooms run with the agent's fast mode, reasoning level and temperature, which they ignored. - The engine moves to Zod 4.6.6 and `@polyant-ai/plugin-sdk` 1.12.0, which accepts plugins written with Zod 3 or 4. The JSON Schema of the core tools is unchanged. The text of validation errors in `400` responses follows Zod 4. - New Polyant logo in the panel and the README. - Dependency updates: `ai` 7.0.156 and `@ai-sdk/openai` 4.0.104 (the engine now requires them), Next.js 16.4.3, langsmith 0.11.7, the AWS SDK, dotenv, tsx, drizzle-kit and Playwright. ### Fixed - A tool run by a WhatsApp or Telegram turn that a new fragment then cancelled was missing from the history of the restarted turn, which could run it again. - `@ai-sdk/openai` before 4.0.104 dropped `reasoning_effort: "none"` on `gpt-6.1-terra`, so it kept reasoning with thinking off. - KPI values and trends stay inside their tile at every width. - The analytics charts no longer cut their donuts or axis labels, and a daily series fills the days without activity. ### Security - Zod 4.6.6 fixes GHSA-mrhx-c7xc-m3w5: an array schema without a length bound collected one issue per failing element, so a large request could exhaust memory. ## [1.3.4] - 2026-10-26 ### Added - The Monitor: one timeline per workspace of tool calls, hook executions, webhook-started conversations and scheduled task runs, with live updates, filters by group, type, agent, outcome, period and text, and a detail drawer. It replaces the Activity page and the tool-call list, whose addresses now open it, and the agent's Logs tab, now called Monitor, shows the same view for that agent in place of its webhook and scheduled-run lists. The engine serves it at `GET /api/monitor` and `GET /api/monitor/:type/:id`; each type is read under the permission that guards it elsewhere and left out of the list when the caller lacks it. - A knowledge document can be switched off. It stays stored and listed, and vector and full-text search, and a plugin's knowledge read, skip it until it is switched on again. Each document row has a switch, and `PATCH /api/instances/:slug/knowledge/:docId` takes `{ "enabled": boolean }`. Export and import carry the flag (migration `0096`). - A skill's name and description can be edited from its page and form. `PUT /api/skills/:name` accepts `displayName`, which cannot be blank; the slug stays fixed and agents keep reading the skill by it. - The Web/API channel tab shows the endpoint to call, the header for the current authentication setting, and server-side curl and fetch examples that read the key from `AGENT_API_KEY`. It warns when authentication is off and when the address is only the localhost fallback (`GET /api/instances/:slug/channels/web/endpoint`). - The conversation list shows the contact, can be filtered by it (`userIdentifier`), and its search box matches it. - A tool can ask a model through the engine: its context carries `ctx.ai.chat({ system, messages, tier })` and `ctx.abortSignal`. The call runs through the AI gateway on the agent's provider as a service call of the turn, logged in the AI log, traced in LangSmith with the turn's settings and priced like the turn's own calls; the tier picks the model. Both fields are optional in `@polyant-ai/plugin-sdk` 1.11.0, so a plugin that also supports older engines keeps a fallback, and `ctx.apiKeys` is unchanged. - A missing-permission 403 carries `code: "missing_permission"` and the `permission` it needed, next to the existing message. ### Changed - Every list page has one shape: title with the primary action top right, search above the list, a table sorted by the visible name with the secondary line under it, an «Active» switch for on/off state, and a right-hand drawer that holds the create and edit forms and delete. Badges, empty, loading and no-results states are the same everywhere. - The agent's sidebar has six groups (Overview, Configuration, Knowledge, Resources, Automations, Governance) with every section on its own row, automations included; `?tab=` values are unchanged, so existing links keep working. Analytics is called Dashboard, the Polyant mark at the top of the sidebar leads home, and skill lists show the description under the name. - The message Debug sheet is now "Message details", in tabs: Model, Prompt, Tools, Messages, Steps and Context. Prompt, Messages and Context each have a Copy action; the conversation state and the messages render as a JSON tree whose values copy on click. The Model tab adds the turn's channel, streaming and, on a voice turn, its transcription, plus a table of the turn's model calls with time, type, model, duration, tokens, cost and outcome. The message debug endpoint returns `turn` and `modelCalls` for it. - The analytics model distribution groups the calls the platform makes on its own (titles, summaries, checks) under one labelled slice. - The agent header puts icon, name, slug and status on one line, the agent sidebar names the agent, and a new agent opens on its prompts. The user menu moves from the sidebar footer to the header. Every side drawer has the same width, and a knowledge document opens from its row in one. - The first-steps card lists every step, derived from what already exists, and ticks the finished ones. The panel no longer offers a page tour on its own; the introduction stays in the Help menu. - Italian copy reviewed across the panel; costs follow the panel language, and agent status and default prompt sections are translated. - At `LOG_LEVEL=debug`, warn and error lines print the stack trace, sanitized. At every level an error line names up to three causes with their codes, and security-relevant refusals (webhook signature and secret failures, SSRF refusals, a channel switched off automatically, dropped audit entries) are logged at error level. 401 and 403 responses stay at warn. - Tool input is validated with ajv 8, with the string formats and without strict mode, so plugin schemas compile as they did with ajv 6. - Dependencies: Next.js 16.4.2, React 19.3.1, AI SDK 7.0.153, NestJS 12.2.0, Express 5.3.0 in the engine, Radix UI 1.8.1 and Vitest 5.0.5. For their security fixes: body-parser 2.4.0, multer 2.5.0, undici 7.30.1 and 8.12.3, follow-redirects 1.16.2, markdown-it 15.0.3 and Vite 8.3.5. ### Fixed - A row the database refused blocked the pipeline trace and AI call log: the batch was put back and failed again on every flush. The refused row is now set aside and the rest is written; on a connection or timeout error the rows stay buffered. - An AI call log row took the time its batch was written, so a call could be placed in the following turn. It now takes the time of the call. - A conversation title could keep a speaker label or wrapping quotes. - A mistyped workspace in the address showed the user as belonging to no organization; it now shows the 404 page, localized like any address that matches no route. - Picking a page right after a list opened sent it back to page 1. - A failed read of the API keys or members list showed an empty table; it now says so and offers a retry. - The guided setup marked the chat step done before the agent had answered. - The Playground announced the new conversation of the user's own chat. - Dialog and sheet close buttons were not translated, and the Italian flag was drawn wrong. ### Security - A failed database query no longer writes its parameters to the error log; they could carry message bodies and tool output. - MCP server secrets of 8 characters or fewer are now fully masked in API responses instead of showing their last four characters, as channel secrets already were. ## [1.3.3] - 2026-10-16 There is no 1.3.2 of Polyant. ### Added - The agent's Embedder panel lets you choose the embedding dimension among the ones the embedder supports. `PATCH /api/instances/:slug` accepts `embeddingDim`; changing it deletes memories and knowledge like an embedder switch, and requires `confirmWipe` when there is data to lose. ### Changed - A 4xx `HttpException` is logged as a warning, a 5xx as an error. - The tool drawer is wider, so parameters and secrets fit. ### Fixed - Uploading an agent icon failed under the default CSP, which blocks `blob:` images. The file is now decoded without loading a URL, and a decode failure shows an error. - The channel manager left the message coordinator running on shutdown. - Expired active webhook triggers were never swept. - The Telegram adapter kept the body reader locked after a capped read. - Speech-to-text requests left their abort listener on the caller's signal. - Outbound HTTP did not close the per-request agent after the response. - The AI call log dropped the newest entries instead of the oldest when its buffer overflowed. - Copying a webhook URL or an auth secret showed a success toast when the clipboard refused the write. - The knowledge tab printed an undefined unit for documents of 1 GB or more. ## [1.3.1] - 2026-10-14 ### Changed - Lists in the admin panel share one shape: the same cell spacing and column layout, the whole row opens the item's detail, and switches or menus in a row never open it. This covers the agent's tools, MCP servers, scheduled tasks and model tasks, the agent list and memory. The trailing arrow on rows is gone. - Tab bars are drawn as chips everywhere (settings, skill detail, tool picker, the agent's automation sections). - Buttons that open a creation are labelled "Add". - Charts use Recharts 3; Recharts 2 is no longer maintained. ### Fixed - The period-over-period badge on the analytics cards showed a growing response time as a negative change (an increase of 436% read "-435.8%"). It now shows the real direction, and its colour says whether the change is good. - A change measured against a previous period with no data showed "+100%". The engine now returns no comparison (`null`) and the card shows no badge; changes above 999% read ">+999%". - Opening a drawer focused its first control, which showed that control's tooltip on every click. The drawer now takes the focus itself. ## [1.3.0] - 2026-10-14 ### Added - An OAuth MCP server can use one shared account for the whole agent. An admin connects it from the agent's MCP section and every conversation uses it; until it is connected the agent does not use the server. Authorizing in each conversation stays available and remains the default (migration `0094`). - Plugin manifests can give `displayName` and `description` one text per locale, keyed by BCP 47 tag (`en-US`, `it-IT`). The panel shows the reader's language and falls back to the same language, then English. A plain string keeps working and stands for every locale; a manifest with a locale map needs engine 1.3.0 or later. - Tools and hooks can carry a `summary` for people, one text per locale, and the `label` and `description` of their configuration fields can be localized the same way (plugin SDK 1.10.0). The Tools section, the tool picker and the hook panels show them in the reader's language; a tool without a summary shows the first line of its description as before. The description the model reads stays one string and is shown in the tool's panel as the text the agent reads. - The panel names tool categories in English and Italian. Every built-in tool and hook has a summary, and its fields have labels, in both languages. ### Changed - About moves from the sidebar footer to the last item of the Help menu, and the agent page section "To review" is now "Alerts". - The panel's chip switchers share one component, and the selected chip stays readable. - Continuous integration reports one required check, `CI`, which needs every other job, the DCO sign-off included. Documentation-only pull requests skip the build and test jobs. ### Fixed - `instance_hooks.action_type` defaults to `function` in the database as it already did in the schema, so a hook row written without it no longer gets a hook type the engine stopped running (migration `0095`). - A refresh of a shared MCP account that the provider refuses no longer erases the tokens a concurrent refresh has just saved, which left the agent disconnected with providers that rotate refresh tokens. - Adding an MCP server under a slug the agent already uses is refused instead of replacing the existing server. ## [1.2.0] - 2026-10-12 > **Upgrading from 1.1.2 needs operator action.** The engine and the panel run > on Node 24. Several built-in tool families and > `fileUpload` now require plugins (agents keep them enabled under their new > names). Attachment storage uses each agent's own bucket and access keys, and > storing inbound attachments becomes a per-agent choice, off for every agent. Telegram and Slack arrive > through webhooks and need a public address. A new agent starts with its Web > channel off and requires its API key. Environment variables that set product > behaviour are gone, and a non-default value has to be entered in the panel > before the upgrade. Migration `0086` rewrites the conversations table. See > [docs/UPGRADING.md](docs/UPGRADING.md). ### Added - First-access guidance in the panel. A new user gets a full-screen guided setup (provider and key, model, agent, a chat test through the Playground, channels and agent key, optional knowledge), then an interface tour; the first time a real agent's page opens, a small offer starts a tour of its sections. Every step can be skipped, "Don't show again" is available at each stage, and the new Help menu in the header reopens all three and links to the documentation, the marketplace and the product site. The state is stored per user on the engine (`users.onboarding`, returned by `GET /api/me` and written with `PATCH /api/me/onboarding`), so it follows the person across devices. Accounts that exist when migration 0093 runs start as dismissed. - A `createSkill` tool lets an agent write a skill from the conversation and enable it on itself. It is opt-in: no agent has it until an operator enables it in the agent's tools. - Organization Owners issue and revoke management API keys from the panel, under **API keys** in the organization's navigation, so `polyant-cli` and scripts can authenticate with `X-Polyant-Key` on an OSS deployment. A key carries the Owner permission set and an optional expiry, and its token is shown once. The routes are `GET`/`POST /api/organizations/:orgSlug/api-keys` and `DELETE /api/organizations/:orgSlug/api-keys/:keyId`; they need `org:write`, and issuing and revoking refuse a request authenticated with a key, so a key cannot mint or revoke keys; it can list its own organization's. Issuing and revoking are recorded in the management audit log. - Settings → General holds the installation's operational policies, for a platform admin: analytics retention, the engine's public address, the per-user and global live-stream caps, the rate-limit window and request limit, the agent-to-agent and MCP connection timeouts, and the two scheduler deadlines. Each is empty until set, and an empty field shows the value in force as its placeholder. - Agents can set their timezone, locale, memory-deduplication threshold and message-coordination timings from their Settings page, and an organization's knowledge-document cap can differ from the shipped default without a redeploy. - The engine's public address can be corrected without a redeploy. `BASE_URL` stays as the value the deployment boots with; the stored address wins where one is set, and every webhook URL, OAuth redirect and agent card is built from the resolved value. - Web chat context: `POST /api/instances/:slug/chat/stream` (and `/v1/chat/completions`) accept a `context` object that is projected onto the conversation state before the turn, through a per-agent web context field mapping edited under Channels → Web/API, so a voice or web front end hands tools and hooks the caller's identity and keeps streaming. A request with context requires `chat_id` and the agent's API key, and an agent that maps at least one context field requires its key on every web turn, with or without context, even when its authentication is otherwise off. Migration `0085` adds the column. - `_private` is a reserved conversation-state key that is stored and returned by the state API but never rendered into the prompt. The whole underscore namespace is reserved: neither the web context mapping nor the HTTP channel mapping may target an underscore key. - A conversation can be followed live from its detail page, on any channel: `GET /api/activity-stream/conversation` streams one conversation's activity and signals each committed write, gated by `conversation:read` and sharing the live-stream caps. Tool calls now reach the activity stream, so the activity panel shows them, and the Playground follows the same detailed-view preference as Conversations. - Conversation activity is shown as a lightweight timeline. - Scheduled tasks recover after a process dies or a run exceeds its deadline. `GET /health/scheduler` reports free slots and stuck runs, and migrations add per-task `max_run_ms` plus an index for running tasks. - Models: OpenAI's gpt-6 family (`gpt-6-astra`, `gpt-6-sol`, `gpt-6-luna`), Claude Opus 5, Opus 5.5, Sonnet 5.5 and Fable 5.1, and on Nebius GLM-5.3, GLM-5.3-Flash, DeepSeek V4.1 Flash, DeepSeek V4 Pro 0813, Kimi K3, MiniMax M3 and Nemotron 3.5 Lightning. On Bedrock, Claude Opus 5 and Opus 5.5 (EU and global profiles) and Sonnet 5.5 (global only, the one profile AWS publishes for it). - Model: Claude Haiku 5.5 on Anthropic and on Bedrock (EU and global profiles). It is priced by prompt length: a model call whose prompt passes 100K tokens pays 5× on every rate, input, output and cache. Each call of a tool loop is priced on its own prompt, and `GET /api/instances/models` returns the higher rates as `costLongPrompt`. - OpenAI GPT-5.4, GPT-5.6 and GPT-6 are priced by prompt length too: a call with more than 272K input tokens pays the long-context rates for the whole request. - AI providers and OpenAI-compatible embedders can be registered at boot (`registerAiProvider`, `registerEmbeddingProvider`) instead of edited into the maps every request reads. The embedder selector in an agent's settings lists what the server serves rather than a hardcoded pair. - `readFile` accepts `offset` and `limit` and returns that line range with the file's total line count, so an agent can read part of a large document without carrying the whole file through the rest of the turn. - Plugin manifests can declare runtime packages and environment values. The Docker build compiles plugins placed under `packages/engine/src/plugins/` and installs only the system dependencies those plugins request. - Plugin manifests accept optional `displayName` and `description`, and a `requiredSecrets` entry can carry a `label` and a `description`; the panel uses them to present the plugin and the fields its tools ask for. - The agent's Tools section lists enabled tools with their origin, opens each tool in a side sheet with the parameters it declares, and enables new ones from a picker. Hooks open in the same kind of sheet, with their event, timeout, order and parameters saved together. - The agent page explains what a capability still needs before it works, such as transcription credentials, instead of leaving it enabled and silent. - The Debug sheet can copy a complete captured turn — model payload and step trace — as one JSON object. - `POSTGRES_POOL_MAX` sets the main Postgres pool (default 10). `POSTGRES_ANALYTICS_POOL_MAX` (default 3) and `POSTGRES_ANALYTICS_STATEMENT_TIMEOUT_MS` (default 15 seconds) size the new analytics pool and bound its statements. ### Changed - **BREAKING — the LangSmith trace metadata key `oa_conversation_id` is now `conversation_id`.** Dashboards and filters built on the old key need updating. - **BREAKING — the engine and the panel run on Node 24**, the active LTS. The published images carry it; a deployment that runs from source or builds its own images must move to Node 24, and Node 22 is no longer tested. Under load the platform's overhead per turn fell sharply, because Node 24 no longer implements `AsyncLocalStorage` through promise hooks. - **BREAKING — GitHub, Render, HubSpot and Markdown-to-PDF tools moved out of core into plugins.** Their names are namespaced (`ghIssue` becomes `github:issue`, `hubspotContact` becomes `hubspot:contact`, and `markdownToPdf` becomes `extra:markdownToPdf`). Migration `0087` renames the catalog rows in place, so agents keep the tools enabled and skills keep their links and required-tool lists; an import of an older bundle maps the old names too. The tools work again once their plugin is installed ([polyant-hubspot-plugin](https://github.com/polyant-ai/polyant-hubspot-plugin) v1.1.1, [polyant-extra-plugin](https://github.com/polyant-ai/polyant-extra-plugin) v1.1.0); GitHub and Render have no public plugin. `verifyDocument` was removed without a replacement: the first boot removes it, and any other tool no plugin provides, from the agents that had it and logs their names. - **BREAKING — `fileUpload` moved out of core into the extra plugin** as `extra:fileUpload`. Migration `0089` renames the catalog row in place, so agents keep the tool enabled and skills keep their links and required-tool lists; an import of an older bundle maps the old name too. The tool works again once the extra plugin is installed ([polyant-extra-plugin](https://github.com/polyant-ai/polyant-extra-plugin) v1.1.0). - **BREAKING — Telegram and Slack now arrive through webhooks.** Telegram used long polling and Slack Socket Mode, so both worked without a public address. Now the engine registers `/webhooks/telegram/` with Telegram, and Slack must be pointed at `/webhooks/slack/` with Socket Mode switched off; the Slack app token is no longer used. The engine needs a public HTTPS address and `/webhooks/*` must reach it. - **BREAKING — attachment storage is configured per agent, and storing inbound files is opt-in.** The four `PLATFORM_S3_*` variables are gone; attachment persistence and `extra:fileUpload` share the agent's bucket, reached only with the agent's own `aws_access_key_id` and `aws_secret_access_key`. The bucket, region and keys are set on the agent's Advanced page, in a card that holds the **Store attachments** switch; an agent keeps the files its users send only while the switch is on. Migration `0088` adds the switch off for every agent, and the model sees each file in the turn either way. Stored files are deleted with their conversation and with the agent. Files earlier versions stored in the platform bucket stay there; the upgrade guide shows how to copy them. - **BREAKING for new agents — Web is a channel and starts off.** The agent's public HTTP endpoints (`POST /v1/chat/completions`, `POST /api/instances/:slug/chat/stream`) answer only while the agent's `web` channel is enabled (Channels section, `PUT /api/instances/:slug/channels/web`). With Web off they answer 404, the way a switched-off Telegram or Slack webhook does, and `GET /v1/models` with the agent's own key lists nothing. Switching Web on needs the agent's API key unless authentication is turned off. The panel's Playground talks to the new `POST /api/instances/:slug/playground/stream`, which uses the panel session and the new permission `agent.playground:use` on the agent, so it works with Web off and asks for no key. Every built-in role holds the permission, viewer included (migration `0092` grants it to existing roles). Migration `0091` switches Web on for every existing agent, with its `auth_enabled` as it was; bundles carry the `web` channel (format 1.3) and an older bundle restores the endpoint on. A2A keeps its own `a2a_enabled` switch. - A new agent now requires its API key (`auth_enabled` defaults to true, migration `0090`). It cannot be reached until a key is saved; the Web channel panel can generate one. Turning authentication off asks the reader to accept the risk first. Existing agents and imported bundles keep their current setting. - **The OpenAI tiers moved to the gpt-6 family** (`fast` → `gpt-6-luna`, `standard` → `gpt-6-sol`, `heavy` → `gpt-6-astra`). They pointed at `gpt-4o-mini`/`gpt-4o`, which OpenAI lists as deprecated. Every OpenAI agent without a pinned model, and its background jobs, lands on the new models; the deprecated ones stay in the catalog so a pinned agent keeps its costs priced. - The Anthropic `fast` tier runs Claude Haiku 5.5 instead of Haiku 4.5, with thinking switched off for the calls that do not ask for it (titles, memory, summaries). The `fast` tier runs the background jobs of every Anthropic agent, pinned or not. - Bedrock's default `standard` and `heavy` tiers no longer require Anthropic model access: they use Amazon Nova Pro and OpenAI gpt-oss 120B respectively. Nova keeps system-prompt caching without placing an invalid cache marker on tool messages. - New agents start with memory off and audio transcription disabled until they are configured (migration `0083`); existing agents keep their settings. - Prices corrected against the published pages: Claude Sonnet 5 is $2/$10 (it was recorded at $3/$15), the gpt-5.6 family came down, its cache writes are charged at the published 1.25× input rate, `o3`'s cached input is $0.50, and Bedrock `eu.*` Claude 4.5+ profiles carry the 10% regional premium, where none was modelled. Anthropic cache writes are priced by their TTL, and traces record five-minute cache-write tokens separately (migration `0084`). - A hook that returns `regenerate` or `injectContext` on a Room or webhook turn, where those controls are not honored, now logs a warning naming the hook and the dropped control. - The plugin SDK is pinned at v1.9.0. SDK 1.9.0 declares `ctx.artifacts` on `ToolContext`, so plugin tools read it from the SDK's own types; `put` and `take` may return a promise and must be awaited. The SDK's knowledge-access declarations (`requiredKnowledge`, `ctx.knowledge`) are not implemented by this engine: a plugin that declares them is loaded, but gets no knowledge base handle. - Runtime dependencies moved to NestJS 12, AI SDK 7, React 19.3, Vitest 5, dotenv 18, jsdom 30 and markdown-it 15. - Telegram webhook registration happens after the channel starts and is retried on rate limits and transient errors, so a failed registration no longer disables the channel. Telegram updates are acknowledged before the turn runs. - Conversations carry their message count, user message count and time of the last message. The conversation list, the conversation detail, the search and the analytics read them instead of counting messages, so their cost no longer grows with message history. On 2 million messages the analytics queries run 4 to 10 times faster. - Dashboards and other analytics reads run on their own connection pool, so a heavy aggregate no longer holds the connections a conversation turn needs. - Connections to MCP servers that do not use OAuth are reused across turns for up to a minute, instead of a new handshake on every turn. A connection that fails is retired and the next turn opens a fresh one. - A model call no longer reads the agent's row from the database: the agent's identity is cached for a minute and dropped when the agent changes. - The panel loads the changelog only for a Platform Admin, and loads the changelog card's markdown renderer only when the dialog opens. - Management keys are verified against a SHA-256 digest instead of bcrypt, which held the event loop for about 250 ms on every request a key authenticated. A key stored as bcrypt keeps working and is rewritten the first time it is used. - `/v1/chat/completions` answers 503 in OpenAI's error shape, with `code: missing_provider_key` and `x-should-retry: false`, when the agent has no API key for its AI provider, before any stream byte. Channels and the panel keep showing the warning. - With no language chosen, the panel starts in the browser's language when it is one the panel supports, and in English otherwise. A stored choice still wins. - Turning Knowledge on gives the agent the read-only knowledge tools (`searchKnowledge`, `getKnowledge`). Only the switch from off to on assigns them, so re-saving the agent does not bring back a tool its author removed. ### Removed - **BREAKING — deployment configuration was narrowed.** `AUTH_MODE`, `DEFAULT_INSTANCE_ID`, `AWS_REGION`, `PLATFORM_ADMIN_EMAIL`, `WORKSPACES_ROOT`, `DEBUG_LLM_PAYLOAD` and the unused `LANGSMITH_*` variables are no longer product configuration. - **BREAKING — ten environment variables that the panel already answers.** `DATETIME_TIMEZONE`, `DATETIME_LOCALE`, `DEDUP_SIMILARITY_THRESHOLD`, `MESSAGE_SOFT_DEBOUNCE_MS`, `MESSAGE_TYPING_DELAY_MS`, `MESSAGE_MAX_RESTARTS`, `KNOWLEDGE_MAX_DOCS_PER_INSTANCE`, `ANALYTICS_RETENTION_DAYS` and `SSE_MAX_CONNECTIONS_PER_USER` were the deployment default behind a value an administrator now sets per agent, per organization or per installation; the shipped defaults are unchanged and live beside the resolver that applies them. `PDF_CONCURRENCY` belongs to the Markdown-to-PDF plugin, which reads it itself. A deployment that still sets any of them is not failed, but the value has no effect. - **BREAKING — seven more environment variables become panel settings.** `SSE_MAX_CONNECTIONS`, `THROTTLE_TTL_MS`, `THROTTLE_LIMIT`, `AGENT_CALL_TIMEOUT_MS`, `MCP_CONNECT_TIMEOUT_MS`, `SCHEDULER_ORPHAN_GRACE_MS` and `SCHEDULER_DEFAULT_MAX_RUN_MS` are now rows in `platform_settings`, edited in Settings → General, with the same defaults as before. `THROTTLE_ENABLED` stays an environment variable: it exists for parallel local runs, and a rate limiter that can be switched off from inside the product is no use to a locked-out administrator. - The CDK stack no longer passes `DEFAULT_INSTANCE_ID` or `DATETIME_LOCALE` to the engine container, and sets the deployment's time zone as `TZ`. The `app` block of `config.yaml` keeps only `timezone`. - The seeded `demo-agent` (authentication off). Migration `0091` deletes it on a database where it was never used (no conversation, memory, knowledge document, scheduled task or secret, and its row never updated); a `demo-agent` that was used or edited stays and is treated like any existing agent. A fresh install has no agent until you create one. ### Fixed - The engine finishes shutting down while a panel holds an activity stream open. `SIGTERM` used to wait on that connection until the platform killed the process, so every deploy with a panel open waited out the stop timeout and lost the last buffered audit rows. Open streams are now ended first, and a request already in flight gets ten seconds to finish. Turns a Telegram, Slack, WhatsApp or event webhook has already acknowledged get the same ten seconds: their sender does not retry them, so cutting them off lost the reply. - A Telegram channel went silent after every rolling deploy: the replica being stopped deleted the webhook the new one had just registered. The webhook is now removed only when the channel is switched off or deleted, or its agent is deleted. - The CDK stack sends `/webhooks/*` and `/channels/http/*` to the engine without the OIDC sign-in. They fell through to the web panel, which answered 404, so inbound Twilio, email, HubSpot and HTTP-channel traffic never arrived on an ALB deployment. - A Room's prompt reaches the model. It was editable in the panel and stored, but only ever used for a token estimate. - A webhook event definition no longer drops a matching event because the model answered "Yes." or "Yes, …" instead of a bare "yes", and "si", "sì" or "y" count as a match only when they are the whole reply, so "Si tratta di…" no longer wakes the agent. The matcher sends the classifier the same projected payload the Room shows instead of the full JSON. - `spawnTask` can be enabled on an agent, and the sub-agents it starts run on the agent's provider and pinned model and stop when the turn is aborted. Its file did not match the tool loader's naming pattern, so it was never registered and sub-agents were unreachable. - An exported agent comes back as it left: the bundle carries the agent's thinking level and the values of tool and hook parameters declared not sensitive, which the import re-checks against its own registries. Credentials still travel as key names only. - `chat/stream` now aborts the pipeline when the client disconnects before the turn has produced anything (no text, reasoning or tool call yet): the model stops and the abandoned turn is not persisted. Once the turn has produced something it runs to its end and is saved, so a tool that may have written is never left unrecorded. - `chat/stream` stops relaying a turn when the client disconnects. It listened for the request's `close`, which Node emits once the body has been read, so a real disconnect was never seen. - A streaming `/v1/chat/completions` request the pipeline refuses is answered with its error status. The stream used to open first, so the refusal arrived inside a 200 already being read, and the response ended without `[DONE]`. - Switching thinking off now switches it off on models that reason by default — gpt-6 sol/luna and Claude Opus 5 and Sonnet 5 kept reasoning (and billing for it) when the parameter was simply omitted, on Bedrock as well as on the Claude API. Claude Sonnet 5.5 refuses the usual off-switch, so it gets `between_tools`, its lowest setting. - Claude Fable 5 offered a thinking toggle that did nothing: the model reasons on every call. The toggle is now locked on, as it already was for Fable 5.1. - The reasoning-level clamp could send `medium` to a model that does not accept it, and two capability checks looked a model up across providers by id alone. - An agent on a registered embedder was reported as missing credentials: the readiness check asked for the OpenAI key regardless of the embedder. - An unknown embedder name — from an import, or a stale row — fell through to OpenAI instead of failing; it now fails, and an import naming one is refused. - A multi-step model call that fails at a later step is logged with the tokens and cost of the steps it completed, which the provider billed; it was logged at zero. - The engine warns at boot for each retired environment variable still set, naming where its value is set now. - Scheduled tasks no longer remain permanently `running` after a deploy or crash, and one malformed tool-audit entry no longer blocks the rest of an agent's audit trail. - Scheduler startup recovery waits through each task's run deadline and does not overwrite an outcome already recorded by another process. - Agent bundles preserve each scheduled task's run deadline; event-source edits and token rotation return 404 for missing sources; inbound messages use default timings when their settings lookup fails. In-process plugin artifacts now have bounded size, lifetime, storage per conversation and aggregate storage, and a handle produced in a turn without a conversation resolves only for the agent that produced it. - A failed upload of an inbound attachment no longer loses the turn: the message and the reply are saved without the attachment's stored copy. - One management-audit row the database refused (an over-long target id, say) stopped every later write of the management audit until 500 newer rows pushed it out. Refused rows are now dropped one by one, and values are cut to their column width. - Tenant-scoped conversation and memory reads and mutations fail closed; unresolved request tenancy is refused instead of widening or silently losing a predicate. - Agent behaviour overrides are persisted and type-validated, and two concurrent fragments can no longer overwrite each other while a message burst is starting. - Attachment routes accept Express 5 wildcard segments, and filenames containing spaces, `#`, `?`, or harmless dot runs remain reachable. - An agent PATCH validates the provider and model the agent will run on together, whenever it changes either, so changing only the provider can no longer leave the agent pinned to a model of the previous one. - Signed-out pages can switch language, and organization members cannot accidentally remove their own membership. - MCP credential edits preserve omitted secrets, OAuth metadata receives the same URL validation as the server URL, A2A cancellation survives handler cache expiry, and the panel refuses to enable API-key authentication without a key. - `docker build -f Dockerfile.web` works again: the web build reads `CHANGELOG.md`, which the image never copied. - Telegram and Slack redeliveries of an update or event seen in the last hour are dropped instead of answered twice, and each bot's webhooks are rate limited on their own bucket instead of sharing one per client address. The CDK stack sets `TRUST_PROXY=1`, so the engine sees the caller's address behind the load balancer. The check is kept in memory in each process: with several replicas, a retry that reaches another replica is answered again. Run one replica, or accept the occasional duplicate. - The scheduler reaper fails only the run it judged overdue, never a newer claim of the same task. - A cron task that `scheduleTask` creates without a zone is stored with `UTC`, the default the tool describes, instead of running in the process zone. - `/v1/chat/completions` with `stream: true` handles a client disconnect like `chat/stream`: a turn that has produced nothing is aborted, a turn that has produced output still runs to its end and is saved, and a heartbeat keeps the connection open through long tool calls. - Analytics cost and token totals include what the provider billed for calls that failed after completing steps; call counts and response times still describe answered calls. - Amazon Nova on Bedrock is priced at the eu-south-1 rates. An `eu.*` inference profile is billed at the price of the Region that calls it, and the catalog carried the us-east-1 price, a cache read at a tenth of the input rate instead of a quarter, and a cache write AWS does not charge for. A warm-cache Nova Lite turn was recorded at a third of its cost. Nova Lite is now $0.096 / $0.384, Nova Pro $1.28 / $5.21, Nova Micro $0.056 / $0.224 and Nova 2 Lite $0.528 / $4.411 per million input / output tokens; Qwen3-Next 80B moves to its Milan price too. - A turn preempted by a follow-up message (cancel-and-restart) is logged with what its model calls were billed, as `outcome = 'aborted'`. It used to leave no row, so that spend was missing from every cost total. Cost and token sums count it; call counts, response times and error rates do not. - Embedding calls are logged with their tokens and cost, as `call_type = 'embedding'`. Knowledge ingestion, memory extraction and retrieval were billed by the provider and left no row. `text-embedding-3-small` is priced at $0.02 and Titan Text v2 at $0.023 per million tokens; a registered OpenAI-compatible embedder is logged with no cost. - Cost totals are summed in double precision. The `real` column was summed as a `real`, so a total over many rows lost precision: a million rows of $0.000123 summed to $122.19 instead of $123.00. - The model call behind the prompt-section tools counts in its conversation's cost, like the other service calls of the turn. - An import normalises the web context field mapping as the panel does, and refuses an invalid one before writing anything. - `readFile` caps `tail` at 500 lines like other windows, and its refusal of a file over 512 KB no longer suggests a range read that would also be refused. - A variable a plugin declares in `system.env` no longer overrides the same variable set in the operator's container environment. - `drizzle.config.ts` lists every schema file again; `drizzle-kit push` is not a supported path and the config says so. - A failed load of Settings → General shows the reason with a retry instead of a skeleton. - A Claude turn on Anthropic or Bedrock that ran three or more tool steps no longer fails. Each step added a cache breakpoint and kept the previous ones, so the request passed the provider's limit of four: Bedrock refused it with a 400 (the caller saw a 500) and Anthropic dropped the newest. The within-turn breakpoint now moves to the latest step. - A scheduled run that finishes after the reaper failed it no longer clears the claim of the run that replaced it, nor deletes a one-shot task under it. A replica that claims a task another replica has just run no longer runs it a second time. - On shutdown the schedulers stop first, so no task starts during the ten-second HTTP grace period to be cut off by the exit, and a step that fails (a trace-store flush against a database already gone, say) is logged and no longer keeps the process alive until the platform kills it. - A WhatsApp or Telegram message sent while the previous answer was being cancelled is answered together with the earlier fragments in one reply. When the cancelled run took longer than the debounce window to stop, the contact got one reply to the new fragment and another to the old ones. - A Telegram reply in flight when the channel is saved is still delivered, and Telegram file downloads stop after 30 seconds and refuse files over 20 MB. - The engine warns at boot when a `PLATFORM_S3_*` variable is still set: 1.2.0 no longer reads them. - The upgrade guide's pre-step for migration `0086` built an index on `conversations.last_message_at`, a column 1.1.x does not have, so it failed. It now builds only the `conversation_messages` index ahead. - Claude Sonnet 5.5 cache reads cost $0.10 per million tokens on Anthropic and on the Bedrock global profile, not $0.20. - Claude models with adaptive thinking (Opus 4.7 and later, Sonnet 5 and 5.5, Haiku 5.5, Fable) show their reasoning again. They return it with empty text unless a summary is requested, so the panel, the logs and the stored turn had none although the reasoning was billed; the gateway now asks for the summary on Anthropic and on Bedrock. - GPT-5.6 Sol is priced at its full $5 / $30 rather than the promotional $4 / $20, so a spend cap does not loosen when the promotion ends. - A skill whose required tool comes from a plugin is offered to the model again. Its namespaced name was compared with the name the tool is given to the provider, so every such skill was left out of the prompt. - An agent configured with its own Bedrock access-key pair signs its chat and embedding calls with that pair even when `AWS_BEARER_TOKEN_BEDROCK` is set on the host; the deployment's token used to win. - Text document uploads, knowledge imports and agent imports accept JSON bodies up to 50 MB. They were refused above 100 KB, so an export larger than that could not be imported back. Every other route keeps the 100 KB limit. - A request body over its size limit is answered 413, and one of an unsupported content type 415, instead of 500. - A stored attachment is read and deleted in the bucket it was written to. After an agent's bucket changed, older files could not be opened and a deleted conversation left them in the old bucket while counting them as removed. Files stored without a recorded bucket still use the agent's current bucket. - Knowledge documents left uploading or processing by a process that died are failed within minutes, at boot and periodically after. A restart within five minutes used to leave them looking busy for good. - The live conversation view shows a turn that starts seconds after the previous one was stored, instead of hiding it until it finished. - A model call aborted by its caller's deadline is logged as an error of kind `timeout` rather than as a user preemption. - Voice notes transcribed by Deepgram Nova-3 ($0.0043 per minute) and Amazon Transcribe ($0.0001667 per second) are priced; they were logged at $0. - Conversation history replays a tool that moved into a plugin under the name it has now, not its old flat name. - The playground's error messages, its copy button and the badge of a transcribed voice message follow the panel's language. - Messages that sent people to "an administrator" name the Organization Owner and Admin, and a Platform Admin where one is the right person. - A management key whose id is not a UUID is refused before the lookup instead of reaching Postgres as an error. - Changing an MCP server's address to another host no longer reuses its stored credential: saving or testing it there answers 400 until the credential is entered again. Anyone allowed to edit MCP servers could otherwise send the stored token to a host of their choosing. ### Security - Each Telegram channel has its own random webhook secret, stored with the channel and independent of the bot token. The engine mints it and registers the webhook when the channel starts. - Message text, reply previews, contact names, the memory extractor's unparseable replies and the full error of a failed Telegram or outbound send are logged at debug level only; the normal levels keep the error message and stable ids. - Closed cross-tenant keyword search and mutation paths by making tenant scope explicit, branded and fail-closed throughout the affected stores. - Disabled skills can no longer resolve their retained credential placeholders; rotating bearer values no longer mint an unlimited series of throttle buckets; MCP OAuth endpoints use the shared SSRF denylist. - Request-controlled channel identifiers are sanitized before logging, unknown imported channel types are rejected safely, and raw provider response bodies are no longer written to logs. - Updated undici to 8.11.2 and 7.30.0 for its open security advisories. The Next.js, Sharp, js-yaml, Multer and qs updates already shipped in 1.1.2. - Updated axios to 1.20.0 for twelve high-severity advisories in 1.19.0. - The engine and web images run as the unprivileged `node` user instead of root, and declare a health check (`/health` on the engine, the sign-in page on the web). A volume mounted on the engine's `/app/logs` or `/app/packages/engine/workspaces` must be writable by uid 1000. ## [1.1.2] - 2026-09-24 Security patch release. ### Security - Library updates for security reasons, including Next.js 16.3.6 in the admin panel. ## [1.1.1] - 2026-09-17 Patch release. It restores agent creation, which fails on every attempt in 1.1.0. ### Fixed - **Creating an agent no longer fails.** `POST /api/instances`, and the admin panel's New Agent button behind it, answered 500 on every call in 1.1.0: the create path seeds its four tables inside one transaction, but the tool seed issued its insert on a second pooled connection, which cannot see the agent row that transaction has not committed, and the insert died on `instance_tools_instance_id_fkey`. Every read and write on the create path now runs on the caller's transaction. The skill seed carried the same split — it is inert on this build, where the default skill list is empty, and is corrected with it. ### Changed - `recomputeInstanceTools` accepts an optional executor. Given one it reuses that transaction instead of opening a second; called without one, as the import paths do after their own transaction closes, it behaves as before. Internal module, not a documented SemVer surface. ## [1.1.0] - 2026-09-04 > **Upgrading from 1.0.0 needs operator action** — this release is not a rolling > upgrade, is forward-only past migrations `0071` and `0076`, and requires forcing > every user to sign in again. See > [docs/UPGRADING.md](https://github.com/polyant-ai/polyant/blob/main/docs/UPGRADING.md). ### Added - **MCP client**: an agent can equip tools from external Model Context Protocol servers, configured per agent, with `none` / `static` / OAuth 2.1 auth modes. Credentials are encrypted at rest and never returned by the API. New tunable `MCP_CONNECT_TIMEOUT_MS` (default 10000) bounds the per-server connect so one slow server cannot stall a turn. - **A2A server**: an agent can be exposed over the Agent2Agent protocol (Agent Card + JSON-RPC). Opt-in per agent, off by default. - **Tenant-scoped frontend URLs**: admin routes live under the workspace segment, `GET /api/me` reports the caller's tenancy, and navigation is scope-aware. - **Every agent section is addressable from the sidebar.** The agent detail page dropped its nested tab row: each section is a sidebar row under the same headings, and `?tab=` remains the address. Twenty-three destinations became eighteen, and the workspace-wide conversations, saved memories and run log gained a per-agent view. - **MCP servers are their own section** (`?tab=mcp`) rather than a block at the tail of the Tools page. - A scheduled task can be created disabled: `POST` accepts `enabled` (default `true`), so a schedule can be staged without a window in which it can tick. - **WhatsApp channels can authenticate with a Twilio API Key** instead of the account Auth Token. Twilio signs inbound webhooks with the Auth Token only, so a channel in that mode receives messages on a dedicated webhook URL carrying a server-generated secret, revealed and rotatable from the admin panel. First released in 1.0.1; included here. - **Retention covers every traffic-driven table.** `tool_audit_logs`, `hook_executions`, `scheduled_task_runs` and the completed half of `event_backlog` now age out on the same policy as `ai_logs` and `pipeline_traces`; the four are also what the heaviest analytics aggregations read. Conversation messages, memories and the knowledge tables are deliberately excluded — ageing product data out is an operator's decision, not a housekeeping job's. - **A failed provider call is recorded.** A turn that died at the provider used to write no `ai_logs` row at all, so an agent with an expired key looked exactly like an idle one. Migration `0077` adds `outcome` and `error_kind`, classified into a closed set (`auth`, `rate_limit`, `bad_request`, `overloaded`, `timeout`, `unknown`); the provider's own message is never stored, because it can quote the request and the request is the prompt. - **The switch that closes an agent's HTTP surface is back in the panel.** `authEnabled` defaults off, so a new agent answers `/v1/chat/completions` with no credential — the Status page reported this correctly, but the control it pointed at had been removed, leaving a hand-written `PATCH` as the only way to close an open agent. ### Changed - **BREAKING — `users.role` is dropped.** Migration `0076` reconciles the flag from the role column one final time — any row promoted by a direct `role` update that never touched `is_platform_admin` is brought into agreement first — then drops the column, so no account silently loses its standing. **There is no rollback past this migration**: the column is gone, and older code that still selects `role` fails on every read of the users table. - **BREAKING — `users.is_platform_admin` is the sole authority for platform-admin standing**, read from the database on every request instead of carried on the session. Promoting or revoking an account now takes effect within the platform-admin cache's five-minute window, without requiring the account to sign out and back in. - **BREAKING — `POST`/`PATCH /api/users` take `isPlatformAdmin: boolean`** and no longer return `role`. `role` is still accepted on input for one release, as a deprecated alias for both legacy spellings (`platform_admin` and `superadmin`), and is never persisted or echoed back. - **BREAKING — RBAC is enforced unconditionally.** The `AUTHZ_ENFORCE` environment variable is gone; there is no shadow mode. An undeclared route or a failed permission check is a 403 with no way to turn it off. Installations that copied the previous sample `.env` were running with every permission check reduced to a no-op. - **BREAKING — `AUTH_MODE=alb-oidc` is not compatible with enforced RBAC.** A gateway-forwarded identity carries no organization and holds no role bindings, so it is denied on every management route. The engine now refuses the combination at boot instead of 403-ing at runtime. Use `AUTH_MODE=session` until the gateway-identity mapping exists. - **BREAKING — membership is granted deliberately, never by signing in.** A new user is provisioned with no organization membership; an administrator must assign a role before they can reach anything. - **BREAKING — the platform-admin role value is `platform_admin`** (was `superadmin`). Reads accept both spellings; writes emit only the canonical value. Migration `0071` rewrites the persisted value. **Forward-only** — see the upgrade guide. - **BREAKING — Member can now configure an agent end to end**, credentials included: migration `0072` grants `agent.secret:read`, `agent.secret:write` and `agent.export:read` to the Member role in every existing organization. Secrets remain write-only through the API (reads return key names only), but this is a real privilege expansion applied on upgrade. - **BREAKING — frontend URLs are tenant-scoped** and the workspace URL segment is authoritative. Pre-existing bookmarks to flat paths (`/instances`, `/conversations/`, `/playground`, …) and to legacy `?tab=` values no longer resolve; navigate from the organization dashboard instead. - **BREAKING — an empty tool set means no tools.** `instance_tools` holding no rows used to be read as "enable everything", so an agent with every switch off — or one seeded before the tool catalogue synced — ran with the whole registry, `httpRequest` and the file tools included. An agent's tool set is now exactly what is stored, on both the runtime and the secret-prompting side. **Audit any agent whose tool rows are empty before upgrading**: it loses the tools it was silently running. - **BREAKING — `POSTGRES_SSL` is parsed strictly.** `true` and `false` are the only accepted values and anything else — `1`, `TRUE`, `require`, `yes` — fails the boot; an empty environment variable now means unset everywhere, for every optional variable. The old coercion treated any non-empty value as true, so `POSTGRES_SSL=false` switched TLS *on*. This bites before the migrations run — audit it first. - **BREAKING — `GET /api/tools`** now requires `ORG_READ` instead of `TOOL_READ`: an agent-scoped principal that could previously list the registry now receives 403. - **BREAKING — `GET /v1/models`** returns an empty list, rather than every active agent, for a principal whose organization cannot be resolved. An integration that enumerates models to discover a slug sees `[]` instead of an error. - **A new agent's prompt sections are seeded empty.** The seven rows still exist, so the panel has somewhere to write; only the default prose is gone, so an agent's behaviour no longer comes from text its author never read. - **Credentials offers every provider**, not just the ones an agent already runs on, so a key can be entered before the agent is switched to it. LangSmith stays the exception, beside the tracing switch that reveals it. - Membership grants and revocations, platform-admin bootstrap, and the `/api/users` mutations are now recorded in the management write-audit log. - **The engine runs on AI SDK 7.** `usage` is now the across-steps cumulative total, which is what feeds cost estimation and the per-model cache rates, and the prompt-cache marker moved onto `instructions` because v7 rejects a `system` message inside `messages`. No configuration changes. - **Outbound HTTP that carries the SSRF-pinned dispatcher goes through one place.** The engine moves to undici 8, whose `Agent` Node's bundled fetch refuses outright — the three call sites that passed a dispatcher to the global fetch would have silently lost DNS pinning. - The engine lints on ESLint 10; the admin panel stays on 9, which `eslint-config-next` still requires. Relative import extensions are enforced per package rather than by a repo-wide rule that was half wrong. ### Fixed - `/v1/chat/completions` reports real token usage instead of zeros. - The live activity SSE feed is scoped to the caller's organization, and its teardown no longer leaks a heartbeat timer plus a bus subscription when the connection drops mid-write. - CodeQL findings closed: log injection, a file-read TOCTOU, and a prototype-pollution-prone key write. Third-party actions are pinned. - **A revoked platform admin kept a full authorization bypass for up to five minutes.** The platform-admin cache had a 5-minute TTL and was never invalidated; every write of the flag now clears it. - **A duplicate, undecorated `InstanceSkillsController` is gone.** It served `POST`/`DELETE` on `api/instances/:slug/skills` with no authorization decorator at all, so under the previous shadow mode any authenticated user could enable or disable skills — and with them the tool surface and prompt — on any agent. - Cross-tenant boundaries closed on attachments, agent creation and listing, memory writes, agent-to-agent handoffs and the sidebar; the organization filter fails closed, a platform admin outranks every organization role, and a route whose authorization is not RBAC is no longer denied outright. - Only one migrator runs at a time (session-level advisory lock), and a journal entry that sorts before an already-applied one is no longer skipped in silence. - A hook that ran out of time no longer affects the turn: the deadline aborts a signal, late writes are refused by a fenced state view, and a control return that arrives post-abort is dropped. - An MCP server URL is validated when it is dialled, not only when it is saved, and an imported server is not treated as a trusted one. - An A2A task belongs to the agent it was created on, and the JSON-RPC endpoint is bounded. - Enabling a tool the catalogue has lost no longer answers 200 with the change undone: the mirror is repaired where registry and catalogue disagree. - The admin route group serves its own 404, the sidebar stays inside the caller's tenant, navigation links no longer misfire while the tenancy loads, and an anonymous visitor keeps their query string across the login bounce. - Resources acquired by the engine are released on every path. - **An instance bundle can no longer introduce a channel credential.** Import strips credential-like keys the way export already did, so a crafted bundle cannot arrive carrying a webhook secret and have the channel enabled with it. - **WhatsApp media downloads only send Twilio credentials to Twilio.** The `MediaUrl` on an inbound message is attacker-influenced; the first hop is now checked against Twilio's API hosts, regional data-residency hosts included, before the Basic credential is sent. - **A Room event-source webhook token now requires write permission to read.** It lets its holder inject events into an agent, so a read-only role could previously go from "can look" to "can drive". Its list response no longer carries the token; a dedicated endpoint reveals it. - Credential-bearing webhook paths are redacted before they reach a log line even when the path is percent-encoded or differently cased, and request-controlled values are stripped of line breaks so they cannot forge additional log records. - `X-Forwarded-Proto` is clamped to `http`/`https` when the webhook URL is reconstructed, so a crafted value can neither corrupt the signature check nor reach the log. - **Rotating a WhatsApp inbound secret can no longer be undone by a concurrent save.** The carry-forward reads under a row lock inside the same transaction as the write; previously a save that started before a rotation could commit the old secret back, reviving it while the audit log said it had been retired. - Destroying a WhatsApp inbound secret is audited, both when a credential-mode switch discards it and when the channel is deleted — so the audit trail no longer records only the mints and rotations. - **A throttled route no longer buckets every caller together.** Buckets were keyed by `req.ip`, and in the standard topology the panel proxies `/api/*` to the engine from one container: five wrong passwords in a minute locked sign-in for the whole deployment, while an attacker shared — and hid in — the same budget. The bucket is now the account for a credential form and the session for an authenticated caller. - **A prerelease engine version no longer fails every plugin's engine gate.** SemVer excludes a prerelease from a range that carries none, so the first build tagged with any suffix would have skipped every third-party plugin with a warning — boot green, agent silently without its plugin tools and hooks. The comparison now reads major, minor and patch and ignores the suffix. - **`PATCH /api/users/:id` rejects a non-boolean `isPlatformAdmin`** instead of coercing it. `"off"` is truthy in JavaScript and false in PostgreSQL, so the last-platform-admin guard saw no demotion while the database performed one, leaving a deployment with zero platform admins and no way back except SQL. - **Creating an agent and patching its tool set are each one transaction.** A failure between the four writes used to commit an agent with no prompt sections or no tool rows — which the runtime reads as "exactly zero tools" — and nothing repaired it, while the taken slug answered the operator's retry with a 409. - **Two statements that failed outright above a few hundred agents are gone.** The boot-time tool-catalogue sync and agent deletion each bound one parameter per row and crossed PostgreSQL's 65 535-parameter limit: the first threw inside the boot transaction so the catalogue never synced, the second made a channel agent with tens of thousands of conversations undeletable through the API. - The per-turn round trips that built each `ask_` tool are batched, the dashboard aggregation is capped, and migration `0075` indexes conversations by `updated_at` so the organization-wide list stops sorting every row the tenant owns to return twenty. ### Security - **Untrusted text is fenced wherever it enters a prompt.** Nonce-tagged delimiters existed in one file; the webhook engine's substituted values and the channel display name went in plain. A display name carrying a newline and a forged closing tag injected instructions at *higher* trust than the user's own message, and in the webhook case they persisted into every later turn. - **A sensitive skill environment variable never reaches the model.** `readSkill` — one of two tools enabled by default on every agent — interpolated the decrypted value into its result, so the credential entered the model's context, the persisted conversation and `tool_audit_logs`. It now emits a placeholder that resolves inside the tool call, and the plaintext exists nowhere else. - **One log serializer, and it no longer writes what the policy forbids.** The file logger serialized errors with `JSON.stringify`, which drops the non-enumerable `message` and `stack` and keeps the custom fields — where pg, the AWS SDK and fetch put connection strings, request configs and authorization headers. A single provider 401 wrote the upstream bearer token and the folded prompt into a log file. Authorization, API-key, token, secret, password, credential, connection-string, cookie and private-key fields are now redacted at any depth, and tool output is capped rather than copied whole into the audit table. ## [1.0.2] - 2026-08-26 ### Fixed - The Speech-to-Text provider setting now accepts an explicit "Disabled" value. An unrecognised or unset provider previously fell back silently to OpenAI Whisper, so an operator who wanted voice messages turned off had no way to say so, and audio replies could fail looking for credentials that were never configured. Choosing "Disabled" now returns a plain "voice messages are not supported" reply instead. ## [1.0.1] - 2026-08-25 > Released from `main` as a hotfix on 1.0.0. Its changes are also present in > 1.1.0, which carries them forward. ### Added - WhatsApp channels can authenticate to Twilio with a revocable API Key instead of the account Auth Token. Twilio signs inbound webhooks with the Auth Token only, so an API Key channel receives messages on a dedicated webhook URL carrying a server-generated secret, which can be revealed and rotated from the admin panel. - The WhatsApp channel has its own configuration card in the admin panel, with a credential-mode selector and the webhook URL to paste into the Twilio Console. ### Changed - Channel configuration is persisted exactly as its schema validates it, so credentials pasted with surrounding whitespace are trimmed and keys outside the validated shape are no longer stored. - The management API writes only known channel configuration keys and ignores unrecognised fields in a request body. - Twilio Account SIDs are validated for format when a WhatsApp channel is saved. ### Security - Webhook paths that carry a credential are redacted before being written to logs, covering both the WhatsApp inbound webhook secret and the Room event-source webhook token. - Inbound WhatsApp webhook requests that fail before authentication all return one identical response, so an anonymous caller cannot enumerate agent slugs or determine which credential mode a channel uses. - Request-controlled values are stripped of line breaks before being written to a log line, so they cannot introduce additional log records. ## [1.0.0] - 2026-08-05 ### Added - First public open-source release of Polyant, with a Supervisor runtime for configurable AI assistants, automatic long-term memory, and multi-channel delivery. - Telegram, Slack, WhatsApp, webhooks, and an OpenAI-compatible HTTP API, alongside multi-instance administration and conversation inspection. - Encrypted per-instance secrets, plugins and Markdown skills, Room automation, and runtime analytics. - Bounded agent-to-agent handoffs, live activity, configurable web search, and structured tool-secret inputs. ### Changed - Semantic Versioning now defines the public compatibility contract for the documented OpenAI-compatible API, Plugin SDK and manifest, and documented configuration and migration behavior. - Conversation traces preserve per-step reasoning and tool metadata; incoming message fragments can safely cancel and restart an in-flight run. ### Fixed - Memory deduplication now honors its configured similarity threshold, and Google OAuth remains optional when its credentials are absent. - Delegated sub-agents cannot recursively spawn further sub-agents. - Node.js 22 is aligned across the supported development and container environments. [Unreleased]: https://github.com/polyant-ai/polyant/compare/v1.3.5...HEAD [1.3.5]: https://github.com/polyant-ai/polyant/compare/v1.3.4...v1.3.5 [1.3.4]: https://github.com/polyant-ai/polyant/compare/v1.3.3...v1.3.4 [1.3.3]: https://github.com/polyant-ai/polyant/compare/v1.3.1...v1.3.3 [1.3.1]: https://github.com/polyant-ai/polyant/compare/v1.3.0...v1.3.1 [1.3.0]: https://github.com/polyant-ai/polyant/compare/v1.2.0...v1.3.0 [1.2.0]: https://github.com/polyant-ai/polyant/compare/v1.1.2...v1.2.0 [1.1.2]: https://github.com/polyant-ai/polyant/compare/v1.1.1...v1.1.2 [1.1.1]: https://github.com/polyant-ai/polyant/compare/v1.1.0...v1.1.1 [1.1.0]: https://github.com/polyant-ai/polyant/compare/v1.0.2...v1.1.0 [1.0.2]: https://github.com/polyant-ai/polyant/compare/v1.0.1...v1.0.2 [1.0.1]: https://github.com/polyant-ai/polyant/compare/v1.0.0...v1.0.1 [1.0.0]: https://github.com/polyant-ai/polyant/releases/tag/v1.0.0