The published Channels docs configured the Intelligence client with
`wsUrl: apiUrl.replace(/^http/, "ws")` in all eight pages. Because the API and
realtime planes are separate hosts, that yields a URL serving no socket, and
the failure is a silent 30s hang rather than an error — so this was the most
harmful copy of the bug: it is the surface developers actually read.
Every page now reads COPILOTKIT_INTELLIGENCE_WS_URL from the environment
alongside the API URL, and the six env blocks that previously showed only a
`your-intelligence-url` placeholder document both hosts with the values that
work against the managed service. The quickstart gains a short paragraph on why
the second host is required and what going wrong looks like.
Refs OSS-621
The Slack and Teams examples derived COPILOTKIT_INTELLIGENCE_WS_URL from
COPILOTKIT_INTELLIGENCE_URL with a scheme-only swap when it was unset, and
documented it as optional. That derive preserves host and port, so it is only
correct on a deployment that puts both planes behind one host+port — which is
neither prod (api.intelligence… vs realtime.intelligence…) nor local dev (4201
vs 4401). In practice it was always wrong, and wrong in the worst way: the
resulting join hangs in `connecting` for 30s and reports only a timeout.
All three call sites (slack native, slack managed, teams) now require both
URLs explicitly and deriveWsUrl is gone. The Teams startup error names both
vars and says why one cannot be computed from the other.
managed.test.ts previously deleted the WS var to exercise the derive; it now
sets a host+port deliberately different from the API URL, so the test encodes
that the planes are deployed apart rather than assuming they are not.
Refs OSS-621
Every documented Intelligence endpoint used api.copilotkit.ai for both the
REST and websocket planes. That host returns 404, and the realtime host it
implied (realtime.copilotkit.ai) has no DNS record at all, so a developer
copying any of these snippets could not connect.
The real managed endpoints are split across two hosts, matching what the CLI
bakes in as its prod defaults and what gitops deploys:
apiUrl: https://api.intelligence.copilotkit.ai
wsUrl: wss://realtime.intelligence.copilotkit.ai
Because the planes are separate hosts, wsUrl cannot be produced from apiUrl by
swapping the scheme, so each site now says so explicitly. The runtime skill's
"common mistakes" section gains that as a third failure mode, and the
CopilotKitIntelligenceConfig JSDoc documents it on the field itself — a wrong
apiUrl fails fast with an HTTP error, but a wrong wsUrl only hangs until the
settle timeout, which is what made this expensive to diagnose.
skills/runtime is the generated mirror of packages/runtime/skills/runtime,
regenerated with pnpm sync:plugin-skills. skills/copilotkit-debug is
standalone; its /info sample keeps the /client suffix, which is correct there
because that response carries the already-derived client URL.
Refs OSS-621
packages/runtime declared "uuid": "^10.0.0", but nothing in the package's
source imports uuid directly — id generation goes through randomUUID()
re-exported from @copilotkit/shared, which already depends on uuid@^11.1.0.
The unused v10 pin only served to emit an npm deprecation warning for every
consumer installing @copilotkit/runtime.
Bump to ^11.1.0 to match @copilotkit/shared and clear the warning.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
## Summary
Angular is now a first-class docs surface instead of a small frontend
island:
- give `/angular` the full shared documentation information
architecture, including deployment, troubleshooting, What's New,
Channels, CLI, telemetry, backend setup, Runtime, agentic protocols, and
CopilotKit Enterprise Intelligence
- keep frontend and backend selection orthogonal, preserving the
selected backend when switching React ↔ Angular
- share frontend-independent content once and swap only frontend-owned
sections, avoiding an Angular × backend × topic copy matrix
- resolve React-owned topics to Angular-native task guides and canonical
redirects instead of showing React hooks or provider setup
- render Angular-specific branches correctly in pages and `.mdx`/LLM
output, including nested shared snippets
- source Angular examples from runnable Showcase regions for tools,
agent config, sub-agent delegation, headless UI, interrupts,
authentication, and Mastra activity renderers
- add Angular-native shared-state, HITL, generative UI, auth, thread,
troubleshooting, metadata, canonical, search, and sitemap coverage
- add a complete `injectThreads` API reference so the threads journey
has no dangling handoff
## Guardrails
- exact React → Angular information-architecture parity test across the
root and all 20 visible backends; every React destination maps to a
published Angular destination
- exhaustive Angular × every visible backend render test: every page
resolves, and rendered Markdown contains no React package, hook,
provider, or unexpanded frontend/snippet components
- shared Runtime, Intelligence, deployment, troubleshooting, Channels,
and operational routes stay canonical instead of being duplicated for
every backend
- backend-owned pages appear under a backend only when that backend
genuinely changes the content
- frontend selector links preserve backend context
- Showcase source-region tests ensure Angular snippets remain tied to
runnable code
- shell-docs image contract ensures Angular source is staged and bundled
before the production build
## Validation
- `npm run test` in `showcase/shell-docs` — 42 files / 209 tests
- `npm run typecheck` in `showcase/shell-docs`
- `npm run lint` in `showcase/shell-docs` — clean apart from existing
warnings
- `npm run build` in `showcase/shell-docs` — 222 static pages
- `pnpm nx run @copilotkit/showcase-scripts:test --skip-nx-cache` — 72
files / 2,324 tests
- `pnpm nx run @copilotkit/showcase-angular-host:typecheck
--skip-nx-cache`
- clean root-context `showcase/shell-docs/Dockerfile` build — all
content generators, TypeScript, and 222 static pages
- production-server smoke checks for Angular root, deployment,
troubleshooting, What's New, Channels, telemetry, LangGraph agent
config/sub-agents, API reference, `.mdx` output, canonical redirects,
and sitemap
- rendered `.mdx` audit confirms shared backend guidance and
Showcase-derived Angular snippets are present with no React
imports/hooks or raw conditional tags
The broad `verify-shell-docs:fast` audit remains red on the repository's
existing whole-tree dead-link/content-shape baseline; the same audit is
red on `main`. Branch-specific tests and production routes are green.
Draft intentionally; mark ready only when requested.
Independent re-verification against packages/react-native source found the
page overstated polyfill auto-install: only /headless (and the root barrel,
via its /headless re-export) side-effect-imports the polyfill barrel.
src/components/index.ts imports no polyfills, so a consumer importing only
from @copilotkit/react-native/components does not get them auto-installed;
it relies on the provider imported alongside it. Corrects the 'all three
surfaces auto-install' and 'every entry point installs these automatically'
statements accordingly.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Fixes OSS-616
## Problem
`scripts/release/verify-runtime-package.ts` **cannot pass on a channels
release PR**, so the `unit (20.x)` leg is red by construction on exactly
the PRs the gate exists to protect — and gets merged past.
1. `packages/runtime/package.json` declares
`@copilotkit/channels-intelligence: workspace:*`.
2. `pnpm pack` rewrites `workspace:` to the workspace's *current*
version — on a release PR, the freshly-bumped one (e.g. `0.3.0`).
3. The temp consumer then has to resolve
`@copilotkit/channels-intelligence@0.3.0` **from npm**.
4. That version doesn't exist yet. This PR is what publishes it.
Real CI output from #6185:
```
ERR_PNPM_NO_MATCHING_VERSION No matching version found for
@copilotkit/channels-intelligence@0.3.0 while fetching it from https://registry.npmjs.org/
This error happened while installing the dependencies of @copilotkit/runtime@1.63.2
The latest release of @copilotkit/channels-intelligence is "0.2.1". Published at 7/17/2026
```
Red instances: **#6185** (`channels/v0.3.0`) and **#6025**
(`channels/v0.2.1`, merged red on 2026-07-17). #5989 (`v0.2.0`) passed
only because the hard runtime dependency landed the next day in
`fea464de52`.
## Fix
`verify-channels-umbrella.ts` already solved this — its own comment
describes the problem verbatim. Same approach applied here: pack the
whole first-party `workspace:` closure locally and pin it through pnpm
`overrides` so nothing unpublished is resolved over the network.
The packing helpers (`packPackage`, the transitive `workspace:` walk)
move into `scripts/release/lib/pack-workspace.ts`, shared by both
scripts instead of duplicated. `verify-channels-umbrella` behavior is
unchanged — it just calls the extracted helpers.
**The check is not weakened.** It still asserts the packed runtime
declares `@copilotkit/channels-intelligence` as a real dependency and
that the package loads through both ESM and CJS. A new guard fails
loudly if channels-intelligence ever stops being a `workspace:` dep, so
a silent fallback to registry resolution can't creep back in. Runtime
dependency ranges were not touched.
## Testing
All runs under Node 20 in a clean worktree off `origin/main`.
**1. Reproduced the failure** — applied the exact 9 version bumps from
`release/publish/channels/v0.3.0`, then ran the pre-fix script from
`origin/main`:
```
The latest release of @copilotkit/channels-core is "0.2.1". Published at 7/17/2026
Command failed: pnpm install --ignore-scripts
```
**2. Fixed script, same bumped state** — acceptance criterion 1:
```
$ pnpm run verify:runtime-package
OK: packed runtime installs @copilotkit/channels-intelligence and loads through ESM and CJS.
EXIT=0
```
**3. Still fails when the dependency is removed** — deleted
`@copilotkit/channels-intelligence` from
`packages/runtime/package.json`:
```
packed runtime must install @copilotkit/channels-intelligence as a dependency
EXIT=1
```
**4. Still fails on a genuine ESM load break** — `throw` at the top of
`packages/channels-intelligence/src/index.ts`:
```
Error: simulated ESM load break
Command failed: pnpm exec node --experimental-import-meta-resolve --input-type=module --eval ...
EXIT=1
```
**5. Still fails on a genuine CJS load break** — `throw` at the top of
`packages/runtime/src/v2/index.ts`:
```
Error: simulated CJS load break
Command failed: pnpm exec node --eval require("@copilotkit/runtime");
EXIT=1
```
**6. Umbrella check unaffected by the helper extraction:**
```
$ pnpm run verify:channels-umbrella
OK: local Channels snapshot is exact, compatible, singly resolved, and TSX-consumable.
```
**7. Release script tests** — new
`scripts/release/__tests__/verify-runtime-package.test.ts` models the
release-PR case (bumped local versions absent from the registry) and
asserts no workspace dependency is left registry-resolvable. The vitest
include glob was extended to cover `__tests__/`:
```
Test Files 10 passed (10)
Tests 137 passed (137)
```
**8.** `oxfmt --check` clean, `oxlint` 0 warnings / 0 errors, `tsc
--strict --noEmit` clean on all four touched scripts.
All mutations from steps 1 and 3–5 were reverted; the final diff touches
`scripts/release/` only.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
`verify:runtime-package` could not pass on a channels release PR. `pnpm pack`
rewrites the runtime's `workspace:` ranges to the workspace's current version,
so on a release PR the temp consumer tried to resolve the freshly-bumped
`@copilotkit/channels-intelligence` from npm — the version this very PR is
about to publish. It failed `unit (20.x)` by construction on #6185 and #6025.
Apply the fix `verify-channels-umbrella` already uses: pack the whole
first-party `workspace:` closure locally and pin it through pnpm `overrides`.
The packing helpers move to `lib/pack-workspace.ts` so both scripts share one
implementation instead of duplicating it.
The contract is unchanged: the packed runtime must still declare
channels-intelligence as a real dependency, and it must still load through
both ESM and CJS.
## Release channels v0.3.0
**Scope:** `channels` | **Bump:** `minor`
---
### How this release process works
1. **This PR was created automatically** by the "release / create-pr"
workflow.
It bumped the `channels` packages to `0.3.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 `channels` packages to npm at version `0.3.0`
- Creates git tag `channels/v0.3.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.
The harness's loosely-typed runtimeTransport string isn't assignable to
ProxiedCopilotRuntimeAgentConfig.transport (CopilotRuntimeTransport); leave it
at the "auto" default since these tests assert threadId/runtimeAgentId only.
Fixes the check-types failure.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
CI 'Showcase: Validate' failed: aimock-fixtures.test.ts asserts every d6/*.json
yields >=1 fixture, but gen-ui-open.json had been emptied to a _meta stub (0
fixtures) by an earlier open-gen-ui shadowing fix that consolidated the pills
into gen-ui-tool-based.json.
Restore gen-ui-open.json as a byte-for-byte context-swap of the canonical
langgraph-python file (context -> langgraph-fastapi): 4 fixtures for the '3D
axis' and 'Inline expression evaluator' pills, LGP's clean shape
(hasToolResult:false leg-1, distinct toolCallId terminals). No shadowing loop:
each file's leg-1/terminal pair is anchored by its own distinct toolCallId, so
the done-signal always resolves.
Verified: aimock-fixtures.test.ts 839/839 green locally; D6 gen-ui-open,
gen-ui-open-advanced, gen-ui-custom all green (two real runs each).
Updated the Angular section to include 'Source Code & Quickstart'
instead of 'Source Code - Quickstart coming soon'.
<!--
Thank you for sending the PR! We appreciate you spending the time to
work on these changes.
Help us understand your motivation by explaining why you decided to make
this change.
**Please PLEASE reach out to us first before starting any significant
work on new or existing features.**
By the time you've gotten here, you're looking at creating a pull
request so hopefully we're not too late.
We love community contributions! That said, we want to make sure we're
all on the same page before you start.
Investing a lot of time and effort just to find out it doesn't align
with the upstream project feels awful, and we don't want that to happen.
It also helps to make sure the work you're planning isn't already in
progress.
As described in our contributing guide, please file an issue first:
https://github.com/ag-ui-protocol/ag-ui/issues
Or, reach out to us on Discord: https://discord.com/invite/6dffbvGU3D
You can learn more about contributing to copilotkit here:
https://github.com/copilotkit/copilotkit/blob/master/CONTRIBUTING.md
Happy contributing!
-->
## What does this PR do?
(Describe the changes introduced in this PR)
## Related PRs and Issues
- (Direct link to related PR or issue, if relevant)
## Checklist
- [ ] 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
- [ ] "Allow edits by maintainers" is checked (lets us help iterate on
your PR directly — faster turnaround for everyone)
Same class as the hitl-in-app fix: turnIndex:0 emit legs get re-served on the
post-tool-result turn under aimock's default (relaxed) turn-index selection,
re-firing the frontend tool instead of narrating.
frontend-tools-async (query_notes): add hasToolResult:false to the 4 emit legs
(3 pills + the 'project planning' catch-all) so they match the emit turn and are
excluded once the tool result returns. The toolCallId narration legs round-trip
fine here (verified on the wire) and are unchanged.
Verified under relaxed aimock: pill -> single result card + narration, no
re-emit, no error (was: double card + 'toLowerCase' error).
subagents.json left as-is: its turnIndex 1/2 legs are intentional server-side
step sequencing, not the client-re-POST pattern.
open-gen-ui / open-gen-ui-advanced NOT touched here: their real fixtures live in
gen-ui-open.json / gen-ui-open-advanced.json (already hasToolResult-gated, no
turnIndex). The stale duplicate generateSandboxedUi entries in
gen-ui-tool-based.json are a separate cleanup, tracked outside this PR.
The factory automation extracts curated snippets via @region markers.
readonly_state_agent_context.py and shared_state_read_write.py were byte-aligned
to langgraph-python except their @region markers had been stripped, so the
factory would extract nothing for those two regions. Restore them to match LGP:
- agent-context-setup around create_agent in readonly_state_agent_context.py
- shared-state-setup around create_agent in shared_state_read_write.py
Comment-only; no behavior change. Audit confirms all 49 fastapi region pairs are
now balanced and LGP-consistent (0 orphans/typos/mismatches).
Writing a per-hook threadId onto an agent resolved by agentId alone mutates a
shared singleton, so two useAgent calls that share an agentId clobber each
other's thread (review feedback from @mme). Require runtimeAgentId when threadId
is provided: the hook then registers a private proxied agent (agentId ->
runtimeAgentId via CopilotKitCore.registerProxiedAgent) and scopes the threadId
to that instance instead of a shared one. Register/unregister run as one
balanced, StrictMode-safe effect, exposing the proxy via state so the hook
swaps from the provisional stand-in deterministically. Passing threadId without
runtimeAgentId now throws. Updates the React Native demo to the new API.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
fe-parity.ts was a scratch parity checker for this alignment work; it is no
longer needed in the worktree. Drop it and de-reference it from
langgraph-fastapi/PARITY_NOTES.md (reword the intro, convert the
machine-readable fe-parity-allow allowlist into a plain human-readable
'sanctioned file-level divergences' list). No other references existed. D6
behavior remains the parity judge.
The byoc D6 featureType covers both declarative demos; the grid cell is green
(hashbrown route+pill). A scoped declarative-json-render --d6 run reds for two
non-fastapi reasons: (1) the shared d5-byoc probe always sends the hashbrown
pill even on the json-render page (harness limitation — build context lacks
demos[]; tracked as a separate follow-up), and (2) fastapi deliberately keeps
hashbrown (@hashbrownai/react) and json-render (@json-render/react) as separate
library integrations with their own pills/fixtures, vs LGP's unified
render_dashboard contract. Sanctioned; do not collapse fastapi's json-render to
the unified contract (content downgrade) — the real fix is probe-side.
aimock replays scripted tool calls keyed on userMessage+context regardless of
which graph ran or its tool set, so a demo mis-wired to the sample_agent
fallback passes D6 green while breaking against a real model (OSS-582:
gen-ui-tool-based looped on query_data; shared-state-streaming wrote only to
chat; agentic-chat ran with the wrong tool set). Document that D6-green is
necessary but not sufficient, the real-LLM live click-through procedure, and
the structural tell (route.ts fallthrough loop vs LGP's dedicated wiring).
Live QA surfaced demos that behaved wrong on fastapi because they fell back to
the generic sample_agent (or were unregistered) instead of the dedicated graph
LGP uses. aimock D6 masked these (fixtures script the tool calls), so they were
green in the grid but broken against a real model. Audit of fastapi's
agentNames fallthrough vs LGP's neutralAssistantCells found exactly these:
- gen-ui-tool-based: port gen_ui_tool_based graph (tools=[], frontend supplies
render_bar/pie_chart via useComponent). Was sample_agent, whose query_data
tool + prompt made the model loop on data queries instead of rendering.
- shared-state-streaming: port shared_state_streaming graph (StateStreaming
middleware + write_document tool + document state). Was sample_agent, which
never emits state.document, so it only wrote to chat. Remove from
not_supported_features (now works, D6 green).
- agentic-chat: port agentic_chat graph (tools=[]). Was sample_agent (7+ tools).
- threadid-frontend-tool-roundtrip: wire to frontend_tools (was unregistered).
- reasoning-custom: align reasoning_agent config to LGP (gpt-5.4 / effort
medium / summary detailed; were gpt-5-mini / low / auto).
Register the 3 new graphs in langgraph.json; remove the 3 names from the
sample_agent fallthrough loop in route.ts. D6 green x2 for reasoning-display,
gen-ui-custom, shared-state-streaming, agentic-chat; agent-config +
tool-rendering-reasoning-chain re-verified (no regression).
Soften the stale 'D6 green'/'under investigation' wording. A live trace
confirmed the a2ui-recovery surface-missing D6 red reproduces identically on
the langgraph-python north star (shared Python ag_ui_langgraph recovery loop;
mastra's TS impl is green), so fastapi is already behaviorally aligned and
there is nothing to fix under integrations/langgraph-fastapi/. Fixing the
shared loop is a separate PR against the north star / the package.
The demo-browser overview (page.tsx, identical to LGP) groups cards by
demo.tags[0] and orders within a group by manifest.features[] index, so the
card arrangement is fully driven by manifest.yaml. fastapi's manifest was an
older curation: a different naming scheme (~26 demos), deprecated feature/demo
ids (agentic-chat-reasoning, reasoning-default-render, byoc-hashbrown,
byoc-json-render, hitl, phantom hitl-in-chat-booking), divergent tags/order,
and a missing shared-state-streaming/shared-state-read card.
Align manifest.yaml demos+features to LGP (names, descriptions, tags, order,
canonical ids), preserving fastapi identity (name/slug/logo/description) and
the sanctioned a2ui-recovery prompt divergence. Overview is now card-for-card
identical to langgraph-python.
Canonicalizing the ids pulled two cells into the D6 tested set that were
mis-wired to the old names; wire them to LGP's shape:
- reasoning-custom/reasoning-default -> reasoning_agent in copilotkit/route.ts
(were wired under agentic-chat-reasoning/reasoning-default-render).
- rename api routes copilotkit-byoc-{hashbrown,json-render} ->
copilotkit-declarative-{hashbrown,json-render}; align endpoints + the
declarative-hashbrown-demo agent id to what the demo pages request.
Full D6 sweep: 37 green / 1 red; reasoning-display and byoc now green (two
real runs each); the lone red is a2ui-recovery (known shared north-star
defect, identical on LGP).
The three turn-0 pill fixtures (refund/downgrade/escalate) were keyed on
turnIndex:0. aimock's default (relaxed) turn-index selection re-serves a
turnIndex-only fixture on the post-approval turn, so approving a
useFrontendTool(request_user_approval) call re-emitted the tool call
instead of narrating the outcome — the turn never settled (stuck
text-unstable).
Re-key each emit fixture on hasToolResult:false instead of turnIndex:0.
It matches the emit turn (no tool result yet) and is excluded on the
post-approval turn (hasToolResult:true), where the toolCallId narration
fixture wins. No turnIndex dependency, so it is correct under both strict
and relaxed aimock, and multi-pill-safe.
Not a bridge/runtime bug: @ag-ui/mastra loops back correctly on real
OpenAI and under strict aimock. Fault was fixture selection only.
Two independent fastapi divergences from the green north-star kept this red:
1. Missing chunkSize:9999 on the 8 tool-call fixtures. Under aimock's global
8-byte chunking the large tool-call args failed to JSON-parse in one piece,
so the AAPL->MSFT reasoning chain stopped at leg 1 (turns 1 & 2). Byte-align
to LGP (and the green mastra/langgraph-typescript siblings carry the same
pattern).
2. Fixture-pool shadowing broke turn 3. aimock pools all d4+d6 fixtures for a
context and matches by userMessage substring, first-match-wins in load order
(d4 before d6). fastapi had broad keys LGP doesn't:
- d4/chat.json: 'weather' -> 'weather in San Francisco', 'flights from SFO
to JFK' -> 'Find flights from SFO to JFK.' (period-terminated so it stops
being a substring of turn 3's 'Find flights from SFO to JFK and show me
the weather there.').
- tool-rendering-{custom,default}-catchall.json: bare 'Find flights' ->
'Find flights from SFO to JFK.' (now matches LGP's exact key).
Also byte-align the backend agent to LGP: scriptable get_stock_price signature,
the detailed chaining system prompt, model gpt-5.4, reasoning summary detailed.
D6 tool-rendering-reasoning-chain green (two real ~21s runs). Shared-fixture
regression all green: agentic-chat, tool-rendering, tool-rendering-custom-catchall,
tool-rendering-default-catchall, headless-complete (Tokyo-weather consumer).
## What
Ports `ms-agent-harness-dotnet`'s `ApiKeyResolver` into
`ms-agent-dotnet` so its 15 agent factories resolve the OpenAI
credential as `OPENAI_API_KEY` (env) → `config[OPENAI_API_KEY]` →
`GitHubToken`, and the endpoint via `OPENAI_BASE_URL` → default —
instead of hardcoding `configuration["GitHubToken"]` in each agent.
## Why
`ms-agent-dotnet`'s main chat clients authenticated **only** with the
GitHub token. On a fixture-miss fall-through, aimock proxies to real
`api.openai.com`, which rejects a GitHub token (`invalid_api_key`,
surfaced as HTTP 502) — the ms-agent-dotnet staging outage.
`ms-agent-harness-dotnet` already works because it resolves
`OPENAI_API_KEY` first; this brings `ms-agent-dotnet` to parity.
Interim showcase-side mitigation while the aimock cross-provider guard
(**PNI-108** / CopilotKit/aimock#340) is deferred. See also PNI-109
(inject `x-aimock-context`) and PNI-110 (per-request upstream routing).
## Changes
- Copy `agent/ApiKeyResolver.cs` verbatim from the harness column (code
copy, no new dependency)
- Rewire 15 factories + `A2uiSecondaryToolCaller` → `ResolveApiKey` /
`ResolveEndpoint`; drop each file's now-dead `DefaultOpenAiEndpoint`
const (−162/+47)
- Add `tests/ApiKeyResolverTests.cs` — 11 cases (precedence,
env-over-config, whitespace-skip, mock-endpoint fallback, non-mock
fail-fast)
## Verification
- `dotnet build` → **0 errors / 0 warnings**
- `dotnet test` → **76/76** (incl. 11 new resolver tests)
- `dotnet format whitespace --verify-no-changes` → clean
- **Live**: built the client exactly as the agents do, resolved a real
`OPENAI_API_KEY`, called `api.openai.com` → **200** (confirms the 502
path is fixed). Throwaway smoke, not committed.
## ⚠️ Operational follow-up (required to flip staging/prod)
`showcase-ms-agent-dotnet` must have a valid **`OPENAI_API_KEY`** set in
Railway (as `ms-agent-harness-dotnet` already does). This code enables
the behavior; the env var makes it live.
## Not run here
Showcase Docker value-test (`bin/showcase test ms-agent-dotnet:… --d6`)
— needs the container stack; worth running before merge.
Poetry's ^0.0.8 on a 0.0.x version resolves to >=0.0.8,<0.0.9,
pinning users to exactly 0.0.8 and blocking newer compatible
releases (0.0.9, 0.1.0, 1.0.0, 1.1.0). Relax to >=0.0.8,<2.0.0
so the SDK can pick up fixes/features while still gating on the
next major (2.0.0) as the breaking boundary.
The only SDK usage is partialjson.json_parser.JSONParser().parse(),
which is unchanged across all published versions up to 1.1.0.
Relates to #4131
The auto-format bot reflowed both captured registry payloads. Harmless this
time — oxfmt only rewraps arrays, it does not reorder keys — but the whole
value of these two files is that they are verbatim `imagetools inspect`
output. A formatter in the loop means the next re-capture churns, and the
"this is exactly what the registry emits" claim stops being checkable.
Exempt the fixture directory in .oxfmtrc.json, restore the captured bytes,
and say so in the provenance comment so nobody tidies them by hand.
The previous suite was 37/37 green against a workflow that could not have
run at all: the guard step had no `packages: write` and no GHCR login, so
every `imagetools create` would have 401'd. Asserting that a step EXISTS
proves nothing about whether it can succeed. Each change below was checked
by reintroducing the defect and confirming the test goes red.
Registry-auth precondition (the miss that let the above ship)
- Both redeploy jobs must declare `packages: write` AND a `docker/login-action`
step for ghcr.io, ordered BEFORE the guard. Dropping either reds the suite.
The intersection, executed rather than restated
- The old test only checked that the string `images=` appears in the compute
step; swapping its jq for the full matrix — so a FAILED build moves
`:latest` — kept it green. The real `changed` shell now RUNS, against the
real ALL_SERVICES matrix read out of the workflow, and the emitted
$GITHUB_OUTPUT is asserted: a failed build is in neither set, and the
`skip_build` slot is in `services` but NOT in `images` (handing it to the
guard fails "manifest unknown" and blocks the redeploy for the whole fleet).
Same treatment for the starter lane.
Fixtures joined to real registry output
- `extractRevisionLabel` was pinned to hand-written payloads that were never
compared with reality. Since `readLatestRevision` maps every failure to
null, and null ADVANCES, a parser that silently never matches yields a
permanently-blind guard with a fully green suite. Both fixtures are now
verbatim `docker buildx imagetools inspect --format '{{json .Image}}'`
output (buildx v0.35.0), unformatted so key order survives: a platform-keyed
multi-arch image carrying the label, and our own `:latest`, which turns out
to be a bare config object with NO labels at all. The label key used in the
injection test is read from the workflow's own `labels:` input, so parser
and producer cannot drift apart silently.
Closed vacuities
- The `:latest` ban read one step's `tags` and was vacuously green on an empty
list. It now covers every tagged step plus hand-rolled `docker push`/`docker
tag`/`imagetools create`, and fails on an empty `tags`.
- The `cancel-in-progress` ban read only top-level config; a job-level
`concurrency` on `build` reproduced the exact harm and stayed green. All
jobs are checked now.
- `already-current` is keyed on the digest, matching the script.
- The unreachable `(null, "ahead")` row is labelled as the defensive
input-space case it is, and a new test pins that the real flow never calls
compare() with an unknown revision.
New coverage: classifyProbeFailure (incl. a REAL execFileSync timeout, and
the two precedence traps — gh's rate-limited 403 is throttling, not auth; a
registry's 404-with-denied is auth, not absent), readFlag, escapeAnnotationData
(a forged `\n::error::` stays inert), isDirectInvocation through a symlink,
the `::error` annotation on fleet failure, and the digest-mismatch advance.
Also: GITHUB_SHA / GITHUB_REPOSITORY are no longer shadowed in the workflow.
The runner exports both and the script reads process.env, so the old
assertions pinned a redundancy rather than a capability — removed together,
as the comment there required. Workflow-reading scaffolding duplicated with
redeploy-guard.test.ts is extracted and the YAML parse memoized.