## Summary
- give locked Threads and Learning a shared conversion-focused layout
- add streamlined Loom demos, setup and engineer CTAs, and detailed
capability sections
- remove the disabled Threads preview so both locked experiences use the
same full-page treatment
## Why
Users without Intelligence enabled should immediately understand what
each feature provides and have clear paths to configure it or talk with
the team.
## How
- render both gates through one responsive locked-feature surface
- use minimal Loom embeds with feature-specific videos
- align content on a single centered rail with theme-aware styling
- add product-specific copy, capability icons, accessibility labels,
telemetry coverage, and fixture assertions
- verify with `pnpm nx run @copilotkit/web-inspector:test` and `pnpm nx
run @copilotkit/web-inspector:check-types`
<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit
* **New Features**
* Added unified locked views for Threads and Learning with feature
videos, capability outlines, setup guidance, and a “Talk to an Engineer”
call to action.
* Added responsive and dark-theme styling for locked-feature overviews.
* Updated self-hosted and setup messaging.
* **Bug Fixes**
* Prevented unavailable Threads content, examples, controls, and
metadata actions from appearing.
* Removed legacy runtime entitlement diagnostics from the interface.
* Standardized locked-state telemetry and CTA behavior.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
`CopilotKitIntelligence` performed no validation on `apiKey`. It assigned the
value and sent it verbatim as a Bearer credential, so a blank key produced
`Authorization: Bearer ` and surfaced much later as a 401 that named nothing.
`apiKey: string` is required on the config type, so TypeScript catches a missing
property. It does not catch an empty one, and the shape that actually happens is
a `process.env` read TypeScript is told to trust: `?? ""` in the starter wiring
block, `!` in this file's own JSDoc examples. Both yield a blank key when the
variable is unset.
Throw at construction instead. Every caller builds the client during boot, so the
error lands at startup rather than on a user's first message. The message names
`CPK_INTELLIGENCE_API_KEY` and `copilotkit project select`, and echoes none of the
key value — the rule `parseProjectIdFromApiKey` already follows for a malformed
key.
This mirrors `configuredUrl`, which already treats a blank `apiUrl`/`wsUrl` as
unset for the same `?? ""` reason.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
## Release channels v0.9.2
**Scope:** `channels` | **Bump:** `patch`
---
### How this release process works
1. **This PR was created automatically** by the "release / create-pr"
workflow.
It bumped the `channels` packages to `0.9.2`
and generated AI-enhanced release notes.
2. **CI runs on this PR** — the full test suite (unit tests, lint, type
checks, build)
must pass before merging. This is the review gate.
3. **Review the release notes** in `release-notes.md` in this PR.
If a Notion draft was created, you can edit the release notes there
before merging.
4. **When this PR is merged**, the `release / publish` workflow
automatically:
- Builds all packages
- Publishes the `channels` packages to npm at version `0.9.2`
- Creates git tag `channels/v0.9.2`
- Creates a GitHub Release with the final release notes
### Before merging
- [ ] CI is green (tests, lint, types, build)
- [ ] Version bumps look correct
- [ ] Release notes are accurate (edit in Notion if a draft was created)
---
> **Do not merge until CI is fully green.** The full test suite runs
automatically on this PR.
## What does this PR do?
Hooks can now expose a frontend tool to browser agents through the
WebMCP browser API, next to the normal agent registration. Set `webmcp:
true`, or pass `{ annotations }` for WebMCP hints:
```ts
useFrontendTool({
name: "searchOrders",
description: "Search the signed-in user's orders by status",
parameters: z.object({ status: z.enum(["open", "shipped", "delivered"]) }),
handler: async ({ status }) => searchOrders(status),
webmcp: { annotations: { readOnlyHint: true } },
});
```
How it works:
1. `FrontendTool` in `@copilotkit/core` gains the `webmcp` option. A new
`WebMCPRegistry` registers the tool on `document.modelContext` with its
name, description, input schema, and annotations. `execute` runs the
tool's own handler. The handler context has no `agent` there.
2. Every tool registry change in `RunHandler` reconciles the WebMCP
registrations. The same availability rules apply as for the agent tool
list. Removing a tool aborts its registration signal, and the browser
then unregisters it.
3. Each adapter picks the option up from core: v2 `useFrontendTool`
(React, Vue, React Native), the v1 `useCopilotAction` and
`useFrontendTool` wrappers (React, Vue), and Angular's
`registerFrontendTool`. Where WebMCP is not available (SSR, React
Native, browsers without the API), registration is a no-op.
The `webmcp` prop is documented on the React, Vue, and Angular reference
pages in shell-docs.
## Related PRs and Issues
- None.
## Checklist
- [x] I have read the [Contribution
Guide](https://github.com/copilotkit/copilotkit/blob/master/CONTRIBUTING.md)
- [x] If the PR changes or adds functionality, I have updated the
relevant documentation
- [ ] "Allow edits by maintainers" is checked (lets us help iterate on
your PR directly — faster turnaround for everyone)
## Testing
**Commands run**
- `pnpm nx run-many -t check-types
--projects=@copilotkit/core,@copilotkit/react-core,@copilotkit/vue,@copilotkit/angular`
— all pass.
- Full test suites: core (829 tests), vue (103), and angular pass.
react-core passes standalone (1589 tests). Under the lefthook pre-commit
hook, react-core flakes on pre-existing e2e tests (A2UI, MCP Apps) that
do not touch this code. Those tests pass when run alone.
**Manual test**
Requires Chrome 149+ with the WebMCP origin trial, or the testing flag.
1. Enable `chrome://flags/#enable-webmcp-testing`, then relaunch Chrome.
2. In an app that uses CopilotKit, register a tool with `webmcp: true`.
3. Run `await document.modelContext.getTools()` in DevTools. The tool is
listed with its schema and annotations.
4. Unmount the hook. Run the command again. The tool is gone.
**How this PR makes testing easy**
The behavior has automated tests on this branch:
- `packages/core/src/core/__tests__/run-handler-webmcp.test.ts` — 15
tests with a `document.modelContext` stub: registration, annotations,
unregistration, availability rules, name collisions, stale-rejection
races, and handler execution.
-
`packages/react-core/src/v2/hooks/__tests__/use-frontend-tool-webmcp.test.tsx`
and the mirrored
`packages/vue/src/v2/hooks/__tests__/use-frontend-tool-webmcp.test.ts` —
pass-through, re-registration, and agent-scoped cases at the hook level.
- `packages/vue/src/hooks/__tests__/use-frontend-tool-webmcp.test.ts` —
reactive `webmcp` getters through the v1 Vue API.
## Risk / rollback
Low. The feature is opt-in per tool. Without `webmcp`, no code path
changes. Where WebMCP is unsupported, registration is a no-op. Revert
this PR to roll back.
## Public API change
New optional `webmcp` prop on frontend tool registrations. Existing call
sites do not change.
**Before**
```ts
useFrontendTool({
name: "searchOrders",
description: "Search orders by status",
parameters: z.object({ status: z.string() }),
handler: async ({ status }) => searchOrders(status),
});
```
**After**
```ts
useFrontendTool({
name: "searchOrders",
description: "Search orders by status",
parameters: z.object({ status: z.string() }),
handler: async ({ status }) => searchOrders(status),
webmcp: { annotations: { readOnlyHint: true } },
});
```
<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit
* **New Features**
* Tools can now be exposed to browser agents through WebMCP.
* Added support for custom annotations and automatic parameter schema
generation.
* WebMCP registrations stay synchronized as tools are added, removed,
enabled, or updated.
* Available across Angular, React, and Vue tool APIs.
* WebMCP reuses existing handlers and safely does nothing when
unavailable.
* **Documentation**
* Added usage guidance and examples for configuring WebMCP-enabled
tools.
* Documented that WebMCP invocations do not include an agent context.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
#6818 renamed the Intelligence docs from `/premium/` to `/intelligence/`
and left the published packages alone. This cleans them up.
## The console notice
`@copilotkit/shared` printed this to every developer who used
`useCopilotChatHeadless_c` without a license key:
> To enable this **premium** feature, add your public license key…
> To learn more about **premium** features, read the documentation here:
docs.copilotkit.ai/**premium**/overview
"Premium" is not a tier we have. The notice now uses the sentence the
Headless UI docs page already uses — "Headless UI requires a CopilotKit
Intelligence license key" — so a developer who reads the notice and then
opens the docs finds the same words.
## One link was broken, not just stale
The "Show me how" button on the missing-public-API-key error opened
`/premium/overview#getting-access`. That heading was deleted on
2026-06-16 in 449237af0c, so the redirect carried the fragment to a page
that has no such anchor and the button landed at the top. It had been
doing that for two and a half months. It now points at
`#plans-and-access`, the section that answers how to get a key.
This was not in the ticket. It came out of tracing the links rather than
replacing them.
## The rest
Stale but working links move from `/premium/*` to `/intelligence/*` in
`react-core`, `web-inspector` and the runtime skill reference (two
byte-identical copies). Every one of them resolved through the redirects
from #6818; each cost a hop and named a retired tier. Four test files
assert these hrefs, so they move with the strings.
Not touched: `seo-redirects.ts` in `showcase/` keeps `/premium/`, since
those entries are what makes the old links work. `user_type: 'premium'`
in the provider examples is invented customer metadata, not a tier.
## Verification
- `nx run-many -t check-types,test` for `shared`, `react-core`,
`web-inspector`, `runtime`, `runtime-client-gql`: all green (17, 33,
152, 138 and 5 test files)
- On a cold worktree, `nx affected` first reported
`runtime-client-gql:check-types`, `runtime:check-types`, `runtime:test`
and `runtime:generate-graphql-schema` as failures. Cause: `check-types`
runs `tsc` concurrently with the `build` that generates
`src/graphql/@generated/*`, so the first run type-checks against files
that do not exist yet. Running `build` first makes all of them pass, and
Nx labels the same four as flaky. Unrelated to this change.
## Blocked on the docs promote
`docs.copilotkit.ai` has not been promoted to production since #6818
merged. Right now production serves `/premium/*` directly with a 200 and
returns 404 for every `/intelligence/*` path. Staging serves the new
paths with a 200 and redirects `/premium/overview` to
`/intelligence/overview` in one hop, so the code is right and the deploy
is pending.
CodeRabbit flagged this and it is correct. Promote `shell-docs` to
production before the next monorepo release, or these links 404 for
anyone who clicks them. Merging is safe on its own, because the packages
only reach users through a release.
## Companion change
The Intelligence repo carries the same rename for the CLI onboarding
prompt and the verify hint: CopilotKit/Intelligence#1117. The two ship
independently.
Refs OSS-1085
The Headless UI console notice told developers about "premium features" and
pointed at /premium/overview. The tier is called CopilotKit Intelligence now, so
the notice named a product that no longer exists. It now uses the same sentence
the Headless UI docs page uses.
The docs links in react-core, web-inspector and the runtime skill reference move
from /premium/* to /intelligence/*. They worked through the redirects added in
#6818, but each cost a hop and carried the old name.
One of them was broken, not just stale: the "Show me how" button on the missing
public API key error opened /premium/overview#getting-access. That heading was
deleted on 2026-06-16 in 449237af0c, so the button had been landing at the top
of the page for two and a half months. It now points at #plans-and-access, the
section that answers how to get a key.
Tests assert these hrefs, so they move with the strings.
Refs OSS-1085
## Release angular v0.5.0
**Scope:** `angular` | **Bump:** `minor`
---
### How this release process works
1. **This PR was created automatically** by the "release / create-pr"
workflow.
It bumped the `angular` packages to `0.5.0`
and generated AI-enhanced release notes.
2. **CI runs on this PR** — the full test suite (unit tests, lint, type
checks, build)
must pass before merging. This is the review gate.
3. **Review the release notes** in `release-notes.md` in this PR.
If a Notion draft was created, you can edit the release notes there
before merging.
4. **When this PR is merged**, the `release / publish` workflow
automatically:
- Builds all packages
- Publishes the `angular` packages to npm at version `0.5.0`
- Creates git tag `angular/v0.5.0`
- Creates a GitHub Release with the final release notes
### Before merging
- [ ] CI is green (tests, lint, types, build)
- [ ] Version bumps look correct
- [ ] Release notes are accurate (edit in Notion if a draft was created)
---
> **Do not merge until CI is fully green.** The full test suite runs
automatically on this PR.
An agent had no trustworthy way to learn who was speaking. The actor reaches
it only inside a context entry — prose in the prompt — so an agent that needs
to act as that person has to read the sentence and copy the id into a tool
argument, which makes the model the source of an identity and lets message
text influence whose account gets used.
The actor now travels in `forwardedProps.channelActor` on every run of a turn,
including a resume, where a resume carries whoever pressed the button rather
than whoever started the turn. The Thread supplies it from its own ingress, so
a caller cannot pass an identity of its own choosing.
AG-UI messages carry an optional author and putting it there would have been
the smaller change, but that field is part of the message list handed to a
model provider and providers validate it: OpenAI accepts only `[A-Za-z0-9_-]`
in a message author, Teams actor ids contain a colon, and adapters bound an
actor id at 512 characters and nothing else. Sanitizing an id to fit is worse
than failing, because a mangled id still reads as an identity and answers as
the wrong person. `forwardedProps` never enters the conversation, so no
provider inspects it and the value stays what the adapter reported.
`id` is provider-scoped, so `platform` travels with it — a consumer keying
anything per person keys on both. A turn that named nobody carries no
`channelActor` key at all rather than one holding an empty string.
An empty user id passed the truthiness guard, so the post went out to this
turn's own recipient and reported success for a recipient the caller never
identified. Comparing the requested id with the recipient directly refuses it
alongside a request naming somebody else.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The comment described an http/https/mailto/tel allowlist, but ui/open-link uses a
denylist (javascript:/data:/vbscript:/blob:/file:). Align the comment with the
actual contract so it does not mislead a future change to the scheme policy.
- Add e2e tests pinning the ui/initialize contract (the compile-time tie to the
spec): a well-formed initialize returns the host context and the negotiated MCP
Apps protocol version; an initialize missing required fields (e.g.
appCapabilities) is rejected with -32603; a widget sending a different
protocol-version string gets the host's MCP Apps version back, not its own
echoed. (2025-06-18 is a base-MCP-protocol version, independent from the MCP
Apps protocol 2026-01-26; it is what the old hand-rolled host hardcoded.)
- Nit: load the bridge via `import(...).catch(rethrow)` with inferred types
instead of `typeof import(...)` annotations, removing three
consistent-type-imports warnings.
- Nit: restore the "ui/message: No agent available" warning log on the no-agent
path, for parity with the hand-rolled host and the oncalltool guard.
## Release monorepo v1.70.0
**Scope:** `monorepo` | **Bump:** `minor`
---
### How this release process works
1. **This PR was created automatically** by the "release / create-pr"
workflow.
It bumped the `monorepo` packages to `1.70.0`
and generated AI-enhanced release notes.
2. **CI runs on this PR** — the full test suite (unit tests, lint, type
checks, build)
must pass before merging. This is the review gate.
3. **Review the release notes** in `release-notes.md` in this PR.
If a Notion draft was created, you can edit the release notes there
before merging.
4. **When this PR is merged**, the `release / publish` workflow
automatically:
- Builds all packages
- Publishes the `monorepo` packages to npm at version `1.70.0`
- Creates git tag `monorepo/v1.70.0`
- Creates a GitHub Release with the final release notes
### Before merging
- [ ] CI is green (tests, lint, types, build)
- [ ] Version bumps look correct
- [ ] Release notes are accurate (edit in Notion if a draft was created)
---
> **Do not merge until CI is fully green.** The full test suite runs
automatically on this PR.
One `@bot say hi` in a Slack channel produced about fifty identical
replies in eight seconds. The loop is in shared ingress code, so it
reaches any Channels app on Slack, not just the one that found it.
## What happens
`message_changed` does not mean somebody edited the text. Slack also
sends it when a reply lands in the message's thread, when a link
unfurls, and when attachments are added.
Every one of those carries a fresh `revisionId`, and that is exactly
what the engine's inbound dedup keys on — deliberately, so a real edit
is never swallowed as a duplicate. So each announcement arrived as a
fresh mention and started another turn: the bot answered, the answer
revised the parent message, the revision asked the same question again,
and the next answer revised it again.
Instrumenting one mention showed eleven turns reaching the app. One was
the original message. Ten were revisions of it.
## The fix
A revision is a turn only when its text actually differs.
`previous_message` is the direct evidence where Slack sends it. Where it
does not, `edited` stands in: Slack stamps that only when a person
edited the message, and the metadata-only revisions that cause the loop
have neither. Edited mentions still reach the app, deletes are
untouched, and the normalizer stays pure — so this covers the
Intelligence-side webhook ingress that shares it as well as the local
adapter.
## Verification
- `packages/channels-slack`: 414 tests pass, three of them new.
- Each new test fails without the guard (checked by reverting it).
- Found and confirmed against a live Slack workspace, running an OpenTag
Composio integration. After the fix: one mention, one agent run, one
reply.
The app that hit this carries a local workaround — it ignores any turn
that is not the first revision of a message — which it can drop once
this ships.
## What changed
- Add standalone `CPK_TELEMETRY_ID` support to Runtime v1 and v2.
- Keep telemetry opt-out, sampling, Segment, and legacy license fallback
behavior.
- Fetch structured Intelligence entitlements and map them to current
client status.
- Share concurrent entitlement lookups, retry short-lived failures, and
reject stale grants.
- Make managed React, Angular, and Vue thread UIs use Runtime
entitlement authority.
- Keep assistant feedback stable when unrelated Inspector settings
change.
- Update Runtime, telemetry, self-hosting, and Web Inspector docs.
## Why
Managed Intelligence projects use a project API key for product access
and a non-secret telemetry ID for attribution. Offline license tokens
remain a self-hosted entitlement concern.
Starter-template and AgentCore changes live in #6188.
## Companion PRs
- Starter templates: #6188
- CopilotKit/Intelligence#628
- CopilotKit/oss-path-to-production#226
## Review corrections
- Scope shared entitlement attempts to one API key and endpoint.
- Ignore stale attempts after credentials change.
- Bound retries after short denials and transport failures.
- Accept telemetry IDs only when they match the public identifier
contract.
- Read Inspector context in its button, so unrelated label changes do
not rerender assistant feedback.
## Validation
- React Core full suite: 1,537 Vitest tests and 47 script tests passed.
- Runtime, Core, Shared, Angular, Vue, and Web Inspector focused suites
passed.
- React Core typecheck and build passed after the final rebase.
- Direct builds and type checks passed for Angular, Core, Runtime,
Shared, Vue, and Web Inspector.
- Shell docs typecheck and production build passed.
- Changed Vue files passed ESLint.
- `git diff --check` passed.
<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit
- **New Features**
- Added structured runtime entitlement support for managed and
self-hosted deployments.
- Feature access and usage limits now reflect active entitlements, with
legacy license compatibility.
- Added runtime entitlement diagnostics to the Inspector’s Threads view.
- Added runtime-scoped telemetry identities and configurable telemetry
ID support.
- **Bug Fixes**
- Licensing interfaces remain in a loading state during retryable
entitlement outages.
- Improved recovery after runtime connection, target, or transport
changes.
- Prevented stale entitlement data from granting access after refresh
failures.
- **Documentation**
- Documented entitlement statuses, telemetry identity precedence,
sampling, and Inspector telemetry behavior.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->