Files
hermes-agent/tests
nftpoetrist 3d59486a86 fix(gateway): fold route + authenticated principal into idempotency key
Review on this PR (tracked issue #84256) found the profile-only fix left
two more dimensions of the shared OpenAI-compat Idempotency-Key cache
unscoped:

1. No endpoint/route discriminator. _run_idempotent() is shared by both
   POST /v1/chat/completions and POST /v1/responses, but _IdempotencyCache
   ._store keeps the fingerprint only as a value inside the key's slot —
   not part of the key itself. A client reusing one Idempotency-Key across
   both routes would have the second route's settled response silently
   overwrite the first route's storage slot, so a legitimate retry of the
   first request would recompute instead of replaying its own cached
   result. Each call site now passes a stable logical route discriminator
   ("chat_completions" / "responses") folded directly into the cache key,
   rather than relying on request.path (which would wrongly split /v1/...
   and its /p/<profile>/v1/... alias into different namespaces).

2. No authenticated principal. The sibling durable /v1/runs admission API's
   _run_idempotency_scope() already composes
   sha256(profile \0 expected-api-key-or-sentinel) for its non-room case.
   _run_idempotent() now delegates directly to
   self._run_idempotency_scope(request) for its principal/profile scope
   instead of re-deriving the profile alone, so both endpoints share one
   definition of "who is this request for" and a caller whose
   API_SERVER_KEY changes mid-cache-lifetime can no longer replay a
   previous principal's cached response.

Adds regression tests proving: route isolation (including the exact
chat -> responses -> chat retry scenario from the review), concurrent
cross-profile inflight isolation, principal/key-rotation isolation, and
that same-route/same-profile/same-principal dedupe is unchanged.

Mutation-verified with three independent negative controls: reverting the
whole diff (TypeError on the now-required route kwarg), keeping route as a
parameter but dropping it from the key (only the two route-isolation tests
fail), and keeping the shared-scope call but reverting it to a bare
profile lookup (only the two principal-isolation tests fail).
2026-09-11 19:37:59 -07:00
..