feat: credential-aware upstream selection on proxy-on-miss (refuse mismatches, route GitHub Models) - #340
Closed
contextablemark wants to merge 2 commits into
Closed
Conversation
commit: |
On a fixture miss in --proxy-only/--record mode, aimock forwarded the caller's real
credential verbatim to the configured upstream. A credential that clearly belongs to a
different provider (e.g. a GitHub token routed to api.openai.com) was relayed to the wrong
vendor, which rejected it with a confusing invalid_api_key that aimock normalized to a 502
— leaking the credential in transit.
Add a fail-safe guard: refuse to forward only when BOTH the credential and the upstream
host are confidently classified to different provider families; fail OPEN otherwise
(unknown/dummy keys, custom/self-hosted hosts, signed/OAuth providers) so legitimate setups
are never blocked. GitHub Models hosts classify as github, so a GitHub token to
models.inference.ai.azure.com is allowed. Escape hatch: AIMOCK_ALLOW_CROSS_PROVIDER_PROXY=1.
Add proxy observability metrics: aimock_proxy_refused_total and
aimock_proxy_forwarded_total{provider,context_present}.
contextablemark
force-pushed
the
fix/proxy-cross-provider-credential-guard
branch
from
July 26, 2026 08:11
bbb8fc7 to
7cf08a9
Compare
GitHub Models is OpenAI-wire-compatible: a github_pat_… token arrives on
the /v1/chat/completions surface and is classified as providerKey "openai".
On a fixture-miss that meant proxying to --provider-openai (api.openai.com),
which rejects the PAT — and the PNI-108 guard then (correctly) refuses the
github→openai mismatch, so the legitimate live fall-through 502s.
Add a first-class "github" provider (bearer scheme, --provider-github,
AIMOCK_PROVIDER_GITHUB_KEY) and credential-aware upstream selection in
proxyAndRecord: when the request routes as "openai", a github upstream is
wired, and the caller's bearer token is detected as a GitHub credential,
reassign the effective provider key to "github" so the URL lookup, auth
scheme, compat guard, and egress metrics all realign and the PAT reaches
GitHub Models. Opt-in — inert unless --provider-github is set, in which case
the PNI-108 guard still refuses every residual mismatch (github-with-no-
upstream, and other foreign creds on the openai surface).
Fixture recording stays under the original surface key ("openai") so replay,
which re-classifies by surface, still matches. The refusal message now names
--provider-github instead of the misleading --provider-openai repoint.
Detection reuses PNI-108's detectCredentialProvider via a new exported
detectBearerCredentialProvider helper that reads the raw Authorization header,
avoiding a buildForwardHeaders reorder on the hot path.
Refs PNI-110 (composes with PNI-108).
Author
|
There's a workaround in the MS Harness Agent that avoids the need for the Github Models endpoint. I'll use that instead. Closing this for now. |
contextablemark
added a commit
to CopilotKit/CopilotKit
that referenced
this pull request
Jul 27, 2026
…yResolver Port ms-agent-harness-dotnet's ApiKeyResolver into ms-agent-dotnet so the 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"] per agent. Previously 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 the GitHub token (invalid_api_key, surfaced as 502). ms-agent-harness-dotnet already works because it resolves OPENAI_API_KEY first; this brings ms-agent-dotnet to parity so its fall-through returns 200. Interim showcase-side mitigation while the aimock cross-provider guard (PNI-108, CopilotKit/aimock#340) is deferred. - Copy agent/ApiKeyResolver.cs verbatim from ms-agent-harness-dotnet (code copy, no new dependency) - Rewire 15 factories + A2uiSecondaryToolCaller to ResolveApiKey/ResolveEndpoint; drop the dead per-file DefaultOpenAiEndpoint const - Add tests/ApiKeyResolverTests.cs (precedence, mock-endpoint fallback, non-mock fail-fast) Verified: dotnet build 0/0, dotnet test 76/76, whitespace format clean, and a live OpenAI call through the resolver returned 200.
ranst91
added a commit
to CopilotKit/CopilotKit
that referenced
this pull request
Jul 27, 2026
…yResolver (#6175) ## 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.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Problem
Two related failure modes on a fixture miss in
--proxy-only/--recordmode, both rooted in aimock picking the proxy upstream by request surface while the caller's credential may belong to a different provider:Credential leak to the wrong vendor (PNI-108). aimock forwards the caller's credential verbatim to the configured upstream. When the credential clearly belongs to a different provider than the upstream — e.g. a GitHub Models token (
github_pat_…) reachingapi.openai.combecause a fixture was missing and--proxy-onlyfell through — the real credential is relayed to the wrong vendor, which rejects it withinvalid_api_key. aimock normalizes that to a502: a confusing OpenAI auth error, and the credential has already left in transit.Legitimate GitHub-Models fall-through 502s (PNI-110). GitHub Models is OpenAI-wire-compatible — clients point a stock OpenAI SDK at
https://models.inference.ai.azure.comwith a GitHub PAT as the bearer token — so aimock classifies the request asopenai. On a single shared aimock instance,--provider-openaiis one global URL (it can't beapi.openai.comfor real-OpenAI integrations and GitHub Models at the same time), so an unmatched/free-text GitHub-Models prompt that should legitimately fall through to a live LLM instead hits OpenAI and 502s. aimock must pick the upstream per request, not per process.This is the failure mode we hit debugging the CopilotKit showcase: real browser traffic reached aimock without
x-aimock-context, missed every context-scoped fixture, and got proxied to OpenAI carrying a GitHub token.Fix — one mechanism, two outcomes
Credential-aware upstream selection. aimock inspects the outgoing credential and picks the upstream per request:
openai, and a--provider-githubupstream is wired, aimock reassigns the effective provider to a first-classgithuband forwards the PAT to GitHub Models. URL lookup, auth scheme, the compat guard, and egress metrics all realign off the effective key.api.openai.com), aimock refuses with a clear502 proxy_refused— no upstream call, no leak.sk-aimock-…) credentials, custom/self-hosted hosts, and signed/OAuth providers (Bedrock SigV4, Vertex OAuth) are never blocked, so legitimate/self-hosted setups are unaffected.AIMOCK_ALLOW_CROSS_PROVIDER_PROXY=1.Net: an unmatched GitHub-Models prompt falls through successfully to real GitHub Models when
--provider-githubis set, and is refused cleanly (never leaked to OpenAI) when it isn't. Route is the configured case; refuse is the general default of the same selection.First-class
githubprovider (PNI-110)--provider-github <url>CLI flag,AIMOCK_PROVIDER_GITHUB_KEYenv var,bearerauth scheme, added toRecordProviderKey.--provider-githubis wired, consistent with the existingproviderKeysdesign.proxyAndRecordvia a new exporteddetectBearerCredentialProviderthat reads the rawAuthorizationheader, so nobuildForwardHeadersreorder on the hot path.openai) so replay — which re-classifies by surface — still matches; only egress (upstream/auth/guard/metrics) uses the effectivegithubkey.--provider-githubinstead of the misleading "point--provider-openaiat the correct host" (impossible on a shared instance).Guard internals (PNI-108)
provider-auth.ts:detectCredentialProvider(key-prefix → family),classifyUpstreamHost(host → family),checkCredentialUpstreamCompat(the fail-open decision). GitHub Models hosts (models.inference.ai.azure.com,models.github.ai) classify asgithub, so the github→github route is allowed and only the mismatched host is refused.recorder.tsproxyAndRecordafterapplyProviderAuth, protecting every provider handler that proxies through that path. No runtime dependencies added.Observability
aimock_proxy_refused_total{provider,reason}aimock_proxy_forwarded_total{provider,context_present}—context_presentsurfaces requests proxying with noX-AIMock-Context(i.e. every context-scoped fixture missed).Both labels use the effective provider, so a github-routed forward is labeled
provider="github".Tests
src/__tests__/credential-guard.test.ts— 38 tests: credential detection (incl. the newdetectBearerCredentialProviderheader helper), host classification, the compat decision (incl. the exact GitHub-token →api.openai.comcase), fail-open branches, non-bearer schemes, the escape hatch, the--provider-githubremedy message, and integration tests proving a GitHub PAT routes to the github upstream (not openai) while a realsk-key still goes to openai. The refusal integration test makes no real network call — the guard short-circuits before any socket is opened.Full suite: 4806 passed / 3 skipped; prettier + eslint clean;
tsdownbuild (typecheck) green.Deployment
Inert until the shared showcase aimock instance is started with
--provider-github https://models.inference.ai.azure.com. That single flag is what turns the unmatched-GitHub-Models 502s into successful live fall-through.Related
x-aimock-contextinjection; this PR covers the residual unmatched/free-text GitHub-Models prompts that legitimately fall through to a live LLM.