## Problem — the leak
The v2 runtime's `shouldForwardHeader` forwarded `authorization` **and
any header whose name starts with `x-`** onto the outgoing agent call.
In a real deployment the inbound request has already traversed a
browser, CDN/edge, load balancer, and hosting platform — each stamping
its own `x-*` headers — so the wide `x-*` wildcard silently forwarded:
- **Hop-by-hop / topology:** `x-forwarded-for`, `x-real-ip`,
`x-forwarded-proto/host/port`
- **Cloud / CDN tracing:** `x-amzn-trace-id`, `x-amz-cf-id`,
`x-cloud-trace-context`, `x-azure-*`, `x-fastly-*`, `x-request-id`
- **Platform-injected:** `x-vercel-*`, `x-middleware-*`
- **CopilotKit Cloud platform credential:**
`x-copilotcloud-public-api-key`
The last item is a real credential-exfiltration concern: a platform key
scoped to Copilot Cloud reaching a third-party agent URL. This is the
**breadth** half of #5712 (option 3); the **precedence** half was fixed
in #5782.
## Design — denylist default + config knob, both paths
- **Default denylist (safe default).** Keep the `authorization` + `x-*`
base eligibility, but strip a curated, greppable set of known
infra/proxy/platform headers (exact names + prefix families) before
forwarding. Legitimate custom `x-*` application headers (`x-tenant-id`,
`x-api-key`, …) keep flowing untouched. The authoritative list is a
single exported constant in `header-utils.ts`.
- **Configurable policy (`forwardHeaders` runtime option).**
- `useDefaultDenylist?: boolean` (default **true**) — `false` restores
the previous wide-open behavior.
- `deny?` / `denyPrefixes?` — extend the default denylist.
- `allow?` — opt into strict allowlist mode (only listed headers
forward).
- **Resolve once.** The constructor resolves `forwardHeaders` into a
`forwardHeadersPolicy: ResolvedForwardHeadersPolicy` field (mirroring
the existing `debug` → `ResolvedDebugConfig` resolve-once), exposed on
`CopilotRuntimeLike` / `BaseCopilotRuntime` with a passthrough getter on
the `CopilotRuntime` shim.
- **Both paths.** The resolved policy is read at **/run**
(`configureAgentForRequest`) and **/connect** (`handleSseConnect`) via
`mergeForwardableHeaders`, so the two can never diverge. Server-wins
precedence and server-self case-dedup from #5782 are untouched.
## Semver
**Minor with an opt-out.** Removing a leak is a fix, not a contract
change, and we ship a documented escape hatch: `new CopilotRuntime({
agents, forwardHeaders: { useDefaultDenylist: false } })` restores the
prior behavior. Custom-header forwarders (the common case) are
unaffected.
## Red-green proof (real surface, both paths)
RED — with the predicate reverted to the old wide-open `authorization ||
x-*` (policy ignored), the new behavior assertions fail; the leak
reproduces (`x-forwarded-for: 203.0.113.7` forwards on both /run and
/connect):
```
❯ header-utils.test.ts (19 tests | 8 failed)
× strips known infra/proxy/platform headers by exact name → expected true to be false
× strips known infra/platform header families by prefix → expected true to be false
× strips denylisted headers case-insensitively → expected true to be false
× deny extends the default set → expected true to be false
× denyPrefixes extends the default set → expected true to be false
× allow switches to allowlist mode → expected true to be false
× extractForwardableHeaders drops denylisted x-* infra → expected {…4} to deeply equal {…1}
❯ agent-utils-header-forwarding.test.ts (/run) (10 tests | 1 failed)
× strips denylisted infra/platform headers (#5712 breadth) → expected '203.0.113.7' to be undefined
❯ sse-connect-agent-id.test.ts (/connect) (5 tests | 1 failed)
× strips denylisted infra/platform headers → expected '203.0.113.7' to be undefined
```
GREEN — with the real policy in place:
```
✓ header-utils.test.ts (19 tests)
✓ agent-utils-header-forwarding.test.ts (10 tests) # /run path
✓ sse-connect-agent-id.test.ts (5 tests) # /connect path
✓ agent-header-precedence.test.ts (2 tests)
Test Files 4 passed (4)
Tests 36 passed (36)
```
Full `@copilotkit/runtime` suite: **113 files / 1593 tests passed.**
Typecheck, oxlint (0 errors), oxfmt, and build all green.
## Builds on #5782
This branches off #5782's head (`636bcad05`) and reuses that PR's
`mergeForwardableHeaders` (server-wins precedence + server-self
case-dedup). It should land **after #5782**. It addresses the
**forwarding-breadth half of #5712** — #5712's precedence core is fixed
by #5782; this is the breadth follow-up (not `Fixes #5712`).
## Problem
When a self-hosted v2 `CopilotRuntime` is configured with a server-side
agent (an `@ag-ui/client` `HttpAgent` with static `headers` for
service-to-service auth), the runtime forwards inbound
`authorization`/`x-*` request headers onto the agent's outgoing call
**and lets them override the headers the server configured** — silently
breaking service-to-service auth to a secured backend (e.g. a private
Cloud Run agent behind IAM).
`Fixes #5712`
## Root cause
`packages/runtime/src/v2/runtime/handlers/shared/agent-utils.ts:125-128`
merged forwarded inbound headers **last**, so they won on collision:
```ts
agent.headers = {
...agent.headers, // server-configured
...extractForwardableHeaders(request), // inbound — overrode the above
};
```
There are actually **two** failure modes:
1. **Same-case collision** — inbound `authorization` overwrites a server
`authorization` (last-write-wins).
2. **Case-mismatch collision** — `extractForwardableHeaders` lowercases
inbound keys (`authorization`), while the server typically configures
canonical casing (`Authorization`). A plain spread treats those as
*distinct* keys and emits **both** — which undici downstream comma-joins
into a single invalid `"Bearer A, Bearer B"` ("multiple JWTs") value.
Flipping the spread order alone does **not** fix this case.
## Fix
In `agent-utils.ts`, make server-configured `agent.headers`
authoritative on collision, matched **case-insensitively**: drop any
forwarded inbound header whose name (case-insensitively) is already set
on the agent, and let non-colliding inbound headers pass through
unchanged. This preserves the existing forward-for-auth behavior for
headers the server does *not* set, while guaranteeing a server-set token
is never overridden or duplicated.
The merge logic lives in a shared
`mergeForwardableHeaders(serverHeaders, request)` helper in
`packages/runtime/src/v2/runtime/handlers/header-utils.ts` so the
precedence semantics are defined in exactly one place.
### Scope note
This is the conservative precedence + case-insensitive-dedup fix (the
issue's suggested fix#1). I did **not** tighten the default allowlist
to drop hop-by-hop/platform `x-*` headers (`x-serverless-*`,
`x-forwarded-*`, …) or add an opt-out — those alter existing forwarding
behavior and are worth a separate, deliberate change. The precedence fix
alone resolves the reported breakage (the server-set token now wins
regardless of what the platform injects on a colliding header name).
A documented workaround already exists for users on released versions:
pass a custom `fetch` to the `HttpAgent` that builds outgoing headers
from scratch (it runs after `configureAgentForRequest` and survives the
per-request `agent.clone()`).
## Red-green proof (the real fix — `/run` path)
The load-bearing assertion: there must be exactly **one** authorization
header carrying the **server** value.
### RED (fix stashed, against unmodified `agent-utils.ts`)
```
❯ src/v2/runtime/__tests__/agent-header-precedence.test.ts (2 tests | 1 failed)
× configureAgentForRequest — header precedence (#5712) > server-configured agent headers win over a colliding inbound header
AssertionError: expected [ 'Authorization', 'authorization' ] to have a length of 1 but got 2
81| expect(authKeys).toHaveLength(1);
Test Files 1 failed (1)
Tests 1 failed | 1 passed (2)
```
The pre-existing `agent-utils-header-forwarding.test.ts` also failed,
because it explicitly encoded the buggy behavior
(`expect(...["x-aimock-context"]).toBe("new-context")` — inbound
winning):
```
FAIL src/v2/runtime/__tests__/agent-utils-header-forwarding.test.ts > ... > request forwardable headers override matching pre-existing agent headers
AssertionError: expected 'old-context' to be 'new-context'
```
### GREEN (fix applied)
```
✓ src/v2/runtime/__tests__/agent-header-precedence.test.ts (2 tests) 2ms
✓ src/v2/runtime/__tests__/agent-utils-header-forwarding.test.ts (8 tests) 3ms
Test Files 2 passed (2)
Tests 10 passed (10)
```
The colliding test (`agent-utils-header-forwarding.test.ts`) was updated
from asserting the old bug to asserting corrected precedence + a new
case-insensitive-dedup guard. The non-colliding-forward test is retained
unchanged as a regression guard.
## Quality gates
```
NX Successfully ran target check-types for project @copilotkit/runtime
NX Successfully ran target test for project @copilotkit/runtime — Test Files 113 passed (113), Tests 1576 passed (1576)
```
---
## `/connect`-path change — forward-looking plumbing, inert today
The original issue and a prior eval flagged the same forwarding pattern
at `handlers/sse/connect.ts`. To keep the two paths' merge semantics
consistent, the `/connect` path now builds the same server-wins merged
headers (via the shared `mergeForwardableHeaders` helper) and passes
them into `runner.connect()`.
**This is not an active auth fix, and it is not red-green-proven as one
— because there is no live bug to fix on the connect path today.** No
shipped runner consumes the `headers` field of
`AgentRunnerConnectRequest`: the in-memory, intelligence, telemetry, and
sqlite runners all destructure only `threadId` from the connect request
and ignore `headers` entirely. Connect is a thread replay/reconnect, not
a fresh outgoing agent call. So whatever headers we pass into
`runner.connect()` are dropped on the floor by every runner that ships.
What this change actually does:
- Threads the per-request agent clone through `handle-connect.ts →
handleSseConnect` so the connect path *has access to* the
server-configured `agent.headers` (it previously did not).
- Passes `mergeForwardableHeaders(agent?.headers, request)` into
`runner.connect()` — the correct, server-wins argument **shape** for a
future outbound-connecting runner that *would* consume connect-path
headers.
- Rewrites the comments/JSDoc on this path to say this plainly, rather
than implying an active auth fix. It also documents that the
connect-site `cloneAgentForRequest` call is the sole `agentId`-existence
guard (the intelligence branch never re-validates the id), and documents
`cloneAgentForRequest`'s `AbstractAgent | Response` (404) dual-return
contract that both callers depend on.
The real outbound header forwarding — the thing that fixes#5712 — is
the `/run` path's `agent.headers` mutation described above. The connect
change is staged plumbing so that if/when a runner starts honoring
connect-path headers, it inherits the same server-wins precedence
without a second fix.
### Tests on the `/connect` path
The connect tests assert the *merge shape* that reaches
`runner.connect()` (server value wins on collision, exactly one
`authorization` key, non-colliding `x-*` still forwards) and that the
agent-undefined case (no server `agent.headers`) degrades to forwarding
allowlisted inbound headers only and does not crash. These verify the
argument we construct is correctly shaped — not that any shipped runner
consumes it.
## Files
- `packages/runtime/src/v2/runtime/handlers/header-utils.ts` — shared
`mergeForwardableHeaders` helper (case-insensitive, server-wins).
- `packages/runtime/src/v2/runtime/handlers/shared/agent-utils.ts` —
`/run` path uses the helper so server headers win on collision (**the
real fix**).
- `packages/runtime/src/v2/runtime/handlers/sse/connect.ts` — `/connect`
path uses the helper; forward-looking plumbing, inert until a runner
consumes connect-path headers.
- `packages/runtime/src/v2/runtime/handlers/handle-connect.ts` — threads
the per-request agent clone into `handleSseConnect`.
-
`packages/runtime/src/v2/runtime/__tests__/agent-header-precedence.test.ts`
— `/run` regression test exercising the real `configureAgentForRequest`
surface with a real `HttpAgent`.
-
`packages/runtime/src/v2/runtime/__tests__/agent-utils-header-forwarding.test.ts`
— updated the test that encoded the old (buggy) precedence; added a
case-mismatch dedup guard.
-
`packages/runtime/src/v2/runtime/handlers/sse/__tests__/sse-connect-agent-id.test.ts`
— connect-path merge-shape + agent-undefined coverage.
### Notes
- A documented `@ag-ui/client` `HttpAgent` `fetch` workaround already
exists for attaching service-to-service auth the runtime can't override
(see the issue). This change makes the workaround unnecessary for the
`/run` precedence case.
- Conservative scope: this is the **precedence flip on `/run`** plus
forward-looking connect plumbing. Tightening the default allowlist
(dropping hop-by-hop / platform `x-serverless-*`, `x-forwarded-*`,
`x-cloud-trace-context`, …) and an opt-out switch — issue suggestions
#2/#3 — are intentionally left as a follow-up to keep the
security-policy change minimal.
Resolve the single conflict in packages/web-inspector/src/index.ts by keeping
both additions: this branch's CpkMemoryList memory-tab element and main's
ɵCpkThreadDetails back-compat alias (independent top-level declarations).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
## What does this PR do?
This PR fixes build failures on Windows by replacing Unix-only shell
commands (`rm -rf`, `cp`, `mkdir -p`) in `package.json` scripts with
cross-platform Node.js `fs` built-in commands.
This follows the project's existing codebase pattern for cross-platform
operations, as seen in `packages/react-ui/package.json` (line 45).
### 🛠️ Changes:
- **`packages/runtime`**: Replaced `rm -rf` in `generate-graphql-schema`
with `fs.rmSync`.
- **`packages/vue`**: Replaced `cp` in `build:types` and `rm -rf` in
`clean` with `fs.cpSync` and `fs.rmSync`.
- **`packages/angular`**: Replaced `mkdir -p` and `cp` in `build:css`
with `fs.mkdirSync` and `fs.cpSync`.
- **`examples/v1/next-openai`, `next-pages-router`, `state-machine`**:
Replaced `rm -rf` clean commands with a single Node.js loop that deletes
`.turbo`, `node_modules`, `dist`, and `.next`.
All modified packages now build successfully on Windows.
## Related PRs and Issues
- Closes#5601
## Checklist
- [x] I have read the [Contribution
Guide](https://github.com/copilotkit/copilotkit/blob/master/CONTRIBUTING.md)
- [ ] If the PR changes or adds functionality, I have updated the
relevant documentation
- [x] "Allow edits by maintainers" is checked (lets us help iterate on
your PR directly — faster turnaround for everyone)
Empty/whitespace-only allow/deny/denyPrefixes entries are trimmed and
dropped before the allowlist-mode decision, so allow: [""] stays in
denylist mode rather than switching on exclusive allowlist mode. Also
document that allowlist mode bypasses the built-in default denylist, so
integrators must not allow-list protected/platform headers unintentionally.
`shouldForwardHeader` returned early on `policy.allow` and silently ignored
the integrator's `deny`/`denyPrefixes`, so a header listed in BOTH `allow`
and `deny` still forwarded — a footgun on a security feature.
Rework the predicate so the integrator's own `deny`/`denyPrefixes` (exact,
case-insensitive, and prefix) always strip, including in allowlist mode:
`allow` selects the candidate set, `deny` subtracts from it. The built-in
default denylist is unchanged and still applies only in denylist mode (an
explicit `allow` is a deliberate opt-in), so only the integrator's OWN deny
subtracts from an allowlist.
Also harden `resolveForwardHeadersPolicy`: trim and drop empty/whitespace-only
entries from `deny`/`denyPrefixes`/`allow`. A stray `denyPrefixes:[""]` made
`startsWith("")` true for every header (silently denying ALL forwarding), and
`allow:[""]`/`allow:[" "]` seeded the exclusive allowlist with an entry that
could never match — both integrator typos that now can't silently break
forwarding. Entries are lowercased consistently with existing handling.
- agent-utils: assert configureAgentForRequest does not mutate the shared
registered agent; only the per-request clone carries merged inbound headers
(guards against a cross-request bearer-token leak, #5712).
- header-utils: direct mergeForwardableHeaders unit test for server Authorization
vs inbound lowercase authorization with a different value — exactly one
authorization-family key survives carrying the server value (#5712).
- header-utils: shouldForwardHeader boundary tests for the bare 'x' name and the
empty-string name under both denylist and allowlist policies.
The published CopilotRuntimeLike interface (v2 export surface) added
forwardHeadersPolicy as a REQUIRED field, which breaks any external
implementor of the interface — inconsistent with the minor-release
classification. The /run (agent-utils) and /connect (sse/connect) read
sites dereferenced runtime.forwardHeadersPolicy with no coalesce, so a
policy-less object crashed with "Cannot read properties of undefined
(reading 'allow')".
Make the field optional on the interface and coalesce both read sites to
the default resolved policy (resolveForwardHeadersPolicy(undefined),
default-on denylist) when absent. Concrete runtimes (BaseCopilotRuntime)
still always resolve and set it, so behavior is identical for all real
runtimes; the interface is now non-breaking and crash-proof.
Adds a red-green test driving configureAgentForRequest with a runtime
whose forwardHeadersPolicy is undefined: asserts no throw and that the
default denylist applies (x-forwarded-for dropped, custom x-* and
authorization forwarded).
- header-utils.test.ts: new coverage for the default denylist (exact names +
prefix families, case-insensitive), custom x-* still forwarding, config
overrides (useDefaultDenylist:false, deny, denyPrefixes, allow allowlist mode),
and the breadth/precedence interaction. Migrate the inverting assertions
(x-request-id / X-Forwarded-For now stripped; extract result drops x-request-id)
and add the new required policy arg to all call sites.
- agent-utils-header-forwarding.test.ts + sse-connect-agent-id.test.ts: /run and
/connect integration coverage — denylisted infra/platform headers dropped,
custom x-* + authorization still forward, and a runtime-supplied forwardHeaders
policy is actually applied (plumb-through). Swap denylisted filler headers for
non-denylisted custom headers in the precedence regression tests.
- handle-run / handle-connect / intelligence-run-telemetry / get-runtime-info:
add the resolved forwardHeadersPolicy to mock runtimes that route through the
header merge so they satisfy the now-required policy.
Tighten which inbound HTTP headers the v2 runtime forwards onto the outgoing
agent call. The old `authorization` + `x-*` allowlist leaked infrastructure,
proxy, and platform headers (x-forwarded-*, x-real-ip, x-amzn-trace-id,
x-vercel-*, and the Copilot Cloud platform key x-copilotcloud-public-api-key)
to arbitrary configured agent URLs (#5712, breadth half).
- header-utils.ts: add DEFAULT_DENY_HEADER_NAMES + DEFAULT_DENY_HEADER_PREFIXES
constants and a policy-aware shouldForwardHeader; thread ResolvedForwardHeadersPolicy
through extractForwardableHeaders and mergeForwardableHeaders. Add the public
ForwardHeadersConfig and resolveForwardHeadersPolicy (useDefaultDenylist defaults
to true; deny/denyPrefixes extend the default; allow switches to allowlist mode).
Server-wins precedence and server-self case-dedup are unchanged.
- runtime.ts: add forwardHeaders?: ForwardHeadersConfig to BaseCopilotRuntimeOptions,
resolve it once in the constructor into forwardHeadersPolicy (mirroring the
debug -> ResolvedDebugConfig resolve-once), expose it on CopilotRuntimeLike /
BaseCopilotRuntime, and add a passthrough getter on the CopilotRuntime shim.
- Apply the resolved policy at both call sites: /run (configureAgentForRequest)
and /connect (handleSseConnect), so the two paths can never diverge.
Default-on in a minor with { useDefaultDenylist: false } as the documented opt-out.
When an agent is configured with both case-variants of the same header
in agent.headers (e.g. Authorization and authorization), the prior
{ ...base } spread kept both keys — the exact undici comma-join hazard
the function guards against for inbound collisions. Collapse server-self
case-collisions to a single first-occurrence-wins entry; server-wins-over
-inbound and case-insensitive inbound suppression are unchanged.
Cover the #5712 header-precedence behavior across both paths:
- agent-header-precedence.test.ts: server-configured agent.headers win
over forwarded inbound headers on collision (case-insensitive), with
single-key uniqueness assertions for both authorization and the x-*
family (exactly one surviving key carrying the SERVER value).
- agent-utils-header-forwarding.test.ts: the /run path merges via
mergeForwardableHeaders so server values are authoritative and inbound
headers fill only unset keys.
- sse/__tests__/sse-connect-agent-id.test.ts: the /connect path applies
the same merge, plus the agent-undefined case (no server agent.headers)
degrades to forwarding allowlisted inbound headers only and does not
crash.
The /connect path now builds the same server-wins merged headers as the
/run path and passes them into runner.connect(). This does NOT fix
connect-path auth: no shipped runner consumes the headers field of
AgentRunnerConnectRequest today. The in-memory, intelligence, telemetry,
and sqlite runners all read only threadId from the connect request and
ignore headers entirely. The real outbound header forwarding lives on the
/run path, where agent.headers is mutated before the agent runs.
Passing merged headers here is the correct argument shape for a future
outbound-connecting runner, and keeps the connect path's merge semantics
consistent with /run. The comments and JSDoc are rewritten to state this
plainly rather than implying an active auth fix: the connect-site
cloneAgentForRequest call is documented as the sole agentId-existence
guard (the intelligence branch never re-validates the id), and
cloneAgentForRequest's AbstractAgent | Response (404) dual-return contract
that both callers depend on is now documented.
When a request hits the /run path, inbound headers are forwarded to the
agent. Previously, forwarded inbound headers could clobber the
server-configured agent.headers on a key collision, letting a client
override server-set values (e.g. authorization). This is the #5712 bug.
Introduce mergeForwardableHeaders (header-utils.ts): a case-insensitive
merge where server-configured agent.headers always win on collision,
regardless of header-name casing. agent-utils.ts now uses this helper on
the /run path so server-configured values are authoritative and inbound
headers only fill keys the server did not set.
Fixes#5712
ɵsubscribeToMemories mirrored ɵsubscribeToThreads and sent userId in the
body, but the platform's memory routes resolve the app user from the
x-cpki-user-id header (like listMemories/createMemory), not the body — so
POST /api/memories/subscribe returned 401 AUTH_UNAUTHENTICATED. Send the
user via the header instead; the body is now empty.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The memory store client POSTs /memories/subscribe to mint realtime join
credentials, but the runtime router had no such route — the path fell
through to /memories/:id (memories/mutate), so a POST returned 405 Method
Not Allowed. Threads had the full chain (route → handleSubscribeToThreads →
platform); memory was missing the runtime proxy between the client and the
platform's POST /api/memories/subscribe.
Add `memories/subscribe` to the route union, match it before /memories/:id
(and exclude "subscribe" from the :id rule, mirroring threads), add
handleSubscribeToMemories returning { joinToken, joinCode } (memory delivers
the join code here, unlike threads where it rides the thread-list response),
and ɵsubscribeToMemories on the platform client.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Memory handlers now forward client-actionable 4xx platform statuses
verbatim (404 not-found, 409 conflict, 422 unprocessable) so a useMemories
consumer can distinguish those from a server error, and map a platform 5xx
(or malformed status) to 502 rather than collapsing everything to 500 — the
runtime is healthy, its dependency failed. This also avoids a Response
RangeError on an out-of-range status.
parseMemoryBody and the platform client's createMemory/updateMemory now
treat scope as optional, deferring the default to the platform.
Adds POST /memories (create), PATCH /memories/:id (supersede), and
DELETE /memories/:id (retire) to the intelligence runtime, mirroring the GET
read route: CopilotKitIntelligence.{createMemory,updateMemory,removeMemory}
proxy the platform's /api/memories with the identifyUser-resolved user via the
x-cpki-user-id header. Completes the REST surface the client memory store's
addMemory/updateMemory/removeMemory call.
memories/list fell through to the POST-only default in validateHttpMethod,
so GET /memories returned 405. Add it to the GET-allowed group alongside
threads/list.
Adds a runtime-native memory read endpoint mirroring /threads: route
(memories/list) → handleListMemories → CopilotKitIntelligence.listMemories,
which proxies the platform's GET /api/memories with the project API key and
the user resolved via identifyUser (scoped through the x-cpki-user-id header,
never a client-supplied id). The client memory store's {runtimeUrl}/memories
fetch now resolves out of the box for any intelligence runtime — no per-app
BFF code. Read-only for now (list); mutations to follow.
Reverts 2cad274. The real fix for models filling optional tool params is to
use OpenAI's Chat Completions API (the Responses API fills every declared
optional); preserving format/pattern in convertJsonSchemaToZodSchema was
unnecessary (OpenAI ignores format) and broadened behavior for every tool.
convertJsonSchemaToZodSchema dropped every string constraint except enum,
so a tool param like { type: "string", format: "uuid" } reached the model
as a bare optional string. Models then fill such optionals with "" instead
of omitting them, and the loosened model-side validation accepts "" — only
for a stricter downstream (e.g. an MCP server) to reject it.
Carry format (uuid/email/url/date-time), pattern, and minLength/maxLength
through to the generated Zod schema so the model sees the field's real
shape and the model-side validation matches the server's.
Repairs TypeScript check-types across the monorepo and adds a CI gate so
regressions are caught going forward:
- core: bundler module resolution and strict-mode fixes
- sdk-js: bundler module resolution; keep codegen, formatter, packaging working
- react-core: fixes across components, hooks, and tests
- react-native: restore catch binding referenced by TypeError cause
- runtime: repair check-types and bound AI SDK schema inference
- web-inspector: nodenext import extensions, export Anchor
- remaining packages and node example: assorted check-types repairs
- deps: add missing type-only devDependencies
- license context driven from /info licenseStatus
- ci: run check-types in the static quality workflow
Squashed from 12 commits for a single, easily-revertable change.
## Release monorepo v1.61.1
**Scope:** `monorepo` | **Bump:** `patch`
---
### How this release process works
1. **This PR was created automatically** by the "release / create-pr"
workflow.
It bumped the `monorepo` packages to `1.61.1`
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.61.1`
- Creates git tag `monorepo/v1.61.1`
- 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.
Addresses PR review feedback:
- Resolve the license token once (option ?? COPILOTKIT_LICENSE_TOKEN) into a
protected readonly field on BaseCopilotRuntime, and have
CopilotIntelligenceRuntime's licenseChecker reuse it. Collapses the duplicated
resolution and structurally enforces that telemetry attribution and feature
gating can never disagree, instead of relying on a "keep in sync" comment.
- Add an integration test for the env-var-only path (no licenseToken option) —
the exact self-hosted scenario this PR targets — proving the env-resolved
token reaches lambdaClient.send through a real request. Kept in its own file
so the process-wide telemetry singleton (last-write-wins) can't false-pass it.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Adds genuine end-to-end coverage beyond the SSE-via-Express case:
- SSE via the Hono adapter
- SSE via the framework-agnostic fetch handler (what node + custom adapters wrap)
- Intelligence mode end-to-end (real CopilotIntelligenceRuntime, WS runner stubbed)
Each constructs a real runtime (so the base-class setLicenseToken runs), drives a
real request through the adapter, and asserts the token reaches lambdaClient.send
on oss.runtime.copilot_request_created.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Only CopilotIntelligenceRuntime called telemetry.setLicenseToken in its
constructor; BaseCopilotRuntime and CopilotSseRuntime did not. As a result,
self-hosted SSE users got anonymous runtime telemetry (no telemetry_id) even
with a license token configured — and those events were additionally throttled
to the 5% anonymous sample rate, leaving runtime telemetry_id stuck at ~1%.
Hoist the licenseToken resolution (option ?? COPILOTKIT_LICENSE_TOKEN env
fallback) and telemetry.setLicenseToken call into BaseCopilotRuntime so SSE and
Intelligence runtimes attribute telemetry identically. Remove the now-redundant
duplicate from CopilotIntelligenceRuntime (its licenseChecker stays).
Tests cover every construction path into the endpoints:
- runtime-license-telemetry.test.ts: SSE/Intelligence direct + CopilotRuntime
shim (both delegates) x {explicit option, env fallback, none}; asserts the
token is set exactly once (guards against a double-set after the hoist).
- sse-license-telemetry.integration.test.ts: end-to-end proof the token rides
to lambdaClient.send through a real Express endpoint request.
- copilot-runtime-license-telemetry.test.ts: regression guard for the v1
CopilotRuntime path (already worked, previously untested).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>