Ships the four sdk-python fixes merged since 0.1.94 (2026-06-04), none of
which are in any published artifact — the newest upload of any kind is the
0.1.95a4 prerelease from 2026-06-19, which predates all of them.
Closes#6231
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
## Summary
- `CopilotChat` fingerprints messages for `useMemo` with `contentKey =
0` for any non-string / non-array content.
- `ACTIVITY_SNAPSHOT` updates often keep the same `messageId` and only
replace object `content` (progress / generative UI). That left activity
renderers stuck on the first frame until some other list change forced a
refresh.
- Include `JSON.stringify(m.content)` for object content in the
fingerprint. Multimodal attachments remain on the array-length branch,
so this does not reintroduce base64 serialization for user uploads.
- Add a regression test that replaces the same activity `messageId` and
asserts the UI updates.
## Test plan
- [x] `nx run @copilotkit/react-core:test` (or the package's activity
e2e suite)
- [x] Manual: stream repeated `ACTIVITY_SNAPSHOT` with the same
`messageId` + `replace: true` and confirm custom activity renderers
update each frame
Made with [Cursor](https://cursor.com)
Renders CopilotKitProvider from the built `/v2` entry and reads
`useLicenseContext` from the built `/v2/context` subpath — the exact
consumer wiring that was broken. On a pre-fix build it asserts
`expected 'null' to be 'valid'`, reproducing the reported symptom of a
license-gated feature never activating.
Skipped (loudly) when no built dist is present: nx `test.dependsOn` is
`^build`, so this package's own build is not guaranteed to have run.
The hard gate remains scripts/context-singleton-preflight.mjs, which
runs as part of `build`.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
`src/v2/context.ts` was compiled into two independent bundles. The build
that emits `dist/` (entries `src/index.tsx` + `src/v2/index.ts`) inlined
it into the shared chunk, while a second build emitted the standalone
`dist/v2/context.*`. Nothing linked them, so `createContext()` ran twice
and the package shipped two distinct React contexts.
`CopilotKitProvider` lives in the shared chunk, so it published to the
inlined copy. Anything importing from `@copilotkit/react-core/v2/context`
read the orphaned copy that no provider ever populated, and so saw the
defaults forever: `useLicenseContext().status` stayed `null` even when
`/info` reported `licenseStatus: "valid"`, permanently disabling
license-gated features such as `useThreads`. `CopilotKitContext` was
duplicated the same way, so `useCopilotKit` imported from that subpath
threw "must be used within CopilotKitProvider".
The headless build already externalized the module for exactly this
reason; the plugin was simply never applied to the `dist/` build. Hoist
it and apply it there too. The UMD builds stay self-contained by design.
Compounding this, `src/v2/providers/index.ts` enumerates its exports by
name and omitted `useLicenseContext`, so the live copy had no public
import path at all and consumers had no correct alternative. Export it.
Add a build-time guard, because this class of bug is invisible to every
existing gate: tsc, vitest (which imports source, where only one module
exists), publint and attw were all green while the published package
shipped two contexts. The guard fails against the real published 1.66.4
dist and passes on this build.
Broken since c3c30969e4 (2026-05-06), the commit that introduced the
split — not a 1.66.x regression.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Final review fix wave for the RN render-tool convergence branch.
Substantive:
- Extend packages/react-core/scripts/assert-headless-purity.mjs to also scan the
built /v2/context chunk (context.mjs/context.cjs), not just /v2/headless.
/v2/context carries CopilotKitCoreReact and is imported by react-native, so a
future shiki/mermaid/katex leak through it would bloat RN bundles (#4893) while
neither hard-fail guard fired. Comment and failure message updated to name both
RN-reachable entries. Mutation-verified against context.mjs.
- Document the closure-staleness convergence: render is now captured at
registration (passed into useFrontendTool) and only refreshed when deps change,
no longer re-read every render. Consumers whose render closes over changing
state must now pass deps. Documented in the useRenderTool JSDoc, the
useRenderTool.mdx reference, and the changeset migration notes.
Minor sweep:
- CopilotChat extraData now lists what renderItem actually reads
({ isRunning, renderToolCall, toolMessages }); drop unused executingToolCallIds.
- headless-type-exports.test-d.ts imports React explicitly instead of relying on
the ambient UMD global.
- useRenderTool.mdx migration heading no longer names the uncut 1.67.0 version.
- Changeset marks @copilotkit/react-core minor (new public type export), matching
its body.
Co-Authored-By: Claude <noreply@anthropic.com>
The RN headless entry now re-exports useRenderToolCall (render-tool
convergence), so the measurement's pending-state comment no longer applies.
Add the symbol back to the measured import surface and drop the TEMPORARY
note. Reported size is essentially flat (92.8 -> 92.7 kB gzip): the hook
reuses the renderer registry/resolver useRenderTool already pulls in.
Co-Authored-By: Claude <noreply@anthropic.com>
useRenderTool.mdx and CopilotKitProvider.mdx described the removed React
Native render-tool registry as if it still existed. Correct the three
false statements on useRenderTool.mdx (cleanup unregisters the renderer;
RenderToolProvider auto-installed; useRenderToolRegistry live Map) and the
false CopilotKitProvider bullet (wraps children in a RenderToolProvider).
Document useRenderToolCall for rendering on any surface, note that
arguments stream on status "inProgress", and add a migration section for
the removed useRenderToolRegistry. Also fix the stale Callout that listed
useRenderToolCall as not exported on React Native.
Co-Authored-By: Claude <noreply@anthropic.com>
Assertions originally written by David McKay in PR #6346, re-driven through
CopilotKitCoreReact's registry instead of a mocked local one.
Co-Authored-By: David McKay <davidmckayv@users.noreply.github.com>
BREAKING CHANGE: useRenderToolRegistry and RenderToolProvider are removed.
Render tools now register into CopilotKitCoreReact.renderToolCalls, the same
registry react-core uses, so a React Native-specific registry and its provider
no longer exist. Use useRenderToolCall() to render a registered component
anywhere in your app, including non-chat surfaces, and delete RenderToolProvider
from your tree — CopilotKitProvider no longer installs it and nothing needs it.
Co-Authored-By: Claude <noreply@anthropic.com>
- SeededAgent.run now declares Observable<BaseEvent> return (via
ReturnType<AbstractAgent["run"]>) so check-types passes; throw-only body
avoids a runtime rxjs import the RN bundler cannot resolve.
- TestCopilotKit accepts optional executingToolCallIds.
- Restore executing-status, empty-string-args, and unrepairable-JSON-args
coverage as tests driven through the real CopilotKitCoreReact.
Co-Authored-By: Claude <noreply@anthropic.com>
Small docs clarification for the Channels SDK.
- **Channels overview** (`/channels`): adds a short note under the
architecture diagram that you can build your own channel runner on the
open-source SDK, with no CopilotKit Intelligence dependency — a
supported path where the team owns state, persistence, concurrency,
locking, retries, and race-condition handling. Intelligence remains the
managed runner; analytics, learning, and governance come in addition.
- **Self-hosting section**: Enterprise Intelligence can be fully
self-hosted today; onboarding guides are still to come, and we're happy
to help in the meantime.
- **Package READMEs** (`channels`, `channels-core`, and the five
adapters): removes the "there is no standalone / DIY runner" phrasing,
which contradicted the above, in favor of the same managed-vs-custom
framing.
Intentionally does **not** add any implementation guidance for custom
runners (no state-store interface details, no persistence examples).
Follow-up (not in this PR): swap in the updated architecture diagram
assets (light + dark) that show the "Build Your Own Channel Runner" box
alongside Enterprise Intelligence.
Refs
[FAC-155](https://linear.app/copilotkit/issue/FAC-155/channels-docs-lack-a-clear-supported-path-without-intelligence)
🤖 Generated with [Claude Code](https://claude.com/claude-code)
The prior comment framed useRenderToolCall's absence from the size probe's
import list as a permanent design fact ("DOM-dependent … would fail the
build"). That is the same permanent-sounding rationale that let the stale
claim in src/index.ts go unrevisited for months. The export is missing only
because RN's headless entry does not export it yet; the render-tool
convergence on this branch adds it. Reframe the comment as TEMPORARY — REVISIT
so a future engineer restores the symbol and re-baselines the number once RN
headless exports it. Comment-only; measured size unchanged at 92.8 kB.
Co-Authored-By: Claude <noreply@anthropic.com>
react-native was missing from static_bundle_size.yml's package glob, so its
dist/ has never been measured despite being the consumer most exposed to the
#4893 regression (Metro does not tree-shake). This adds coverage:
- Extend the compressed-size-action glob to include react-native.
- New scripts/measure-headless.mjs: an esbuild-driven gzip signal for the
@copilotkit/react-native/headless entry, mirroring react-core's
measure-copilotchat.mjs (stdin + resolveDir, gzip sum, job-summary output).
- Wire a build + measure step into the copilotchat-import-size CI job.
First baseline: @copilotkit/react-native/headless = 92.8 kB gzip
(esbuild regression signal, not a Metro figure).
No limit fields (Phase 1 policy — see dev-docs/bundle-size.md).
esbuild added as a react-native devDependency (^0.27.0, matching react-core);
the root ">=0.25.4" override keeps the monorepo on a single esbuild (0.27.3).
Two corrections to the drafted script, verified by running it:
- Fed the entry via esbuild stdin with resolveDir=pkgRoot; a temp-dir entry
cannot resolve @copilotkit/react-native/headless through workspace node_modules.
- Dropped useRenderToolCall from the import list — the RN headless surface
deliberately does not export it (DOM-dependent; see src/index.ts).
Co-Authored-By: Claude <noreply@anthropic.com>
New export from Figma: two runner paths (managed Intelligence runner or
build your own), durable-data emphasis, Any Agent framework list, and
channel platforms with the +4 more row. Used for both themes until a
dark export exists.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Drop the defensive parentheticals around Intelligence pricing; say
"available on a free plan" and move on.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Note in the Channels overview and package READMEs that building your own
channel runner on the open-source SDK primitives is a supported path with
no CopilotKit Intelligence dependency; teams choosing it own their state,
persistence, concurrency, locking, retries, and race-condition handling.
Intelligence remains the managed runner, with analytics, learning, and
governance in addition.
Also updates the production self-hosting note: Enterprise Intelligence can
be fully self-hosted today, onboarding guides are still to come.
Refs FAC-155
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Four defects in `useCopilotReadable` were introduced together in 80dffec4e7
(v1.50.0), when the hook was repointed from the v1 context tree onto the v2
flat context store. The pre-1.50 implementation was correct on every count.
- `available` was missing from the effect's dependency array, so toggling it
between "enabled" and "disabled" after mount did nothing. It is back in the
deps, alongside the restored `available = "enabled"` default.
- `convert` was invoked as `convert(value)` against its declared
`(description, value)` signature, so a user's function received the value as
`description` and `undefined` as `value`. The branches are now split, because
`JSON.stringify(description, value)` would treat the value as a replacer.
- The `dependencies` positional argument was destructured but never reached the
deps array, matching the pattern already used by
`useCopilotAdditionalInstructions`.
- The `found` dedup branch is removed. It compared a raw value against a stored
entry whose value had already been serialized by `addContext`, so it never
matched and its early return was unreachable. Repairing the comparison rather
than deleting it would newly let one component's unmount remove a context
entry another component still depends on.
`parentId` and `categories` are marked @deprecated: both were dropped from the
hook body by the same commit but left in the options type and the JSDoc, which
advertised a nested-context feature that has been a no-op since v1.50.0. The
JSDoc example is rewritten to document behavior that actually exists. Real
hierarchy support needs parent/child modelling in the v2 context store and is
tracked separately.
Adds the hook's first test file: 12 tests, five of which fail against the
pre-fix implementation.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-authored-by: jwgrsol <wefhio1985@gmail.com>
## What does this PR do?
Adds the CopilotKit consumer side of ENT-1173 across Shared, Runtime,
Core, Web Inspector, and the existing Shell Docs pages.
- Defines and parses optional trusted Inspector metadata for identity,
plan, license, action, usage, and expiry. Runtime proxies it through a
private, failure-isolated route, and Core refreshes it without changing
connection state.
- Groups Inspector navigation into Threads, Agents, and Learning.
Threads renders finite, unlimited, unknown, overage, and expiring usage
states plus matching trusted plan or license actions.
- Keeps explicit `threadEndpoints` as the only authority for Thread
requests. Locked or absent capability states make no list, subscription,
detail, message, event, or state calls.
- Keeps the zero-thread video, three example Threads, detail tabs, and
guided tour in empty and locked states. General Intelligence remains the
default onboarding path; only trusted `team_self_hosted` metadata uses
self-hosted onboarding.
- Gives an active license with missing Runtime routes a short **Finish
setting up Rich Threads** state. Users can copy a safe coding-agent
prompt or open the public Runtime setup guide. The same copy control
appears in that guide, and raw Markdown/LLM views include the full
prompt.
- Keeps finite usage green below 90%, orange from 90% to the limit, and
red at or above the limit. At 90%, a trusted plan action changes from
**Manage Your Plan** to a purple **Upgrade Your Plan** without changing
its trusted URL, action kind, or telemetry contract.
- Adds a deterministic 33-state loopback lab for CopilotKit developers.
It has no production route or export, is absent from public docs and
package metadata, and is excluded from the npm tarball.
`Expiring Soon` is display-only; this PR does not enable the thread
culler. Managed Enterprise receives no manage-plan action, and Team
Self-Hosted receives no hosted plan action. Optional metadata and the
additive expiry field remain compatible across mixed producer, Runtime,
Core, and Inspector versions.
A small Channels test-only change updates fetch mocks for current
TypeScript types. It changes no Slack or Teams docs or runtime behavior.
## Related PRs and issues
- Refs
[ENT-1173](https://linear.app/copilotkit/issue/ENT-1173/ship-plg-ready-inspector-navigation-metadata-and-locked-threads)
- Producer:
[CopilotKit/Intelligence#696](https://github.com/CopilotKit/Intelligence/pull/696)
## Validation
- `@copilotkit/web-inspector`: 20 files and 372 tests passed; typecheck
and production build passed.
- Shell Docs: 57 files and 383 tests passed; lint, typecheck, and
production build passed. The build generated all 222 static pages.
- Browser checks cover the copy-prompt flow, unchanged white **Manage
Your Plan**, purple **Upgrade Your Plan**, orange 4,500/5,000 usage, and
red 5,000/5,000 usage.
- Independent review found no Critical or Important issues.
- The broader Runtime, React Native, Channels, package-quality,
compatibility, and Node-version checks from the prior pushed head remain
green.
## Checklist
- [x] I have read the [Contribution
Guide](https://github.com/copilotkit/copilotkit/blob/master/CONTRIBUTING.md)
- [x] I updated the relevant documentation
- [ ] "Allow edits by maintainers" is checked
## The bug
`agentIdFromUrl` in `src/app/api/copilotkit/[[...slug]]/route.ts` only
read the target agent from the URL **path** (`/agent/:agentId/run`).
Thread routes carry it in the **query string** instead
(`/threads?agentId=<id>`), so every thread-list request looked
agentId-less and fell through to `defaultSkinId`'s `identifyUser` —
banking's.
That split identity for every non-default skin:
| path | resolves via | result |
| --- | --- | --- |
| `POST /agent/people/run` | `people`'s resolver ✅ | thread created
under `rowan-demo-user` |
| `GET /threads?agentId=people` | **banking's** resolver ❌ | asks for
`northwind-demo-user`, gets `[]` |
## Why it was hard to see
Nothing errors. The thread rail just reads *"No conversations yet"*
forever and reopening a conversation after a reload is impossible —
which reads to a viewer as "this product doesn't persist threads", the
exact opposite of what the demo exists to prove. **Banking was immune
only because it IS `defaultSkinId`.**
## The fix
Fall back to `?agentId=` from the query string when the path carries no
`/agent/<id>` segment.
## Verification
Against a local Intelligence stack, thread counts from `GET
/api/copilotkit/threads?agentId=<id>`:
| skin | before | after |
| --- | --- | --- |
| people | 0 | 11 |
| airline | 0 | 1 |
| logistics | 0 | 3 |
| banking | 7 | 7 (unchanged) |
Confirmed the backend held those threads all along — they were being
listed under the wrong end-user id.
Skins with no `identifyUser` (airline) still fall through to
`genericIdentity()` via the existing `if (!resolve)` guard, so this
widens correct resolution without adding a failure mode.
`pnpm build`, `pnpm lint`, `pnpm test:unit` (335/335) green.
## Skill-staleness check
Per the app's standing rule: **checked, no skill impact.** This is
shell-internal identity plumbing; `.claude/skills/reskin/` documents the
`identifyUser` contract, which is unchanged — a skin still contributes
the same resolver in the same place.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Rowan is a People Operations command center and the second skin built against
the full nine-beat bar in `.claude/skills/reskin/demo-beats.md` (banking was the
first). Pages: Roster (index), Compensation, Requests, Onboarding.
REST-backed like banking and logistics: `/api/people/v1/*` serves one `ledger`
snapshot read plus the write paths, a generated `offer-letter` PDF, and a
presenter-gated `dev/reset`. Components read the ledger through the skin's own
`usePeopleLedger()` context, so `useData` is omitted. That context is mounted in
`RuntimeProviders` rather than `Providers`, which lets the single fetch also feed
`useRuntimeProperties`.
The signature element is the band ladder: one rail per level, each normalized to
its OWN band, so "halfway up L3" and "halfway up L7" line up at the same height
and become comparable. Anyone outside their band is drawn outside the rail, in
the negative colour, always labelled.
Beats, all walked in a browser against a live Intelligence stack:
1 face showCompBands renders the ladder + a two-sentence answer
2 rich thread gen-UI replays intact on reopen after a hard reload
3a drive the app setBaseSalary — the figure is typed into a chat card and
goes straight to REST; it appears nowhere in the transcript
3b sees screen route readable + per-page on-screen readables on all four
pages; Roster and Requests give different, correct answers
3c levers HITL confirm naming the levers, then
?status=pending&sort=aging_desc&top=10 with the Status, Sort
and Show controls visibly tinted, "TOP 10 OF 11"
3d multimodal an offer-letter PDF rides the pill, and the filed packet
survives deleting the thread and reloading
4 memory seeded preference recalled AND named in the component's
`note` slot
5 stored skill one vague sentence fires three visible writes in order, no
confirmations, amid four distractor tools
6 teach a skill 422 OUT_OF_BAND (symptom only) -> decline -> record the
demonstration -> save -> apply unaided to a DIFFERENT person
in a fresh thread
Notes for reviewers:
- The beat-6 gate is deliberately discriminating. Decoy exception codes file and
finalize successfully and still do not lift it, and an unknown code is refused
without enumerating the catalogue — so "the agent filed an exception" is not
the same as "the agent cleared the gate". Two out-of-band comp requests are
seeded so the case taught on stage and the unaided replay are different people.
- Seed dates are relative offsets materialized at store init, not absolute ISO
strings, so request aging and the generated offer letter stay coherent years
from now and a Reset genuinely re-freshens the queue.
- Memories are seeded and saved at `user` scope, not `project`. Verified against
the running stack: a project-scoped row is returned for EVERY user id in the
instance, so with several skins sharing one backend it is not a per-skin
boundary. For the same reason this skin's `forgetAllMemories` skips
project-scoped rows rather than deleting data it does not own, and `dev/reset`
reports the skipped count.
- `temperature` is not set. gpt-5.4 rejects it and the value is discarded, so
carrying it alongside a comment claiming determinism would be misleading.
- Beat 2 additionally requires the thread-list identity fix sent separately; the
skin merges and runs fine without it, it just cannot demo thread reopen.
Docs updated for the fifth skin per the app's standing skill-staleness rule:
CLAUDE.md (skin list, substrate split, beat matrix), README.md,
docs/teach-mode/README.md (teach-mode is now per-skin, not banking-only), and
`.claude/skills/reskin/{SKILL,demo-beats,templates}.md` — including six
"only banking does this" claims that are no longer true.
Verified: pnpm build, pnpm lint, pnpm test:unit (335/335) on this base.
Co-Authored-By: Claude <noreply@anthropic.com>
`agentIdFromUrl` only read the target agent from the URL PATH
(`/agent/:agentId/run`). Thread routes carry it in the QUERY STRING instead
(`/threads?agentId=<id>`), so every thread-list request looked agentId-less and
fell through to `defaultSkinId`'s `identifyUser` — banking's.
The result was a split identity for every non-default skin: runs created threads
under the skin's own end-user id (the run path resolves correctly), while the
list asked for banking's id and got an empty array back. The thread rail read
"No conversations yet" forever and reopening a conversation after a reload was
impossible.
Nothing errored, which is what made it hard to see — and it reads to a viewer as
"this product doesn't persist threads", the opposite of what the demo exists to
show. Banking was immune only because it IS `defaultSkinId`.
Verified against a local Intelligence stack; thread counts returned by
`GET /api/copilotkit/threads?agentId=<id>` before → after:
people 0 → 11
airline 0 → 1
logistics 0 → 3
banking 7 → 7 (unchanged; it was already resolving correctly)
Skins with no `identifyUser` (airline) still fall through to `genericIdentity()`
via the existing guard, so this widens correct resolution without introducing a
new failure mode.
Co-Authored-By: Claude <noreply@anthropic.com>
Fixes#261 (open since March 2024). Supersedes #4622 — @ashish4143
diagnosed the same wrappers and is credited as co-author on the commit.
## The bug
`CopilotSidebar` wraps consumer content in two divs:
- `.copilotKitSidebarContentWrapper` (`Sidebar.tsx`) — only sets
`overflow`, `margin-right`, `transition`
- `.copilotKitModalChildrenWrapper` (`Modal.tsx`) — **has no CSS rule
anywhere in the repo**
Both are auto-height blocks, so a child's `height: 100%` has no definite
containing block to resolve against and collapses to content height.
## The fix
An opt-in `fullHeightChildren` prop on `CopilotSidebar` that adds a
modifier class to the content wrapper. Two deliberate choices, both from
the review on #4622:
- **Opt-in, not default.** The content wrapper wraps the *entire*
consumer app. Making it a fixed-height flex column for everyone would
reflow apps that never asked for it.
- **A viewport unit, not `height: 100%`.** `100%` only resolves if every
ancestor (`html`/`body`/`#root`) also declares a height — react-ui
neither sets that nor can guarantee it, so `100%` would silently no-op
in a stock Next.js app. `min-height: 0` on the children wrapper clears
the flex-item `min-height: auto` floor so tall content scrolls inside
the child rather than stretching the wrapper past the viewport.
```tsx
<CopilotSidebar fullHeightChildren>
<div style={{ height: "100%" }}>...</div>
</CopilotSidebar>
```
## Testing
**Unit** — `packages/react-ui/src/css/sidebar-full-height.test.ts` (4
tests), in the repo's existing CSS-contract style. Guards both halves:
the escape hatch's rules, and that the default wrapper stays
auto-height. Also asserts the height is *not* `100%`, since that's the
regression that would make the whole feature a silent no-op.
```
✓ src/css/sidebar-full-height.test.ts (4 tests)
Test Files 9 passed (9) Tests 58 passed (58) # full react-ui suite
```
`npx tsc --noEmit` → exit 0. `oxlint` on changed files → 0 warnings, 0
errors.
**Live in Chrome** — the acceptance criterion from the #4622 review: a
stock app where **nothing** declares a height on `html`/`body`/`#root`,
loading the real built `dist/index.css` (not the source CSS), standards
mode, 762px viewport. DOM per `Sidebar.tsx:92` + `Modal.tsx:143`.
| case | child `height:100%` measures |
|---|---|
| default (no opt-in) | **17px** — collapsed, i.e. behavior unchanged
for existing consumers |
| `fullHeightChildren` | **762px** — exactly the viewport |
| `fullHeightChildren`, content 3000px tall | **762px**, scrolls inside
the child (`min-height: 0` holds) |
Also confirmed on the opt-in path: `.copilotKitSidebar` stays `position:
fixed`, and the expanded push-aside `margin-right` is still `448px`
(28rem), so the sidebar's own layout is untouched.
**Docs** — `CopilotSidebar.mdx` is auto-generated from `Sidebar.tsx`;
regenerated via `scripts/docs/gen.ts` and committed only the new
`fullHeightChildren` entry (the generator also surfaces unrelated
pre-existing drift in other reference pages, left out of this PR).
## Not covered
The issue mentions a "works in Safari, not Chrome" symptom. I verified
in Chromium only — the mechanism above is spec behavior rather than a
Chrome quirk, but I haven't measured WebKit.
Fixes#5961 (OSS-609).
## The bug
Both LangGraph auth pages told self-hosted readers to wrap their graph
in `CopilotKitRemoteEndpoint`. That path is retired and fails two ways
against the stack the reporter used (`copilotkit==0.1.94`,
`ag-ui-langgraph==0.0.4x`, Python 3.12):
```
import LangGraphAgent -> ImportError: cannot import name 'LangGraphAgent' from 'copilotkit'
execute_agent -> AgentExecutionException: Agent 'sample_agent' failed to execute:
'LangGraphAGUIAgent' object has no attribute 'execute'
```
(Reproduced locally against the `langgraph-fastapi` example's venv —
output above is verbatim.)
## The fix
`showcase/shell-docs/src/content/docs/auth.mdx` (Self-hosted tab of the
`auth_pattern: langgraph` section) and
`showcase/shell-docs/src/content/docs/integrations/langgraph/auth.mdx`
now document the supported pattern: **serve the AG-UI endpoint
yourself**, validate in a FastAPI dependency (401 before the graph
runs), and bake the resolved user into a **per-request**
`LangGraphAGUIAgent(config={"configurable": {"auth_user": user}})` so
nodes read an already-verified identity off `RunnableConfig` — no raw
token in the graph, no shared agent carrying another request's identity.
The gate-only variant (`FastAPI(dependencies=[Depends(current_user)])` +
`add_langgraph_fastapi_endpoint`) is documented for readers who only
want unauthenticated traffic rejected.
Two adjacent bugs on the same pages, fixed here because they break the
same walkthrough:
- **The frontend channel was wrong.** The pages said to pass
`properties={{ authorization: userToken }}` and claimed it "is forwarded
as a Bearer token". Nothing in `packages/` converts properties into
headers — `properties` reach the agent as AG-UI `forwardedProps` (run
payload data). The v2 runtime *does* forward the inbound `authorization`
header (and custom `x-*`) onto the agent call, so `headers={{
Authorization: ... }}` is the channel that actually works, for both
Platform and self-hosted.
- **The Platform user key was wrong.**
`config["configuration"]["langgraph_auth_user"]` →
`config["configurable"]["langgraph_auth_user"]` (matches
`langgraph/pregel/main.py` and `langgraph_api/worker.py`).
## Testing
**1. Doc snippets extracted verbatim from the MDX and executed** (a
script pulls the `main.py` + node code blocks out of each page, stubs
only `validate_your_token`, and drives them with `TestClient`; run under
the `examples/integrations/langgraph-fastapi` venv — `copilotkit
0.1.94`, `ag-ui-langgraph 0.0.41`, Python 3.12):
```
# docs/auth.mdx
PASS no header -> 401 {"detail":"Missing bearer token"}
PASS bad token -> 401 {"detail":"Invalid token"}
PASS valid token -> 200
PASS no RUN_ERROR
PASS node read user_123 off RunnableConfig
PASS node read role 'member'
PASS run completed
MESSAGES_SNAPSHOT: [{"id": "None", "role": "assistant", "content": "hello user_123"}]
ALL DOC-SNIPPET CHECKS PASSED
# docs/integrations/langgraph/auth.mdx — same script, same 7 checks
ALL DOC-SNIPPET CHECKS PASSED
```
**2. Gate-only variant**
(`FastAPI(dependencies=[Depends(current_user)])` + stock
`add_langgraph_fastapi_endpoint`):
```
no token -> 401 {"detail":"Missing bearer token"}
valid token -> 200 True True
PASS gate-only pattern (401 without token, run proceeds with token; identity NOT injected)
```
The trailing `True True` is `RUN_FINISHED` present **and** the node
seeing `nobody` — i.e. the gate works but no identity lands on the
config, exactly as the docs now say.
**3. Header forwarding actually reaches a LangGraph deployment** — the
claim behind the new `headers` guidance. Pointed a real
`@ag-ui/langgraph` `LangGraphAgent` at a local stub server, set
`agent.headers` the way `configureAgentForRequest` does, and recorded
what arrived:
```
[ { "url": "/assistants/search", "auth": "Bearer end-user-token", "apiKey": "server-side-key" } ]
PASS: authorization forwarded to the deployment
```
Both the end-user token and the server-side key arrive, which is why the
"server-configured headers win on collision" note is accurate.
Runtime-side breadth is already covered by
`packages/runtime/src/v2/runtime/__tests__/agent-utils-header-forwarding.test.ts`
("authorization header IS forwarded").
**4. Docs render checks** — both pages compile as MDX (`@mdx-js/mdx`
`compile()`), and every `python` block on both pages parses
(`ast.parse`), including the ones I didn't touch.
## Follow-up
**The DIY endpoint is deliberate but temporary.**
`add_langgraph_fastapi_endpoint` exposes no per-request seam (no
`dependencies` passthrough, no config/agent factory), and `endpoint.py`
is byte-identical in 0.0.41 and 0.0.42 — so owning the route is
currently the only way to get a verified identity onto
`config["configurable"]`. Adding that seam upstream in
`ag-ui-protocol/ag-ui` is tracked as **OSS-760**; when it lands, both
pages collapse back to the helper form and the DIY route stays only as
an escape hatch.
The broader "document request-scoped auth for
`add_langgraph_fastapi_endpoint`" ask in #3177 is now substantively
answered by these pages; leaving that issue open pending a maintainer's
call on whether it wants an SDK-level hook rather than the DIY endpoint.
🤖 Generated with [Claude Code](https://claude.com/claude-code)