Releases copilotkit Python SDK 0.1.93 to PyPI, shipping the App Context
forwarded-headers strip (#5096).
Bumps sdk-python/pyproject.toml. Merging triggers the
publish-release.yml PyPI lane (OIDC trusted publisher).
Note: uv.lock not regenerated — this is a poetry-managed project without
a [project] table, so `uv lock` is not applicable. `uv sync --dry-run`
confirms the existing lock is consistent. Only sdk-python/pyproject.toml
changes, which is the exact trigger the publish workflow keys on.
## Summary
- **Root cause**: `langgraph-api` auto-copies the entire
`config.configurable` dict into `runtime.context`. That dict carries the
CopilotKit-internal transport key `copilotkit_forwarded_headers`,
populated solely to drive the httpx header-forwarding hook. The
middleware's `before_agent` step renders `runtime.context` into the LLM
prompt as an "App Context" system message, and the `expose_state` path
can surface the same key via the state note — both leak transport
headers into the prompt body.
- **Symptom**: under strict fixture matching (D6 / aimock), the leaked
headers change the request payload and the match fails (503). Prompts
are also polluted with transport metadata that the user never set.
- **Fix**: hard-exclude `copilotkit_forwarded_headers` from both render
paths. The App Context renderer strips the reserved key before
serialization; the `expose_state` allowlist applies the same exclusion
so an explicit allow cannot reintroduce the leak. The httpx
header-forwarding hook is unchanged — the key still reaches downstream
as an HTTP header. Pure conveyance, no body pollution.
## Why
This is part of the D6 langgraph-python header-conveyance work. Header
conveyance to aimock already works via the httpx hook, but the same
forwarded-headers dict was leaking into the prompt body via
langgraph-api's `configurable` → `context` auto-copy. That broke aimock
strict fixture matching and polluted prompts. The reserved key
`copilotkit_forwarded_headers` is now transport-only.
## Test plan
- Red-green unit tests added for both paths:
- App Context renderer strips `copilotkit_forwarded_headers`.
- `expose_state` default-deny strips it; explicit-allow allowlist also
strips it.
- Full `sdk-python` suite green: **181 passed, 11 skipped, 0 failed**.
- Middleware test file: **51 passed**.
- Proven end-to-end locally via the D6 1-pill
(`langgraph-python:agentic-chat`, 3/3 turns green). aimock journal
confirms the header arrives on the request, no App Context leak appears
in the prompt body, and the fixture matches.
## Known follow-ups (NOT in this PR)
- The OpenAI Responses API (`/v1/responses`) path does not currently
carry forwarded headers — a separate, pre-existing conveyance gap
affecting reasoning-model demos. Tracked separately.
- Full D6 rollout also requires the `@copilotkit/runtime` release
carrying `@ag-ui/langgraph` 0.0.34. Not bundled here.
This PR does **not** claim full D6 parity — it closes the prompt-leak
half of the conveyance work.
## Summary
- Redirected the remaining broken legacy docs URLs to their current
shell-docs locations, including `copilot-suggestions`, `integrations`,
`integrations/built-in-agent`, `telemetry`, and
`learn/tutorials/multi-conversation-chat`.
- Ported the missing telemetry page into the built-in-agent docs and
added it to the sidebar.
- Added the missing `useCapabilities` reference page, restored
`migrate/1.10.X`, and taught shell-docs to resolve docs stored under
route-group folders like `(other)`.
- Added DeepAgents to the framework picker and docs navigation so
framework redirects resolve cleanly.
## Testing
- `npm run test` passed.
- `npm run typecheck` passed.
- `npm run lint` passed with existing unrelated warnings.
- Verified the local shell-docs preview serves the new canonical pages
and returns the expected redirect targets for the legacy URLs.
langgraph-api auto-copies the entire config.configurable dict into runtime.context. That dict
carries the CopilotKit-internal transport key `copilotkit_forwarded_headers`, populated solely
to drive the httpx header-forwarding hook (it is not user-visible state). The middleware's
before_agent step then rendered runtime.context into the LLM prompt as an "App Context" system
message, and the expose_state path could surface the same key via the state note — either path
leaks transport headers into the prompt body and, under strict fixture matching (D6/aimock),
changes the request payload and breaks the match.
Fix: hard-exclude `copilotkit_forwarded_headers` from both render paths. The App Context
renderer strips the reserved key before serialization, and the expose_state allowlist applies
the same exclusion so an explicit allow cannot reintroduce the leak. The httpx hook is
unchanged, so the key still reaches aimock as an HTTP header — pure conveyance, no body
pollution.
Adds red-green unit coverage for both paths (App Context strip, expose_state default + allowlist).
## Summary
- Update shared error troubleshooting URLs to current shell-docs routes
and heading fragments.
- Drop the authentication fragment because shell-docs no longer has an
equivalent section.
## Tests
- node source check for generated docs URLs and shell-docs heading slugs
- NX_DAEMON=false pnpm nx run @copilotkit/shared:test
- pre-commit hook: package test/check suite
## Notes
- NX_DAEMON=false pnpm nx run @copilotkit/shared:check-types currently
fails on existing @copilotkit/license-verifier export errors in
packages/shared/src/index.ts and a telemetry index-signature error in
packages/shared/src/telemetry/telemetry-client.ts; this PR only changes
docs URL strings in packages/shared/src/utils/errors.ts.
publish-python now skips when dry-run=true (matching the npm lane). Add
set -euo pipefail to the tag step so a failed push no longer emits a ghost
tag output or proceeds to the GitHub Release. Pass GITHUB_TOKEN via env
instead of inline interpolation, guard against an empty dist/ before
uv publish, and surface curl errors from the PyPI verify-live loop.
Right-pad version tuples so 0.2 and 0.2.0 compare equal (PEP 440), avoiding a
duplicate-version publish that PyPI rejects. Exclude fully-yanked releases when
computing the published max so a yanked high version can't block real bumps.
Fail loud when a 200 response lacks a releases key instead of assuming the
package is new. Surface curl transport errors and add red-green coverage.
Compare against the max numeric version in PyPI `releases` rather than
`info.version` (latest-uploaded, not highest). Apply strict dotted-numeric
validation to the LOCAL version only; non-numeric published versions
(prereleases) are filtered out instead of aborting the script. Add curl
--max-time/--retry hardening and red-green test coverage for both cases.
Add set -euo pipefail and non-empty SHA validation to the pyproject-change
detection step so a null/stale merge_commit_sha fails loudly instead of
silently skipping a real version bump. Mirror the npm lane's success guard
on publish-python. Correct the python_publish input description (detection
still runs) and drop the dead checkout token (persist-credentials is false).
## Summary
- Add a version selector to the reference docs so readers can switch
between v1 and the default reference content
- Update reference routing and item loading to support the new v1
hierarchy
- Add v1 reference content metadata and pages for hooks, components,
SDKs, and classes
- Extend SEO redirects to preserve reference navigation across versions
## Testing
- Not run (not requested)
The PR bumps copilotkit==0.1.91 -> 0.1.92 across three showcase
requirements.txt files (langgraph-python, langgraph-fastapi, strands).
The validate-pins ratchet compares the SHA-256 of the sorted [FAIL]
tuple set against the recorded baseline. The langgraph-fastapi tuple
"copilotkit pinned ==0.1.91, Dojo has ==0.1.87" now reads
"==0.1.92, Dojo has ==0.1.87" — same already-failing tuple, new text,
so the FAIL count is unchanged at 106 but the hash flipped.
No new pin drift was introduced (count stays at 106). The
pre-existing langgraph-fastapi <-> Dojo parity gap is out of scope
for this bump and tracked separately. Updating only validatePinsFailHash
to reflect the new tuple text.
Old: d340cdebe623177b957b62576821b51cde7f174b6b788040d67cc87e3d20b702
New: 4355457a222f8011c361da7a848d7361e897ee588ad0b8c6487b851fd0c23b77
Bumps copilotkit Python SDK from 0.1.91 to 0.1.92 across the three showcase integrations that
pin it: langgraph-python, langgraph-fastapi, and strands.
This picks up the header_propagation fix from CopilotKit/CopilotKit#5088, which ensures the
runtime's X-* headers (including X-AIMock-Context) propagate end-to-end through the Python
SDK middleware so D6 testing of langgraph-python sees the expected context routing.
No lockfiles to regenerate — these are plain pip requirements consumed directly by the
integration Dockerfiles.
Adds a module-level docstring at the top of header_propagation.py that explains in plain
English what the module does (forwards CopilotKit request-context x-* headers onto
outbound LLM/provider HTTP calls so downstream services like the aimock test server and
proxies can correlate the outbound call with the original inbound request), the precise
scope (only headers the application itself set via set_forwarded_headers; no request
bodies, cookies, user data, credentials, or telemetry), and the mechanics (walks the
._client chain to find the httpx client and attaches an async hook for AsyncClient or a
sync hook for Client). Also softens a few comments and docstrings whose wording could
read as surveillance language ("inject" / "installing an async hook ... silently dropped")
to neutral engineering phrasing ("attach" / "the forwarded headers would not be attached
to the outbound request") without losing any technical meaning or warnings. No executable
logic, signatures, conditionals, or string literals in code paths were changed; the diff
is comments and docstrings only.
In response to review feedback on PR #5088.
Forwarded headers (e.g. X-AIMock-Context) were dropped before reaching the
wire in two cases. First, install_httpx_hook only attached to the immediate
client, missing an httpx client nested one or more ._client hops deep; it now
walks the ._client chain (bounded) to find the object that carries
event_hooks. Second, a sync def hook installed on an httpx.AsyncClient was
invoked as a coroutine and never awaited, silently dropping headers; the hook
is now async def for AsyncClient and sync def for Client. Async detection
prefers isinstance against the real httpx classes and falls back to an EXACT
"AsyncClient" MRO class-name match (not startswith("Async"), which would
misclassify a sync client whose MRO includes an Async*-named base).
## Summary
Adds `showcase/bin/railway` — a single-file Ruby tool (stdlib only, no
Bundler/Gemfile) that exposes 9 subcommands for Showcase Railway
operations:
| Subcommand | Purpose |
|---|---|
| `snapshot` | Capture an env's services + config into a YAML snapshot.
|
| `restore` | Restore an env to a snapshot (force-redeploy each
service). |
| `rollback` | Roll a single service back one deploy (`--to
DEPLOYMENT_ID` for specific). |
| `rollback-commit` | Restore an env to the snapshot committed at a
given git SHA. |
| `promote` | Promote staging digests to production with prechecks. |
| `pin` | Pin a service to a specific image digest. |
| `env-diff` | Diff two envs; exits 1 on drift. |
| `resolve-digest` | Resolve an image tag to its `sha256:` digest via
GHCR. |
| `lint-prod` | CI gate: warn if any prod service is not digest-pinned.
Supports `--exit-zero` (advisory mode) and `--format json`
(machine-readable output). |
Spec: https://www.notion.so/36d3aa38185281df97e4cfe11dad7d47 (companion
to the main rollback spec)
## Design highlights
- **Stdlib only** — `net/http`, `json`, `yaml`, `optparse`. No gem deps,
no Gemfile, no Bundler.
- **Auth** — reads `RAILWAY_TOKEN` env var first, falls back to
`~/.railway/config.json`. Never invokes `railway login`/`railway
logout`/`op`.
- **GraphQL** — direct calls to
`https://backboard.railway.app/graphql/v2` using
`serviceInstanceDeployV2` for force-redeploy.
- **GHCR digest resolution** — uses OCI Distribution Spec manifest HEAD
+ `Docker-Content-Digest` header. Anonymous token for public packages.
- **Snapshot YAML schema** — versioned (`version: 1`). Records env-var
KEYS only, never values, so snapshots are safe to commit.
- **Production protection** — every mutating subcommand requires `--yes`
AND a typed `production` confirmation phrase on stdin.
`--non-interactive` skips the prompt but still requires `--yes`. No way
to mutate prod without explicit acknowledgement.
- **Uniform exit codes** — 0 clean, 1 drift/findings/refused, 2 error.
## Promote precheck classes
Per the spec:
- **MOVE**: image digests; startCommand (only with
`--include-startcommand`); auto-update-disable.
- **VERIFY-REFUSE**: service-set parity, critical env-key parity
(RAILWAY_TOKEN, GHCR_TOKEN, SHARED_SECRET, OPS_TRIGGER_TOKEN,
POCKETBASE_SUPERUSER_*, GITHUB_APP_PRIVATE_KEY, OPENAI/ANTHROPIC/GOOGLE
keys).
- **WARN**: missing/extra custom domains (expected:
showcase/dashboard/dojo/docs/hooks.copilotkit.ai for prod,
.staging.copilotkit.ai for staging).
- **IGNORE**: env-scoped URLs, volumes.
## Tests
Minitest suite (stdlib) at `showcase/bin/spec/`:
- `test_cli_parsing.rb` — argv parsing for each subcommand, dispatcher
behavior, env aliases, `lint-prod --format` parsing.
- `test_snapshot_roundtrip.rb` — YAML write/read, schema version
validation, `find_service` helper.
- `test_ghcr_digest.rb` — image-ref parsing across all shapes, digest
resolution decision tree, 404 → nil, 5xx → raise.
- `test_production_protection.rb` — staging bypasses, prod-without-yes
aborts, prod+yes+non-interactive proceeds, typed-phrase prompt
accept/reject.
27 runs, 70 assertions, 0 failures.
```sh
ruby showcase/bin/spec/all_tests.rb
```
## CI integration
New workflow `.github/workflows/showcase_lint_prod.yml` runs on every PR
that touches `showcase/**`:
1. `bin/railway lint-prod --exit-zero --format json` — checks that every
prod service is digest-pinned. (Soft-skips with a warning if
`RAILWAY_TOKEN` secret is unset.)
2. `ruby showcase/bin/spec/all_tests.rb` — runs the test suite.
### lint-prod is advisory during initial soak
The lint-prod step currently passes `--exit-zero`, which makes the
command exit 0 even when findings exist. The workflow is also resilient
to snapshot/GraphQL errors: if `lint-prod` itself crashes for any
reason, the workflow renders an "audit unavailable" block instead of
failing the PR. Findings still print to the job log, so we can see
drift, but they will not block PRs while we soak the check against real
production state.
**Plan to flip to enforcing:**
1. Merge this PR; let the advisory job run on every showcase PR for a
few rounds.
2. Confirm the findings list stays clean (or fix any genuine drift we
surface).
3. Remove `--exit-zero` (and the error-tolerant capture) from the
workflow step in a follow-up one-liner PR to turn lint-prod into a hard
CI gate.
This avoids the failure mode where a brand-new check immediately blocks
unrelated PRs because of pre-existing prod state we haven't audited yet.
### Visibility surfaces
Every run renders the audit result in two places so people don't have to
click into the job logs:
1. **`$GITHUB_STEP_SUMMARY`** — a structured markdown block at the top
of every workflow run page. Shows on every event (`pull_request`,
`push`, `workflow_dispatch`). Contains: one-line status, table of
unpinned services (only — `pinned` services are not enumerated),
Pacific-time run timestamp, finding count.
2. **Sticky PR comment** — on `pull_request` events, the workflow posts
(or updates) a single comment per PR. The comment is keyed by the HTML
marker `<!-- lint-prod-sticky-comment -->` so re-runs PATCH the same
comment instead of creating duplicates. Plain `gh` CLI only — no
third-party action.
The workflow also writes the findings count to `$GITHUB_OUTPUT`, so a
future Slack-alert step can compare against prior runs.
If `lint-prod` itself fails (e.g. a snapshot/GraphQL error), both
surfaces render an "audit unavailable" block with the captured error
inside a `<details>` fold, rather than going blank.
## Test plan
- [ ] CI passes (`Showcase: lint-prod (digest pinning)` job)
- [ ] `showcase/bin/railway --help` lists all 9 subcommands
- [ ] `showcase/bin/railway <sub> --help` works for every subcommand
- [ ] Local: `RAILWAY_TOKEN=... showcase/bin/railway snapshot --env
staging --dry-run` produces valid YAML
- [ ] Local: `RAILWAY_TOKEN=... showcase/bin/railway env-diff staging
production` produces a drift report
- [ ] Local: `RAILWAY_TOKEN=... showcase/bin/railway lint-prod` returns
0 (or 1 if prod drift exists — informational)
- [ ] Local: `RAILWAY_TOKEN=... showcase/bin/railway lint-prod --format
json` emits valid JSON with `services`, `findings`, `timestamp`
- [ ] CI run shows the audit block in the step summary
- [ ] PR has a single sticky comment that updates (not duplicates) on
re-runs
## Summary
Audit of staging + production PocketBase collection rules turned up two
open holes. Both are now closed in both environments and captured as
forward/backward migrations under `showcase/pocketbase/pb_migrations/`.
## Findings
### users.createRule = "" (both envs)
Any anonymous client could POST to `/api/collections/users/records` and
create a real account. The dashboard exposes no signup UX — operators
authenticate via the superuser credentials in `PbAuthPrompt` — so create
should be admin-only.
Verified on prod with curl:
```
POST /api/collections/users/records {anon body} -> 200 (BEFORE)
POST /api/collections/users/records {anon body} -> 403 (AFTER)
```
### baseline.updateRule = "" (prod only — staging has no baseline
collection)
Any anonymous client could PATCH baseline rows, silently flipping status
cells (`works` ↔ `impossible`) and rewriting `updated_by` /
`updated_at`. The dashboard's baseline edit flow already gates writes
behind `PbAuthPrompt`, so locking `updateRule` to admin-only keeps the
existing operator workflow intact.
Verified on prod with curl:
```
PATCH /api/collections/baseline/records/<id> {} -> 200 (BEFORE)
PATCH /api/collections/baseline/records/<id> {} -> 403 (AFTER)
```
## Rule matrix (after this change)
| Collection | list | view | create | update | delete | Notes |
|-----------------|------------------|------------------|--------|--------------------|--------------------|-------|
| users | `id=@req.auth.id`| `id=@req.auth.id`| null | `id=@req.auth.id`
| `id=@req.auth.id` | self-edit only; admin-only signup |
| status | "" | "" | null | null | null | dashboard reads anon, harness
writes as admin |
| status_history | "" | "" | null | null | null | same |
| probe_runs | "" | "" | null | null | null | same |
| baseline (prod) | "" | "" | null | null *(was "")* | null | dashboard
reads anon, edit gated by PbAuthPrompt |
| alert_state | `auth.id != ""` | `auth.id != ""` | null | null | null |
already locked |
The only fields changed by this PR are `users.createRule` (both envs)
and `baseline.updateRule` (prod). Everything else was already correct.
## Procedure followed
1. Read superuser credentials from Railway via direct GraphQL
(`variables` query) on the `pocketbase` service in both environments.
2. Authed against `/api/collections/_superusers/auth-with-password` in
both envs and captured JWTs.
3. Enumerated `/api/collections?perPage=200` and built the before/after
matrix.
4. Confirmed anonymous user signup succeeded against prod (HTTP 200) and
that a probe user was created; deleted it via the admin API.
5. Confirmed anonymous baseline PATCH succeeded against prod (HTTP 200).
6. Applied the rule changes via `PATCH /api/collections/<id>` against
**staging first**:
- `users.createRule` → `null`
- Re-fetched all rules, confirmed.
- Verified anonymous signup now 403, anonymous status read still 200,
dashboard at `dashboard.showcase.staging.copilotkit.ai` still HTTP 200
with a populated HTML payload.
7. Applied the same change set to **production**:
- `users.createRule` → `null`
- `baseline.updateRule` → `null`
- Verified anonymous signup 403, anonymous baseline PATCH 403, anonymous
status + baseline READ both 200, dashboard at
`dashboard.showcase.copilotkit.ai` still HTTP 200 with a populated HTML
payload.
## Test plan
- [x] Staging dashboard renders after change (verified)
- [x] Prod dashboard renders after change (verified)
- [x] Anonymous user signup returns 403 (verified both envs)
- [x] Anonymous baseline update returns 403 (verified prod)
- [x] Anonymous reads of status/baseline still return 200 (verified
prod)
- [ ] On a fresh PocketBase instance, applying these migrations from
scratch leaves the final rule shape identical to the audit (the
migrations key off `findCollectionByNameOrId` so they only mutate the
two fields, leaving everything else untouched).
Fixes zizmor findings on .github/workflows/showcase_lint_prod.yml:
- unpinned-uses: pin actions/checkout to the repo-canonical SHA (v4)
- unpinned-uses: pin ruby/setup-ruby to v1.310.0 SHA
- artipacked: set persist-credentials: false on the checkout step
Matches the rest of the repo, which SHA-pins every uses: with a trailing
# vX.Y.Z comment for Dependabot to keep current.
The tool was written from a spec but never exercised against live Railway,
so several queries reference fields that do not exist in the public
schema. Live runs (including the CI lint-prod step) failed with errors
like `Cannot query field "domains" on type "Project"`. Unit tests passed
because they only covered parsing and IO, not the GraphQL shape.
Verified the live schema via introspection (2026-05) and corrected
every mismatch:
- Project has no `domains` field. The previous `customDomains: domains
{ customDomains { ... } }` block on the Project selection is gone.
Custom domains now come from `serviceInstance.domains.customDomains`
for the env we are inspecting.
- Service has no `serviceInstances` field. We can no longer enumerate
per-env instances by nesting under Service. The new flow is:
1. SERVICES_LIST_QUERY -> list services in the project
2. SERVICE_INSTANCE_QUERY -> per (service, env), fetch source,
startCommand, latestDeployment, domains
3. ENVIRONMENT_VARIABLES_QUERY -> all variables in the env, then
group keys by Variable.serviceId for per-service env_keys
- `serviceInstanceDeployV2` does not accept an `image` argument; its
signature is (commitSha, environmentId, serviceId). To pin a service
to a specific image we now use
serviceInstanceUpdate(input: { source: { image } })
followed by serviceInstanceRedeploy. RestoreCommand exposes a
pin_and_redeploy class method that PromoteCommand and PinCommand
reuse.
- `deploymentRollback` returns scalar Boolean, so the previous
`deploymentRollback(id: $id) { id }` was invalid GraphQL — selection
sets are not allowed on scalars. Dropped the selection set.
- Auth.token now reads `user.accessToken` from ~/.railway/config.json
first. The legacy `user.token` field is a short CLI session token
(4 chars on a fresh login) that does not authenticate against the
public GraphQL API and was producing silent "Not Authorized" errors.
Added spec/test_snapshot_graphql.rb with a FakeGQL that returns realistic
shapes, plus regression guards that fail the build if anyone reintroduces
`Project.domains`, `Service.serviceInstances`, `serviceInstanceDeployV2`
with an image arg, or a selection set on `deploymentRollback`.
Smoke-tested live (read-only) against the showcase project:
- lint-prod: "OK: all production services digest-pinned." (27 services)
- snapshot --env staging: full YAML with images, startCommands, env_keys
- env-diff staging production: 32 drift findings (expected, staging is
not digest-pinned)
- resolve-digest ghcr.io/copilotkit/showcase-aimock:latest: digest
returned successfully
No mutating subcommands (restore, rollback, promote, pin) were run live.
The lint-prod step crashes today on a pre-existing snapshot bug (GraphQL
schema query for a "domains" field that no longer exists on Project).
The `--exit-zero` flag only suppresses exit-on-findings — it doesn't
catch fatal Ruby errors during snapshot building, so an error short-
circuits the whole job and the visibility surfaces never render.
Make the workflow truly advisory:
- Capture lint-prod stderr + exit code instead of failing the step.
- If lint-prod errors (or produces no JSON), synthesize a minimal
payload with an "error" field so the renderer has something to chew on.
- Renderer detects the error field and emits an "audit unavailable" block
with the captured error inside a <details> fold instead of leaving the
step summary / sticky comment blank.
Snapshot bug itself is out of scope for this change; tracked separately.
Make the lint-prod audit result legible without having to click into the
workflow logs. Two surfaces, both rendered from the same JSON payload:
1. `$GITHUB_STEP_SUMMARY` — structured markdown block at the top of every
workflow run page. Shows on every event (push, pull_request,
workflow_dispatch).
2. Sticky PR comment — one comment per PR, keyed by the HTML marker
`<!-- lint-prod-sticky-comment -->`. Re-runs update the same comment via
`gh api -X PATCH` instead of creating duplicates. Plain `gh` CLI only,
no third-party action.
Both surfaces show: one-line status, a table of the unpinned services only
(not all 27), and a Pacific-time run timestamp with the finding count.
To support the renderer, add `--format json` to `lint-prod`:
{services:[{name,source,status}], findings:N, timestamp:"ISO8601"}
The workflow consumes this shape and also writes `findings` to
`$GITHUB_OUTPUT` so downstream jobs (future Slack alert) can compare runs.
Idempotent: re-running the workflow finds the existing comment by marker
and PATCHes it — never duplicates.
Add --exit-zero flag to bin/railway lint-prod that makes the command exit 0
even when digest-pinning findings exist. Findings still print to stdout so
they remain visible in the CI step log.
Wire the showcase_lint_prod.yml workflow to pass --exit-zero so the job
cannot block PRs while we soak the check against real production state.
Once we have confidence the findings are clean, remove --exit-zero from
the workflow step to flip lint-prod to enforcing.
Updates README to document the advisory-mode behavior and the path to
flipping the check to enforcing.
## Problem
On
[docs.copilotkit.ai/built-in-agent/build-with-agents](https://docs.copilotkit.ai/built-in-agent/build-with-agents),
**"JSON Configuration File"** shows a blank panel.
<img width="789" height="99" alt="image"
src="https://github.com/user-attachments/assets/1f8d8c4a-c5fb-4d59-9d3e-785352a3bf5c"
/>
## Why
Our `Tab` wrapper called `escapeValue()` before passing to Fumadocs's
`FumadocsTab` — which also calls it internally. For 3-word labels, the
double-escape produces different values for the trigger vs the content
panel, so Radix hides the content.
```
"JSON Configuration File"
→ trigger (1 pass): json-configuration file ← space
→ content (2 passes): json-configuration-file ← hyphen (mismatch → hidden)
```
## Fix
Remove `escapeValue()` from the `Tab` wrapper — Fumadocs already handles
it. The `Tabs` `defaultValue` still needs pre-escaping since
`FumadocsTabs` doesn't apply it internally.
Audit of staging + production PocketBase rules turned up two open holes:
- users.createRule = "" allowed any anonymous client to POST to
/api/collections/users/records and create a real account. The
dashboard exposes no signup UX — operators authenticate via the
superuser credentials in PbAuthPrompt — so create should be
admin-only.
- baseline.updateRule = "" (production only) allowed any anonymous
client to PATCH baseline rows, including silently flipping status
cells and overwriting the updated_by/updated_at audit fields. The
dashboard's baseline edit flow already gates writes behind
PbAuthPrompt, so locking updateRule to admin-only keeps the existing
operator workflow intact while removing the open hole.
Both changes were applied via the PocketBase admin API to staging first
(verified dashboard still renders and live status SSE still flows),
then to production (same verification, plus 403 confirmations on the
anonymous signup + anonymous baseline PATCH requests that previously
returned 200).
All other collection rules already had the right shape: status,
status_history, probe_runs, baseline retain listRule/viewRule = ""
because the dashboard reads them unauthenticated via the PocketBase JS
SDK; create/update/delete rules stayed null because the harness writes
with the superuser JWT.
Fumadocs's Tab component applies escapeValue() internally to the value
prop. Our DocsTab wrapper was also calling escapeValue() before passing
to FumadocsTab, causing multi-word tab values to be escaped twice.
For "JSON Configuration File" (3 words, 2 spaces):
1st escape (our wrapper): "json-configuration file" (1 space left)
2nd escape (Fumadocs Tab): "json-configuration-file" (fully hyphenated)
The trigger value uses only ONE escapeValue call:
escapeValue("JSON Configuration File") = "json-configuration file"
Trigger "json-configuration file" != content "json-configuration-file"
so Radix sets data-state="inactive" on the content panel, which is
then hidden by data-[state=inactive]:hidden.
Fix: remove escapeValue() from our Tab wrapper. The Tabs defaultValue
still needs pre-escaping because FumadocsTabs accepts it as-is (no
internal escape); only the individual Tab has internal escaping.
Single-word and two-word tabs (HTTP, Application Settings) were
unaffected because one escapeValue pass already produces a hyphen-only
string that is idempotent under a second pass. Same bug also affected
"stdio Transport (Local)" in the Windsurf section.
Three classes of legacy URLs were 404'ing because shell-docs (with BIA
as the soft-default framework) doesn't serve them at the root surface
the old docs did:
1. BIA-canonical pages — /server-tools, /mcp-servers, /model-selection,
/advanced-configuration, /agent-app-context live only under
/built-in-agent/ now. Internal sidebar clicks already framework-scope
via SidebarLink; external traffic (marketing, blog posts, bookmarks)
was 404'ing.
2. Moved root pages — /mcp-apps (moved to /generative-ui/),
/copilot-runtime, /custom-agent (moved to /backend/), /deep-agents
(renamed to /deepagents), /multi-agent-flows (LangGraph-only),
/custom-look-and-feel folder index, /generative-ui/specs/* (specs
subgroup retired), plus assorted misc (/what-is-copilotkit,
/getting-started/quickstart-chatbot, /telemetry, /migration-guides/*,
/reference/hooks/useCoAgent).
3. Legacy /integrations/<fw>/* prefix — R15/R17 already handled the
built-in-agent variant; this extends the same pattern to every other
framework, mirroring the existing /docs/integrations/* coverage.
All redirect destinations verified to return 200 against a local dev
build; existing tests still pass.
## What does this PR do?
Fixes#4995 in the Python SDK by making `LangGraphAGUIAgent.dict_repr()`
build its metadata directly instead of calling `super().dict_repr()`.
### Problem description
`CopilotKitRemoteEndpoint.info()` crashed with `AttributeError: 'super'
object has no attribute 'dict_repr'` whenever a registered agent was a
`LangGraphAGUIAgent`. That made the runtime info endpoint unusable for
LangGraph CoAgent setups.
### Root cause
`LangGraphAGUIAgent` inherits from AG-UI's `LangGraphAgent`, and that
upstream base class does not implement `dict_repr()`. The existing
implementation called `super().dict_repr()`, so Python resolved to
`object` and raised `AttributeError`.
### Fix
- Replace the `super().dict_repr()` call with an explicit metadata dict
built from `self.name` and `self.description`
- Preserve the existing CopilotKit-specific `type: "langgraph_agui"`
field
- Add regression coverage for both `dict_repr()` and
`CopilotKitRemoteEndpoint.info()`
## Related PRs and Issues
- Fixes#4995
## Testing
- [x] `python3 -m pytest sdk-python/tests/test_agui_agent.py -q`
- [x] `python3 -m pytest sdk-python/tests/test_agui_agent.py
sdk-python/tests/test_agui_context_serializable.py
sdk-python/tests/test_emit_filtering.py -q`
- [x] `python3 -m pytest sdk-python/tests -q`
- [x] Manual verification that `CopilotKitRemoteEndpoint.info()` returns
serialized LangGraph AGUI agent metadata without raising
## 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)
Three classes of legacy URLs were 404'ing because shell-docs (with BIA
as the soft-default framework) doesn't serve them at the root surface
the old docs did:
1. BIA-canonical pages — /server-tools, /mcp-servers, /model-selection,
/advanced-configuration, /agent-app-context live only under
/built-in-agent/ now. Internal sidebar clicks already framework-scope
via SidebarLink; external traffic (marketing, blog posts, bookmarks)
was 404'ing.
2. Moved root pages — /mcp-apps (moved to /generative-ui/),
/copilot-runtime, /custom-agent (moved to /backend/), /deep-agents
(renamed to /deepagents), /multi-agent-flows (LangGraph-only),
/custom-look-and-feel folder index, /generative-ui/specs/* (specs
subgroup retired), plus assorted misc (/what-is-copilotkit,
/getting-started/quickstart-chatbot, /telemetry, /migration-guides/*,
/reference/hooks/useCoAgent).
3. Legacy /integrations/<fw>/* prefix — R15/R17 already handled the
built-in-agent variant; this extends the same pattern to every other
framework, mirroring the existing /docs/integrations/* coverage.
All redirect destinations verified to return 200 against a local dev
build; existing tests still pass.
## What does this PR do?
Add complimentary TypeScript examples now that the TypeScript Strands
integration (`@ag-ui/aws-strands`) has been published.
## Related PRs and Issues
- https://github.com/ag-ui-protocol/ag-ui/pull/1681
## Checklist
- [x] I have read the [Contribution
Guide](https://github.com/copilotkit/copilotkit/blob/master/CONTRIBUTING.md)
- [x] If the PR changes or adds functionality, I have updated the
relevant documentation
- [x] "Allow edits by maintainers" is checked (lets us help iterate on
your PR directly — faster turnaround for everyone)