Commit Graph

6 Commits

Author SHA1 Message Date
Jordan Ritter 12fbc78802 fix(showcase/ms-agent-harness-dotnet): use Uri.IsLoopback for robust loopback detection
The previous wave-4 fix added `host == "::1"` for IPv6 loopback support,
but verification against `mcr.microsoft.com/dotnet/sdk:9.0` showed that
`new Uri("http://[::1]:8000/").Host` returns `"[::1]"` WITH brackets, not
`"::1"`. The string-equality check therefore never matched and IPv6
loopback detection was silently broken.

Switch to `Uri.IsLoopback`, which is the framework-provided helper that
robustly identifies all loopback variations: `localhost`, the entire
`127.0.0.0/8` range, `::1` in any bracketed form, and IPv4-mapped IPv6
loopback (`::ffff:127.0.0.1`). This sidesteps `Uri.Host`'s bracket-
formatting quirks entirely.

The explicit `0.0.0.0` (unspecified address, kept for back-compat) and
`aimock` (Docker-compose service name) checks are retained because they
are not loopback addresses.
2026-05-26 13:36:38 -07:00
Jordan Ritter 973c6dc800 fix(showcase/ms-agent-harness-dotnet): treat empty-string env vars as absent in ApiKeyResolver
ResolveApiKey and ResolveEndpoint previously cascaded through env var →
configuration sources using the `??` operator, which only short-circuits
on null. When `OPENAI_API_KEY=""` or `OPENAI_BASE_URL=""` was set in the
environment, the empty string was returned by the cascade WITHOUT
consulting the configuration fallbacks (`configuration["OPENAI_API_KEY"]`,
`configuration["GitHubToken"]`, `configuration["OPENAI_BASE_URL"]`),
masking the configured values entirely. The downstream
`IsNullOrWhiteSpace` check would then push the resolver into the
mock-or-throw path even when a valid key was configured.

Replace the `??` chains with a `FirstNonBlank` helper that treats both
null and whitespace-only candidates as absent, so empty env vars fall
through to the configuration fallbacks as intended.
2026-05-26 13:36:38 -07:00
Jordan Ritter fc1be111b0 fix(showcase/ms-agent-harness-dotnet): correct IPv6 loopback host check
ApiKeyResolver.IsMockEndpoint compared Uri.Host against "[::1]", but
System.Uri.Host strips the surrounding brackets and returns "::1" for
inputs like http://[::1]:8000/. The bracketed comparison was therefore
dead code: developers running aimock on IPv6 loopback got the fail-fast
error instead of the intended mock-key fallback.

Change the literal to "::1" and update the inline + docstring comments
to reflect the bracket-less form.
2026-05-26 13:36:38 -07:00
Jordan Ritter 3366674c3b fix(showcase/ms-agent-harness-dotnet): tighten IsMockEndpoint and reject whitespace API keys
Two surgical hardening fixes to ApiKeyResolver:

1. IsMockEndpoint: drop host.StartsWith("aimock.", ...) and
   host.EndsWith(".aimock", ...). The StartsWith match is exploitable
   — an attacker-registered domain like aimock.attacker.example.com
   would be classified as a mock endpoint and bypass the fail-fast,
   silently returning the mock key. The EndsWith match is unused
   noise (no .aimock TLD exists). Only exact, well-known mock hosts
   ("localhost", "127.0.0.1", "0.0.0.0", "[::1]", "aimock") are
   accepted. IPv6 loopback [::1] is added so dual-stack dev paths
   are still covered.

2. ResolveApiKey: change !string.IsNullOrEmpty(apiKey) to
   !string.IsNullOrWhiteSpace(apiKey). A whitespace-only key such as
   " " would otherwise be accepted as valid, bypassing the mock-key
   /fail-fast logic entirely and sending a bogus key upstream.
2026-05-26 13:36:38 -07:00
Jordan Ritter 2e33d58a63 fix(showcase/ms-agent-harness-dotnet): tighten IsMockEndpoint to host-only match
The previous implementation used Contains() substring matching against the
full endpoint URL, which is exploitable. An attacker-controlled endpoint
such as https://attacker.example.com/aimock-decoy or
https://api.openai.com/?env=localhost would be classified as a mock and
bypass the fail-fast guard, silently returning the sk-mock-local key for
what is actually a non-mock destination.

Parse the URL and inspect only the host component, matching exact dev
hosts (localhost, 127.0.0.1, 0.0.0.0, aimock) plus aimock subdomains.
Path, query, and arbitrary subdomain segments containing "aimock" or
"localhost" no longer trigger the mock fallback.
2026-05-26 13:36:38 -07:00
Jordan Ritter a939054d8a fix(showcase/ms-agent-harness-dotnet): harden A2uiSecondaryToolCaller + centralize API key resolution
- Replace silent `return null` paths in A2uiSecondaryToolCaller with
  TryGetProperty guards that each emit a structured LogWarning naming
  the exact missing/unexpected field (choices, message, tool_calls,
  function, name mismatch, arguments). Callers can now tell why a
  design-tool call produced no content.
- Add ILogger parameter to GetDesignToolArgumentsAsync and pass
  BeautifulChatAgent._logger from the single call site so warnings
  flow into the existing log stream.
- Log the response body (truncated to 1 KB) at LogWarning before
  EnsureSuccessStatusCode throws, so upstream error payloads survive
  the throw and reach operators.
- Extract the OPENAI_API_KEY/GitHubToken/sk-mock-local fallback chain
  from Program.cs and A2uiSecondaryToolCaller.cs into a new
  ApiKeyResolver helper. Both call sites now share one implementation.
- ApiKeyResolver fails fast with InvalidOperationException + LogCritical
  when no real key is present and OPENAI_BASE_URL is not an
  aimock/localhost endpoint, so misconfigured prod deploys cannot
  silently send sk-mock-local to a real LLM provider. The silent
  mock-key fallback is preserved for aimock/localhost dev endpoints.
2026-05-26 13:36:38 -07:00