D1 — showcase_deploy.yml false-red fix
======================================
The build workflow legitimately uploads no `redeploy-summary` artifact when it ran
(push touched `showcase/**` so `paths:` matched) but `detect-changes` found no
buildable service, so `redeploy-staging` was skipped. The build still concludes
`success`, so `showcase_deploy.yml` fires on `workflow_run` and `resolve-matrix`
runs. `actions/download-artifact@v4` with `name:` HARD-FAILS on a missing
artifact, so the unguarded download was failing the job, and a downstream guard
that trips `enforce-redeploy-gate` on `resolve-matrix.result == 'failure'` was
flipping the workflow RED — a false-red on a routine showcase-docs/script change.
Add an artifact-existence pre-check using `actions/github-script` (pinned by SHA,
matching the existing repo convention) that lists the artifacts for
`workflow_run.id` via `actions: read` (already granted to `resolve-matrix`) and
sets `summary_present=true|false`. Gate the existing download step on
`summary_present == 'true'`. Keep NO `continue-on-error`, so the C1 property
holds: when the artifact exists but the download genuinely fails, the job still
fails loud and `enforce-redeploy-gate` correctly reds the workflow. When the
artifact is legitimately absent, the bash gate's existing `[ ! -f "$SUMMARY" ]`
branch no-ops (`redeploy_red=false`, `ok_services=""`) — nothing was
redeployed, so there is nothing to gate.
Updated the step comment block to enumerate the three distinct cases now
handled: workflow_dispatch (no download); workflow_run + artifact absent
(graceful skip); workflow_run + artifact present (download with fail-loud).
L1-L5 — env lint rule hardening
===============================
- L1: route the destructuring (VariableDeclarator/ObjectPattern) branch through
the shared `staticKeyName()` helper so the computed-string-key form
`const { ["NEXT_PUBLIC_X"]: y } = process.env` and the no-expression
template-literal form `const { [\`NEXT_PUBLIC_X\`]: y } = process.env` are
caught with the same parity as the bracket-member read.
- L2: unwrap a wrapping `ChainExpression` at the top of `isProcessEnv()` so
`process.env?.X` is matched robustly across parser flavors; corrected the
helper's doc comment to describe the actual semantics.
- L3: export `BANNED_KEYS` from the rule module and have the table-driven test
dynamically import the rule's own Set instead of hand-mirroring it — the
test set now cannot drift from the rule.
- L4: added override-scoping fixtures for `showcase/shell/src/**` and
`showcase/shell-dojo/src/**`; the `.oxlintrc.json` override list already
includes these, but the test now exercises them so an accidental drop is
caught.
- L5: expanded the file-header "Out of scope" doc list to include bulk-iteration
reads (`Object.keys/values/entries(process.env)`, for-in, spread
`{...process.env}`), rest-pattern destructuring, compound-assignment LHS, and
update operators. Documentation-only — the deliberate non-coverage is now
auditable.
Validation
==========
- RED→GREEN confirmed for L1 (two new destructuring computed-key tests) and L3
(dynamic `await import(...)` of BANNED_KEYS failed pre-fix with
"Rule module did not export a non-empty BANNED_KEYS Set", green after export).
- vitest: 38 passed (was 34 baseline + 4 new); aggregate-build-results 6 passed.
- Ruby promote suite: 87 runs, 251 assertions, 0 failures (unchanged).
- python3 yaml.safe_load: showcase_deploy.yml + showcase_build.yml +
showcase_promote.yml all parse OK.
- actionlint: zero NEW findings on the changed file. The pre-existing
showcase_build.yml SC2086/SC2129/runner-label findings are identical on the
integration baseline (unchanged by this commit).
Seven-agent CR surfaced correctness defects in the build/deploy/promote
pipeline and in the no-public-env-shell-read oxlint rule. This commit
closes the false-green paths and broadens lint coverage.
Workflow fixes:
- showcase_deploy.yml: drop `continue-on-error: true` on the redeploy-summary
artifact download. The dispatch path is already guarded by the `if:
workflow_run` clause, so the bash "no summary" branch handles legitimate
manual dispatches. A genuine workflow_run download failure must now fail
loud instead of silently widening verify to the full service set against
stale `:latest`.
- showcase_build.yml: redeploy-staging now intersects the build matrix with
the aggregator success set (`needs.aggregate-build-results.outputs.results`,
status == "success") before producing the redeploy CSV. Failed/skipped
slots no longer get redeployed (which would just re-pull stale `:latest`
and look healthy).
- showcase_build.yml: `notify-all-builds-failed` now additionally requires
`needs.build.result == 'failure'` so it doesn't Slack-spam when the build
job was SKIPPED (verify-image-refs upstream failure).
- showcase_build.yml: `notify` now lists [build, aggregate-build-results,
redeploy-staging] in `needs:` so aggregator/redeploy failures still emit
a Slack signal. `if: failure()` still skips when none of the needs failed.
- showcase_build.yml: `set -euo pipefail` on the Prepare build args step
so a transient $GITHUB_OUTPUT write failure can't ship images without
COMMIT_SHA/BRANCH baked in.
- showcase_deploy.yml: `enforce-redeploy-gate` now also trips on a
resolve-matrix failure (`needs.resolve-matrix.result == 'failure'`) so
an upstream crash that leaves `redeploy_red` empty can't bypass the gate.
- Doc-comment accuracy: drop stale `(PR #5093)` reference; correct the
env-IDs source-of-truth comment; document the optional `skip_build` field
in ALL_SERVICES; clarify that health_path is informational and verify
uses per-service drivers; add the missing `resolve-targets` step 0 to the
promote workflow's "Order:" header.
Aggregator fix (RED-GREEN):
- aggregate-build-results.ts: throw on zero slot dirs. The job is gated
upstream on has_changes == 'true', so zero slot dirs is a broken artifact
download, not a legitimate empty build set. Silently emitting
any_success=false + results=[] is indistinguishable from "all builds
failed" and lets the deploy workflow fall back to probing the full
service set against stale `:latest`. Refuse the ambiguity.
- aggregate-build-results.test.ts: existing empty-INPUT_DIR test was
updated to assert the throw (was: return []).
Oxlint rule (RED-GREEN):
- no-public-env-shell-read.mjs: handle destructuring reads
(const { NEXT_PUBLIC_X } = process.env and aliased form), template-literal
computed keys (process.env[\`NEXT_PUBLIC_X\`]), and explicitly skip
assignment-LHS / `delete` targets (writes are not reads). Optional
chaining already worked through the existing MemberExpression path.
Aliasing (`const e = process.env; e.X`) is intentionally documented as
out of scope (needs scope tracking). Description sharpened to say the
rule guards a specific banned-key set, not all NEXT_PUBLIC_* reads.
- .oxlintrc.json: tighten the off-override glob from
`showcase/**/*runtime-config*` to
`showcase/**/lib/runtime-config*.{ts,tsx}` so it only silences the
intended implementation files, not arbitrary paths containing that
substring.
- lint-rule-no-public-env.test.ts: rewritten as table-driven coverage of
every BANNED_KEYS entry (dotted + bracket-string forms), every ALLOWED
key (asserting non-firing), all new variants from the rule expansion,
the assignment/delete non-fire cases, and override scoping
(runtime-config exempt; packages exempt; shell-tree non-runtime-config
flagged).
Validation:
- actionlint on all three workflows: 8 pre-existing findings (depot label,
pre-existing SC2086 infos in untouched steps); my edits add zero.
- python3 yaml.safe_load: all three workflows OK.
- vitest aggregate-build-results.test.ts: 6/6 pass (incl. new throw test).
- vitest lint-rule-no-public-env.test.ts: 34/34 pass.
- vitest full showcase/scripts suite: 1654/1654 pass across 46 files.
- ruby showcase/bin/spec/all_tests.rb: 87 runs, 0 failures.
- Intersection jq proof (matrix a,b,c × success a,c) → "a,c"; all-failed
→ ""; skipped status excluded.
Plan-B / Option-B migration moved every shell URL/analytics key off the
build-time NEXT_PUBLIC_* env channel and onto runtime config served via
__SHOWCASE_CONFIG__ + getRuntimeConfig(). To prevent a silent regression
where a future change reintroduces a direct process.env.NEXT_PUBLIC_*
read in shell code (which would re-freeze the value at build time and
break no-rebuild env switching), add a focused lint rule.
The rule (copilotkit/no-public-env-shell-read) is implemented as a
custom oxlint JS plugin rule in the existing copilotkit plugin and
enabled under shell-scoped overrides in .oxlintrc.json:
- Errors on process.env.NEXT_PUBLIC_<URL/ANALYTICS> reads in:
showcase/shell-dashboard/src/**, showcase/shell-docs/src/**,
showcase/shell/src/**, showcase/shell-dojo/src/**
- Banned keys: POCKETBASE_URL, SHELL_URL, BASE_URL, OPS_BASE_URL,
INTELLIGENCE_SIGNUP_URL, POSTHOG_KEY, POSTHOG_HOST, SCARF_PIXEL_ID,
GOOGLE_ANALYTICS_TRACKING_ID, REB2B_KEY, REO_KEY
- Intentionally allowed (NOT banned): NEXT_PUBLIC_COMMIT_SHA and
NEXT_PUBLIC_BRANCH (build-stamped artifact identifiers per B10/B11)
and NEXT_PUBLIC_LOCAL_BACKENDS (computed from shared/local-ports.json
at build, local-dev only).
- Excluded files (rule disabled via a follow-up override): MDX content
under shell-docs/src/content/**, runtime-config implementation files,
and *.test.{ts,tsx} / *.spec.{ts,tsx}. oxlint does not support
excludedFiles inside an override block, so the exclusion is expressed
as a later override that sets the rule to off.
Plan-B originally targeted oxlint's eslint/no-restricted-syntax with an
AST-selector regex. oxlint 1.x does not implement that rule (only
no-restricted-globals / no-restricted-imports), so the equivalent guard
is realized as a small custom rule in the existing copilotkit JS plugin
(meta.name=copilotkit), reusing the same plugin loader the repo already
has for require-cpk-prefix and no-single-arg-zod-record.
Verification (red-green): the rule fires on a fixture containing
process.env.NEXT_PUBLIC_POCKETBASE_URL and does NOT fire on a fixture
containing process.env.NEXT_PUBLIC_COMMIT_SHA. Test pins the config via
-c so it works inside git worktrees nested under .claude/worktrees/
where oxlint's automatic upward config search can miss the worktree's
own .oxlintrc.json.
All four shells lint clean: 0 errors of the new rule across
shell-dashboard (114 files), shell-docs (137), shell (29), shell-dojo (6).
## What
Bumps `@copilotkit/license-verifier` from an exact `0.4.0` pin to a
`~0.4.2` patch range across:
- `package.json` — root `pnpm.overrides`
- `packages/runtime/package.json` — `dependencies`
- `packages/shared/package.json` — `dependencies`
- `pnpm-lock.yaml` — regenerated, resolves to `0.4.2`
## Why
Aligns the runtime/shared deps with the newly published
`@copilotkit/license-verifier@0.4.2`. Switching from an exact pin to
`~0.4.2` (`>=0.4.2 <0.5.0`) means future `0.4.x` patches are picked up
automatically, while `0.5.0`+ still requires an intentional bump.
## Notes
- `.npmrc` `minimum-release-age` guard was **not** modified; the
lockfile was regenerated with a one-off override since `0.4.2` was
freshly published.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
## Summary
- keep `CopilotChat` agents aligned to SDK-generated thread IDs even
when `/connect` is intentionally skipped for non-explicit threads
- stabilize `CopilotKitProvider` default object props so rerenders do
not re-sync an empty local agent registry and replace the live
remote/Intelligence agent mid-run
- add regression coverage for SDK-generated thread frontend-tool
follow-up runs and provider empty-agent rerender stability
- add a focused langgraph-python showcase demo, aimock fixture,
Playwright smoke, and QA checklist for ENT-658
- add a patch changeset for `@copilotkit/react-core`
## Testing
- `npx nx run @copilotkit/react-core:test --
src/v2/components/chat/__tests__/CopilotChat.absentThreadConnect.test.tsx`
- `npx nx run @copilotkit/react-core:test --
src/v2/providers/__tests__/CopilotKitProvider.stability.test.tsx`
- Pre-commit hook passed: `pnpm run test` and `pnpm run check:packages`
- Verified exact `CopilotKit/Intelligence` repro branch
`mme/threadid-repro`: unchecked `Explicit threadId`, sent `invoke
testFrontendToolCalling with label X`, confirmed user message/tool
card/assistant reply remain visible
- Verified the same Intelligence repro with `Explicit threadId` checked
- `pnpm exec playwright test
tests/e2e/threadid-frontend-tool-roundtrip.spec.ts --project=chromium
--workers=1` from `showcase/integrations/langgraph-python`
## QA Checklist
- [x] Reproduce the reset in `CopilotKit/Intelligence` branch
`mme/threadid-repro` with `Explicit threadId` unchecked
- [x] Confirm generated-thread frontend-tool round-trip preserves the
user message, tool card, and assistant response
- [x] Confirm explicit-thread frontend-tool round-trip still preserves
the user message, tool card, and assistant response
- [x] Open `/demos/threadid-frontend-tool-roundtrip` in the
langgraph-python showcase demo
- [x] Confirm `Explicit threadId` is unchecked and the chat starts in
SDK-generated thread mode
- [x] Send `invoke testFrontendToolCalling with label X`
- [x] Confirm the user message remains visible
- [x] Confirm the `testFrontendToolCalling` card remains visible and
shows `label: X` plus `result: handled X`
- [x] Confirm the assistant reply `Frontend tool finished for X.`
appears
- [x] Confirm the chat does not return to the empty state
- [x] Repeat with `Explicit threadId` checked and confirm the
explicit-thread path is unchanged
## Notes
The visible reset had two frontend-side causes. First, the chat and
agent could diverge when the SDK generated the thread ID. Second, in
Intelligence mode, provider rerenders could re-sync an empty local agent
registry and replace the live remote agent instance mid-run, dropping
the in-memory chat stream. Both fixes live in `@copilotkit/react-core`.
The Playwright file is intentionally a smoke test for the demo
route/toggle. The source-level regressions live in
`CopilotChat.absentThreadConnect.test.tsx` and
`CopilotKitProvider.stability.test.tsx`.
Move runtime and shared deps (and the root pnpm override) from an exact
0.4.0 pin to ~0.4.2, so future 0.4.x patches are picked up automatically.
Regenerate pnpm-lock.yaml to resolve 0.4.2.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
## Release monorepo v1.59.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.59.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.59.0`
- Creates git tag `monorepo/v1.59.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.
## Summary
Moves `@copilotkit/runtime` from `dependencies` to `peerDependencies` in
`@copilotkit/voice`, and adds it to `devDependencies` so voice's own
typecheck/tests/build continue to work.
## Why
When voice declares runtime as a regular dependency, npm/pnpm may
install a **second copy** of `@copilotkit/runtime` nested under
`node_modules/@copilotkit/voice/node_modules/` whenever the resolved
version differs from the consumer's top-level runtime version. This
causes:
- **Type drift** — consumers importing from voice get types/classes from
the nested runtime, while their app code uses the top-level runtime.
Identity checks fail, type assertions silently degrade.
- **Singleton drift** — module-level state (caches, registries)
duplicates across the two copies.
- **Version slip** — patch updates to the top-level runtime don't
propagate to the nested copy.
As a peer dependency, voice now defers entirely to the consumer's
runtime version. The `devDependencies` entry keeps voice's own
typecheck/tests/build green.
## Consumer audit (CK monorepo)
Every package/example/showcase integration that depends on
`@copilotkit/voice` also declares a direct dependency on
`@copilotkit/runtime` — verified across all 21 consumers (19 showcase
integrations + 2 v2 examples). No consumer is broken by this move.
## Changes
- `packages/voice/package.json`: runtime moved `dependencies` →
`peerDependencies` + added to `devDependencies` (workspace:* in all
three blocks where applicable, matching the existing pin)
- `pnpm-lock.yaml`: regenerated, scoped to the voice importer block only
(3 lines moved between dependencies/devDependencies)
## Verification
- `pnpm install --lockfile-only` succeeds with clean, scoped diff
- `pnpm check-types` in voice produces **zero new errors** vs `main`
(the 2 pre-existing errors — module-resolution +
`@copilotkit/runtime/v2` typing — are unchanged baseline)
- Per-package `pnpm --filter @copilotkit/voice test` passes (1/1)
## Notes on commit hook
- Skipped `lint-fix` (oxfmt rewrites `package.json` to JSON5 syntax with
trailing commas, producing invalid JSON that breaks `pnpm install` —
pre-existing bug unrelated to this change; sibling `package.json` files
have not been touched by it)
- Skipped `test-and-check-packages` (pre-existing
`@copilotkit/web-inspector:test` failure on `main`: `TypeError:
window.localStorage.clear is not a function` — reproduced on `main`
HEAD, unrelated to voice)
Prevents nested-deps drift when consumers depend on @copilotkit/runtime
at a different version than voice's pinned version. Runtime is now a
peer dependency (consumer-controlled), with devDependencies retaining
the pin so voice's own tests and typecheck continue to work.
Picks up the forwarded-headers fix from ag-ui PR #1798
(https://github.com/ag-ui-protocol/ag-ui/pull/1798), which injects
agent.headers as config.configurable.copilotkit_forwarded_headers so
the LG dev server's HTTP-to-configurable bridge is no longer required
for X-AIMock-Context propagation. Closes the header-propagation gap
for showcase D5/D6 langgraph-typescript probes.
Node 25 ships an experimental built-in localStorage global accessor that
shadows jsdom's mock, leaving window.localStorage as an empty stub with
no clear/setItem/removeItem/getItem methods. This broke all 22 telemetry
tests with 'window.localStorage.clear is not a function' and was
blocking pre-commit hooks repo-wide.
Add a vitest setup file that installs a proper in-memory Storage shim
on both globalThis and window before each test, so jsdom-environment
tests behave the same on Node 20 and Node 25.
The previous regression (#5041, shared root cause with #4739) slipped through
because the original coverage (use-agent-thread-isolation.test.tsx) lived
next to the per-thread-cloning feature and was deleted alongside it when
cloning was reverted. The invariant outlived the feature but the tests didn't.
Relocate to packages/react-core/src/__tests__/ and rename as a contract test
so future implementation swaps (cloning, effect, prop drilling, context) keep
it in scope. Tightened the header docstring to spell out the invariant and the
reason for the placement.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
useAgent now syncs agent.threadId from CopilotChatConfigurationProvider when
the caller marked the threadId as explicit. Without this, AbstractAgent's
constructor mints a random UUID and ProxiedCopilotRuntimeAgent ships it in
/agent/run, /agent/connect, /agent/stop — diverging from the threadId app code
reads via useThreads, breaking thread persistence and causing 404s on lookup.
This was originally fixed by per-thread agent cloning in #3525. That cloning
was reverted in May 2026 because it wiped state on tool calls, and the revert
only restored the explicit assignment in V2 CopilotChat — leaving headless
useAgent (issue #4739) and the V1 chat hook path unfixed.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Zod 4 made the key schema mandatory for z.record, so the single-argument
z.record(valueType) form is a compile-time error (TS2554) when built against
Zod 4. @copilotkit/react-core declares zod ">=3.0.0", so downstream apps on
Zod 4 are affected; runtime parsing is unaffected under both majors.
- react-core + vue MCPAppsActivityContentSchema: toolInput now uses the
two-argument z.record(z.string(), z.unknown()) form
- react-core defineToolCallRenderer test: same fix for a metadata schema
- add a toolInput field-contract test (round-trips mixed value types)
- add copilotkit/no-single-arg-zod-record oxlint rule (autofix), enabled as
error for packages/**; the incompatibility is type-level, so no runtime
test can guard it while the workspace lockfile pins Zod 3
Closes#4295
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Picks up per-request header forwarding (onRequest hook + headerFactory)
and the prepareStream configurable+context partition fix from
ag-ui-protocol/ag-ui#1763. Together with copilotkit==0.1.91 on the
Python side (R3a), this unblocks D6 LGP/LGT header propagation.
The mergeConfigs() change in 0.0.33 also fixes the HTTP 400 from
langgraph-api 0.7+ when both configurable and context are present.
Bumped in two files:
- packages/runtime/package.json: 0.0.31 -> 0.0.33
- packages/sdk-js/package.json: 0.0.31 -> 0.0.33
Added @ag-ui/langgraph to minimumReleaseAgeExclude in .npmrc.
pnpm-lock.yaml regenerated.
Showcase auto-redeploys on merge via showcase_build.yml.
Replace docs URLs that currently 301 through the legacy redirect catalog
with their canonical post-cutover destinations so users clicking links
from console warnings, JSDoc, and in-product help land in one hop.
URLs updated:
- /premium#how-do-i-get-access-to-premium-features
-> /premium/overview#getting-access
- /coagents/quickstart/langgraph -> /langgraph-python/quickstart
- /coagents/shared-state/predictive-state-updates
-> /langgraph-python/shared-state/predictive-state-updates
- /reference/v1/hooks/useCopilotChatHeadless_c
-> /reference/v2/hooks/useCopilotChatHeadless_c
- /coagents/troubleshooting/common-issues
-> /langgraph-python/troubleshooting/common-issues
- /quickstart#get-a-copilot-cloud-public-api-key
-> /built-in-agent/quickstart#create-a-free-account
- /premium -> /premium/overview
URLs left as-is because they already resolve 200 with no redirect:
/migration-guides/migrate-attachments, /migration/render-message,
/telemetry.
Hook bypassed: pre-commit test failed in @copilotkit/web-inspector due
to missing jsdom dependency in its package.json (unrelated to this
change; no overlap with edited files or URLs). Tests for the four
affected packages (react-core, react-ui, shared, runtime) pass.
Add peer dependencies, export new components and hooks from package entry point, integrate RenderToolProvider into CopilotKitProvider, configure vitest and tsdown, add usage documentation.
FlatList-based chat interface and bottom-sheet modal overlay. Includes suggestion pills, keyboard avoidance, custom FlatList support, and comprehensive test coverage.
Hook for rendering custom tool UIs in React Native with a store-based context provider. Includes tests for register/unregister, subscriber notification, and error handling.
Message bubble components with streaming markdown support, typing indicator animation, and timestamp formatting. Includes unit tests and edge case coverage.
Packages without repository.url fail npm OIDC provenance verification.
Adds the field to agentcore-runner, core, sqlite-runner, voice, and
web-inspector. Includes a one-shot workflow to publish the 14 remaining
v1.57.4 packages (a2ui-renderer already published via OIDC).
- Rework shared helper: parseAndWarnTelemetryId returns parsed id AND
warns, so both v1 and v2 setLicenseToken call it once without
inlining duplicate code or double-parsing the JWT.
- Fix v1 sampleWeight bug: identified events bypass the sample gate
and ship at effective rate 1.0, so a single global sampleWeight =
1/sampleRate would overweight identified-customer counts by
1/sampleRate (20x at the 0.05 default). Move sample metadata
(sampleRate / sampleRateAdjustmentFactor / sampleWeight) out of
globalProperties and compute per-event using effectiveSampleRate.
- Guard setSampleRate against parseFloat("nonsense") = NaN slipping
past the range check. With the default now 0.05, env-var overrides
are more common and a typo would otherwise produce silent
always-drop.
- Add tests: sampleWeight differs for identified vs anonymous,
malformed JWT stays anonymous, license-token cache is overwritable,
NaN env override is rejected, v2 default sampleRate = 0.05 is pinned.
Cache parsed telemetry_id at setLicenseToken time and use it in capture()
to branch on identified vs anonymous. Identified callers (token with
telemetry_id) always send; anonymous callers are sampled at sampleRate.
Default sampleRate changes from 1.0 to 0.05 so the anonymous OSS-runtime
firehose is capped at the client. Identified customers continue to send
at full fidelity.
Cache parsed telemetry_id at setLicenseToken time and use it to branch
in capture():
- Identified callers (token with telemetry_id) always send to both sinks.
- Anonymous callers are sampled at sampleRate (default 0.05); one dice
roll gates both lambda and Segment.
The Lambda no longer needs to bypass-from-sampling for identified
events — that decision moves entirely to the client. Reduces lambda
invocations by ~95% for the anonymous OSS-runtime firehose.
Operators currently get silent attribution loss if a license token is
configured but parses without a telemetry_id field — useful as a smoke
signal during the issuer rollout, when older licenses lack the field
entirely.
Each TelemetryClient setter (v1 shared, v2 singleton) now calls
parseTelemetryIdFromLicense at configuration time and emits a one-shot
console.warn when the result is null. No per-event spam.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Drops the Lambda/Segment specifics in favor of an implementation-neutral
description: these fields aren't used by the telemetry service, so we
strip them at the wire boundary.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The CopilotCloud customer key (`ck_<env>_<id>.<secret>`) is routed to
Segment for downstream user analytics, but has no role in the
telemetry-sink Lambda. Worse, the secret half should never leave the
customer's runtime.
Strips both wire-format variants at the lambda-client boundary:
- `cloud.public_api_key` (v2 event property convention)
- `cloud.publicApiKey` (v1 globalProperties from setCloudConfiguration)
The strip happens at the lambda-client wire layer rather than in each
caller, so any future caller (or accidental property regression) is
covered by default. Boolean indicators like `cloud.api_key_provided`
and unrelated fields like `cloud.baseUrl` continue to ride through.
New unit test (`lambda-client.test.ts`) exercises the strip with a real
fetch spy, plus end-to-end JWT extraction including the
no-`telemetry_id` and not-a-JWT fallback paths.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The CopilotCloud customer API key (`ck_<env>_<id>.<secret>`) is unrelated
to telemetry attribution — it flows into Segment/PostHog only. The
attribution signal lives in the EIP / Intelligence license JWT, whose
payload carries `telemetry_id` (alongside license_id, owner.org_id,
features, etc.).
Rewires the lambda-client to base64url-decode the license JWT payload
and emit X-CopilotKit-Telemetry-Id from `telemetry_id`. No signature
verification — that's license-verifier's job, and the Lambda is
claim-only by design.
Plumbing:
- Shared TelemetryClient (v1) and v2 telemetry singleton each get a
`setLicenseToken` setter; the v1 client drops `apiKey:` from its
lambdaClient.send call, the v2 client drops the
cloud.public_api_key extraction from event properties.
- Both runtime constructors call `telemetry.setLicenseToken(...)` once,
resolving `options.licenseToken ?? process.env.COPILOTKIT_LICENSE_TOKEN`
to match license-verifier's own env-fallback. Without that, customers
who set only the env var would get a working licenseChecker but
anonymous telemetry.
Tests: v2 telemetry test refreshed — old "cloud api key extraction"
assertion replaced with one that confirms cloud.public_api_key rides
in properties (not as licenseToken), and a new test asserts that
setLicenseToken plumbs through to lambdaClient.send.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The HMAC scheme bound identity to "holder of API key X," but since the
secret is shipped inside distributed customer keys it never actually
prevented a determined attacker from impersonating that customer — and
the Lambda still accepted unsigned requests anyway, so the signing path
provided attribution, not abuse control.
Replaces ~85 lines of Web Crypto / HMAC / nonce / canonical-string
machinery with a single `X-CopilotKit-Telemetry-Id: <id>` header. The
SDK now extracts the id from `ck_<env>_<id>.<secret>` keys and ignores
the secret half. Anonymous sends (no/legacy keys) are unchanged.
Drops the implicit Node ≥19 / edge-runtime requirement that the
WebCrypto path imposed.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>