applyByteCap's "hard guarantee" docstring was false: it trimmed only the
metadata bag (Steps 1-3), so caller-supplied variable-length STRING fields
that the caller controls — `slug`, `demo`, `parent_span_id`, `trace_id` —
were never trimmed. A 5000-char `demo`/`slug` enqueued ~5552B over the 2048B
default cap while stamped `_truncated:true` as if bounded (spec §7 R5-F3
violation).
Add Step 4: after the metadata bag is exhausted (dropped to {}), if the
envelope is STILL over cap the excess can only be in those four caller string
fields. Clamp them longest-first to a short prefix + `…[clamped]` marker until
under cap. Minted ids (`test_id`, `span_id`) are left intact — they are
fixed-width by construction and already bounded; `trace_id` mirrors `test_id`
(fixed-width UUIDv7) so it is included only for completeness so the
post-condition holds even for a hand-built oversized `trace_id`. Format-
constrained / enum / numeric fields (`boundary`, `layer`, `outcome`, `ts`,
`mono_ns`, `duration_ms`, `schema_version`, 9-key `edge_headers`) are bounded
by construction and never touched. Post-condition: serializedSize(env) <= cap
for ANY realistic envelope.
Fix `_truncated` semantics: stamp it only when trimming ACTUALLY occurred (a
field was modified), per its documented meaning, rather than on bare over-cap
detection. A detection that finds nothing trimmable until Step 4 ends with
Step 4 trimming and stamping.
applyByteCap is private; its sole caller is buildEnvelope (which both emit()
and flush() route through). Pure instrumentation preserved — never throws.
Adds a durable bytecap-invariant matrix test over pathological shapes (huge
slug/demo/parent_span_id, huge nested + scalar-heavy metadata, default + debug
tiers) asserting serializedSize(env) <= BYTE_CAP_BY_TIER[tier] for every case.
The §6 PII guarantee was incomplete in three ways that three CR rounds kept
re-finding in the same class. This completes the scrub.
1. Nested-value scrub. `validateMetadata` scrubbed ONLY top-level string
values; secrets buried in allow-listed array/object values (e.g.
`backend.error.caught.stack_brief`, `aimock.match.decision.reject_reasons`)
leaked. New `scrubDeep(value)` in scrub.ts (leaf module; called from
schema.ts which already imports from ./scrub.js — no cycle) applies
`scrubSecrets` to every string LEAF at any depth, preserving structure and
non-string leaves. validateMetadata deep-scrubs object/array values on a
`structuredClone` (cycle-preserving; no caller mutation), falling back to
in-place scrub on the rare unclonable input (correctness > no-mutation).
Walker cycle-safety: ITERATIVE (explicit work stack, never the call stack)
with a WeakSet visited-guard — a self-referential/cyclic or 10k-deep object
is visited once and never re-entered, so no stack-overflow/hang on untrusted
metadata. Only plain objects (Object.prototype/null proto) and arrays are
descended; Date/RegExp/etc. are left as-is.
2. base64url key alphabet. SK_KEY_REGEX
`/sk-(?:[A-Za-z0-9_-]*[A-Za-z0-9]{12,})[A-Za-z0-9_-]*/g` redacts modern keys
whose body uses base64url incl. `_`/`-` (sk-ant-api03-AbCd_Ef-…, sk-proj-…),
with a ≥12-char contiguous-alnum entropy gate (shortest real tail
`0123456789xyzAB` = 15) so prose ("ask-me-later" → me/later; "task_list_items"
uses `sk_` not `sk-`) is NOT redacted. Legacy `sk-<16+ alnum>` still matches.
3. multi-@ URL userinfo. URL_USERINFO_REGEX `/([a-z][a-z0-9+.-]*:\/\/)[^/\s]*@/gi`
redacts the FULL authority up to the LAST `@` before the host/path; excluding
`/` keeps it from crossing into the path. Fixes `https://a@b@c.com/x` leaking
`b@c.com`; user:pass@ and bare-token tok@ still work.
scrubSecrets signature + SCRUB_REPLACEMENT unchanged. Call sites of the deep
scrub: schema.ts `validateMetadata` (object/array branch) → scrubDeep; emit.ts
buildEnvelope routes data-plane metadata through validateMetadata unchanged.
Red→green: scrub-coverage corpus (schema.test.ts) + nested array-of-objects
sweep & cyclic-input safety (emit.test.ts) observed RED (base64url key
unredacted; multi-@ tail leaked; nested secret survived envelope JSON), GREEN
after fix. Full cvdiag suite 119/119 (pb-writer live-PB test skipped sans
PB_BIN). tsc clean; oxfmt + oxlint 0 errors on touched files.
1.61.0 is now published. Regenerates all 20 starter package-lock.json files
so @copilotkit/* resolves to 1.61.0 and the transitive
@copilotkit/license-verifier moves 0.4.2 -> 0.5.0 (shipped by runtime@1.61.0).
ENT-939
buildEnvelope's accounting branch aliased args.metadata into the
envelope; applyByteCap's in-place trims (Steps 1/2) then mutated the
caller's object as a side effect of pure instrumentation. Shallow-clone
the accounting bag before it enters the envelope (data-plane events
already get a fresh `survivor` from validateMetadata, so only the
accounting branch needed the clone).
applyByteCap Step 3's full-bag size-drop stamped _metadata_dropped, but
that flag is the §6 PII closed-world signal (set by buildEnvelope when
validateMetadata drops unknown keys); overloading it for a SIZE drop
pollutes PB drift queries. A size-drop is already observable via the
_truncated flag stamped earlier, so Step 3 now drops the bag without
touching _metadata_dropped. The genuine PII stamp is unchanged.
Call sites (both private, in-module only):
buildEnvelope: emit() (emit.ts:309), flush() accounting (emit.ts:514).
applyByteCap: buildEnvelope() (emit.ts:388).
No external callers.
The old SK_KEY_REGEX /sk-[A-Za-z0-9]{16,}/ stopped its character class at
the first hyphen, so modern hyphenated-prefix keys leaked into PocketBase:
the `proj` (4) / `ant` (3) / `api03` (5) prefix segments never reached the
required 16-char run, so `sk-proj-…` and `sk-ant-api03-…` never matched and
were stored verbatim — defeating the §6 secret-scrub guarantee for the two
most common real-world key formats.
New pattern: /sk-(?:[A-Za-z0-9]+-)*[A-Za-z0-9]{16,}/g
`sk-` then zero-or-more hyphen-terminated alnum SEGMENTS (the prefix words)
followed by a ≥16-char alnum entropy TAIL.
Why it does not over-redact ordinary hyphenated prose: the mandatory 16+
alphanumeric tail is the entropy gate. Words like "ask-me-later" or
"task-list" have no 16+ contiguous alnum run after an `sk-`, so they never
match (negative test asserts the phrase is returned unchanged). The legacy
`sk-<16+ alnum>` form is preserved as the zero-segment case.
SCRUB_REPLACEMENT and scrubSecrets structure unchanged. Four new tests
(sk-proj-, sk-ant-api03-, legacy sk-<16+>, negative over-redaction);
RED on the two hyphenated cases pre-fix, GREEN after. Full cvdiag suite
110/110.
Move the `pbWriter === undefined` early-return to the TOP of flush(), before
the queue.splice and the drop-accounting block. Previously, with no writer,
flush() spliced the queue into `batch` and discarded it on the trailing
early-return while resetting droppedSinceFlush to 0 — silently losing all
queued telemetry every flush window. Under { autoFlush: true } with no writer
this was continuous data loss, contradicting the documented pbWriter contract
("When absent, events stay queued").
flush() callers: the startBackgroundFlush() interval timer and any manual
flush() call. With a writer present, the A3 drop-accounting + reset-only-
after-landing behavior is unchanged.
flush() previously zeroed droppedSinceFlush BEFORE confirming the accounting
emit() landed, then recovered the just-enqueued event via a fragile second
splice of the whole queue (which APPENDS despite the "prepend" docstring). If
that emit() returned null (validateEnvelope failure / exception) the drop count
AND the cvdiag.queue_dropped record were permanently lost.
Fix: extract the envelope-construction + validateEnvelope + applyByteCap logic
of emit() into a private buildEnvelope() helper (no enqueue side effect). flush()
now calls buildEnvelope() for the accounting event and pushes the result DIRECTLY
into the outgoing batch (no queue round-trip / re-splice). droppedSinceFlush is
reset to 0 ONLY after the accounting envelope is in the batch, so a null/throw
retains the count for the next flush. Docstring updated to "append" (event order
in a best-effort batch is not load-bearing — the classifier re-sorts by mono_ns/ts).
Call sites of the extracted buildEnvelope(): emit() (enqueues result, still
resolves/returns CvdiagEnvelope|null) and flush() (appends to batch, still
resolves/never-rejects). Public method signatures unchanged.
Red-green: src/cvdiag/emit.test.ts. Loss-path test forces the accounting build
to return null and asserts the drop count is retained (lands on the next flush);
RED on original = 0 queue_dropped events land (count silently lost), GREEN after
fix = exactly 1 lands with _dropped_count preserved.
applyByteCap previously stamped _truncated:true and trimmed only >64-char
string values / nested objects in metadata. An envelope over the tier byte
cap due to many short-string or numeric/boolean metadata values (none
reachable by that pass) was stamped truncated yet still enqueued OVER cap,
violating spec §7 R5-F3 ("the whole serialized envelope must fit the tier
byte cap").
Fix escalates while still over cap: (1) legacy >64-char/object trim, then
(2) clamp ALL string values progressively, then (3) drop metadata to {}
(stamping _metadata_dropped). Fixed scalar envelope fields are bounded by
construction, so an empty metadata bag is the hard guarantee the enqueued
row fits any realistic cap. Method stays pure-instrumentation: it never
throws.
Call sites: applyByteCap is private; emit() (emit.ts:361) is the sole caller
and already wraps it in try/catch. Post-condition holds: on return the
serialized envelope is <= cap OR metadata is already {}.
validateMetadata kept surviving metadata VALUES verbatim and never scrubbed
them, so Bearer/sk-/URL-userinfo secrets in free-text fields (e.g.
*.message_scrubbed, probe.*.url) reached PocketBase unscrubbed — violating
spec §6 (the field is literally named message_scrubbed). Now every SURVIVING
string metadata value is run through scrubSecrets; non-string values are left
untouched.
Also tighten URL_USERINFO_REGEX: the old pattern required a user:password@
colon, so a bare-token userinfo (scheme://token@, e.g. https://ghp_xxx@host)
was NOT redacted. The new pattern matches any non-/, non-@, non-ws run before
@, covering BOTH the colon and no-colon forms; the $1[REDACTED]@ replacement
shape is unchanged and the / exclusion confines the match to the authority.
Import-cycle avoidance (approach a): moved scrubSecrets + its regex constants
into a new leaf module scrub.ts that has NO intra-cvdiag imports. Applying the
scrub inside validateMetadata required schema.ts to call scrubSecrets, but
edge-headers.ts already imports EDGE_HEADER_KEYS from schema.ts, so importing
scrubSecrets from edge-headers into schema would form a schema -> edge-headers
-> schema cycle. The leaf module is imported by both; edge-headers re-exports
the symbols so the historical public surface is preserved.
scrubSecrets call-site enumeration (all still resolve):
- src/probes/drivers/d4-chat-roundtrip.ts (imports from ./cvdiag/index.js;
index.ts does `export * from ./edge-headers.js`, which re-exports scrub.ts)
- src/cvdiag/raw-byte-capture.ts (imports from ./edge-headers.js — re-export)
- src/cvdiag/index.ts (`export *` from edge-headers — re-export intact)
## Summary
- fix Showcase docs snippets that import `randomUUID` from non-existent
`@copilotkit/shared/v2`
- use the published `@copilotkit/shared` entrypoint instead
- move the fix to the publishing Showcase docs source under
`showcase/shell-docs`
## Linear
- FAC-65
## Verification
- `rg -n "@copilotkit/shared/v2" showcase/shell-docs/src/content`
returns no matches
- `pnpm validate:model-names`
- `npm ci --ignore-scripts` in `showcase/shell-docs`
- `npm run build` in `showcase/shell-docs`
- `git diff --check`
- pre-commit passed after refreshing root dependencies with `pnpm
install`
## Summary
- wrap the Deep Agents TypeScript `observed_steps` custom state field in
`zodState`
- clarify that `step_progress_tool.steps` intentionally maps into the
`observed_steps` state field
- move the fix to the publishing Showcase docs source under
`showcase/shell-docs`
## Why
The QA report called out a mismatch between `steps` and
`observed_steps`. That mapping is intentional via `stateItem`; the
actual issue is that the custom state field was declared as plain Zod.
In LangGraph JS, fields that are not wrapped with `zodState` can be
filtered out of the AG-UI state snapshot, so the tool call can happen
while the UI never receives the rendered state.
## Verification
- `pnpm validate:model-names`
- `npm ci --ignore-scripts` in `showcase/shell-docs`
- `npm run build` in `showcase/shell-docs`
- `git diff --check`
## What
Bumps `@copilotkit/license-verifier` from `0.4.2` to `0.5.0`:
- `packages/runtime/package.json`: `~0.4.2` → `~0.5.0`
- `packages/shared/package.json`: `~0.4.2` → `~0.5.0`
- root `package.json` `pnpm.overrides`: `~0.4.2` → `~0.5.0` (this
override was the effective version gate — without bumping it the
lockfile stayed pinned at 0.4.2)
- `pnpm-lock.yaml`: regenerated, now resolves `0.5.0`
- `.npmrc`: added `@copilotkit/license-verifier` to
`minimum-release-age-exclude[]` (same treatment as `@ag-ui/langgraph`)
so the freshly-published 0.5.0 could be locked before clearing the 24h
`minimum-release-age` guard
## Status
0.5.0 is published and the lockfile resolves it cleanly. The earlier
"blocked on publish" state is resolved. Ready to come out of draft
pending CI.
Linear: ENT-938
Fumadocs' default callout palette (generic blue/amber/green) renders the
docs <Callout> accent and left bar off-brand against the purple-anchored
theme — and on the main docs route the info/success tokens weren't emitted
at all, falling back to the near-white muted color.
Define --color-fd-info/warning/success as plain :root custom properties
(not @theme tokens, which Tailwind v4 tree-shakes when no utility class
references them — the Callout reads them only via inline var()). Map info
-> brand accent (purple), warning -> the existing docs --warning orange,
success -> brand mint (new --success token, mint/800 light, mint/400 dark).
All theme-aware; error stays mapped to --destructive via shadcn.css.
## Summary
Fixes#5417. The v1 `<CopilotKit>` wrapper's `validateProps` threw
`ConfigurationError: Missing required prop: 'runtimeUrl' or
'publicApiKey' or 'publicLicenseKey'` whenever neither `runtimeUrl` nor
a public key was supplied — without considering self-managed agents.
This rejected the documented self-managed-agent setup, even though the
underlying v2 `CopilotKitProvider` accepts it via its `hasLocalAgents`
gate.
- **Fix:** `validateProps` now mirrors the provider's `hasLocalAgents`
check, so `selfManagedAgents` and `agents__unsafe_dev_only` satisfy the
requirement without a `runtimeUrl` or Cloud key.
- **Test:** new rendering test pins the behavior — still throws when
nothing is configured, no longer throws when local agents are supplied.
- **Docs:** the showcase error-reference "v1 behaves differently"
callout claimed the wrapper throws unconditionally and rejects
`selfManagedAgents` (both now false); corrected, and dropped the "(v2
only)" label on the self-managed example.
## Test plan
- [x] `nx test react-core` — 1284 passing, 0 failing
- [x] New test fails before the fix (red) and passes after (green)
- [x] No new type errors introduced (pre-existing `tsc` noise unchanged
vs `main`)