Skip to content

feat: credential-aware upstream selection on proxy-on-miss (refuse mismatches, route GitHub Models) - #340

Closed
contextablemark wants to merge 2 commits into
mainfrom
fix/proxy-cross-provider-credential-guard
Closed

feat: credential-aware upstream selection on proxy-on-miss (refuse mismatches, route GitHub Models)#340
contextablemark wants to merge 2 commits into
mainfrom
fix/proxy-cross-provider-credential-guard

Conversation

@contextablemark

@contextablemark contextablemark commented Jul 26, 2026

Copy link
Copy Markdown

Problem

Two related failure modes on a fixture miss in --proxy-only / --record mode, both rooted in aimock picking the proxy upstream by request surface while the caller's credential may belong to a different provider:

  1. 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_…) reaching api.openai.com because a fixture was missing and --proxy-only fell through — the real credential is relayed to the wrong vendor, which rejects it with invalid_api_key. aimock normalizes that to a 502: a confusing OpenAI auth error, and the credential has already left in transit.

  2. 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.com with a GitHub PAT as the bearer token — so aimock classifies the request as openai. On a single shared aimock instance, --provider-openai is one global URL (it can't be api.openai.com for 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:

  • Route (PNI-110). When the caller's bearer token is detected as a GitHub credential, the request is routed as openai, and a --provider-github upstream is wired, aimock reassigns the effective provider to a first-class github and forwards the PAT to GitHub Models. URL lookup, auth scheme, the compat guard, and egress metrics all realign off the effective key.
  • Refuse (PNI-108). When the credential and the upstream host are confidently classified to different provider families and no matching upstream is configured (e.g. a GitHub token bound for api.openai.com), aimock refuses with a clear 502 proxy_refused — no upstream call, no leak.
  • Fails OPEN on any uncertainty: unknown/dummy (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.
  • Escape hatch: AIMOCK_ALLOW_CROSS_PROVIDER_PROXY=1.

Net: an unmatched GitHub-Models prompt falls through successfully to real GitHub Models when --provider-github is 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 github provider (PNI-110)

  • --provider-github <url> CLI flag, AIMOCK_PROVIDER_GITHUB_KEY env var, bearer auth scheme, added to RecordProviderKey.
  • Opt-in — inert unless --provider-github is wired, consistent with the existing providerKeys design.
  • Credential-aware remap in proxyAndRecord via a new exported detectBearerCredentialProvider that reads the raw Authorization header, so no buildForwardHeaders reorder on the hot path.
  • Fixture recording stays under the original surface key (openai) so replay — which re-classifies by surface — still matches; only egress (upstream/auth/guard/metrics) uses the effective github key.
  • The refusal message now names --provider-github instead of the misleading "point --provider-openai at the correct host" (impossible on a shared instance).

Guard internals (PNI-108)

  • Pure helpers in 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 as github, so the github→github route is allowed and only the mismatched host is refused.
  • Enforced in recorder.ts proxyAndRecord after applyProviderAuth, 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_present surfaces requests proxying with no X-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 new detectBearerCredentialProvider header helper), host classification, the compat decision (incl. the exact GitHub-token → api.openai.com case), fail-open branches, non-bearer schemes, the escape hatch, the --provider-github remedy message, and integration tests proving a GitHub PAT routes to the github upstream (not openai) while a real sk- 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; tsdown build (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

  • Linear: PNI-108 (the refuse half) + PNI-110 (the route half) — folded into one PR since PNI-108 had not merged and the two are two halves of one mechanism.
  • PNI-109 removes the miss for curated demo prompts via x-aimock-context injection; this PR covers the residual unmatched/free-text GitHub-Models prompts that legitimately fall through to a live LLM.

@pkg-pr-new

pkg-pr-new Bot commented Jul 26, 2026

Copy link
Copy Markdown

Open in StackBlitz

npm i https://pkg.pr.new/@copilotkit/aimock@340

commit: f675b7f

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
contextablemark force-pushed the fix/proxy-cross-provider-credential-guard branch from bbb8fc7 to 7cf08a9 Compare July 26, 2026 08:11
@linear

linear Bot commented Jul 26, 2026

Copy link
Copy Markdown

PNI-108

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).
@contextablemark contextablemark changed the title fix: refuse cross-provider credential forwarding on proxy-on-miss feat: credential-aware upstream selection on proxy-on-miss (refuse mismatches, route GitHub Models) Jul 27, 2026
@contextablemark

Copy link
Copy Markdown
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.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant