fix(training-agent): enforce idempotency replay/conflict/expired semantics (#2346)#2367
Merged
fix(training-agent): enforce idempotency replay/conflict/expired semantics (#2346)#2367
Conversation
…ntics (#2346) The hosted training agent declared adcp.idempotency.replay_ttl_seconds in its capabilities response but did not enforce the contract, silently double-booking retries with the same key. Middleware now validates key presence + format, returns cached responses with replayed: true on payload match, IDEMPOTENCY_CONFLICT on payload drift, IDEMPOTENCY_EXPIRED past TTL. Cache is scoped by (auth principal, account, key) so shared sandbox tokens cannot be used as a cross-caller oracle. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…tion or class' Co-authored-by: Copilot Autofix powered by AI <223894421+github-code-quality[bot]@users.noreply.github.com>
This was referenced Apr 19, 2026
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.
Closes #2346.
Summary
The hosted training agent at
test-agent.adcontextprotocol.orgadvertisedadcp.idempotency.replay_ttl_seconds: 86400inget_adcp_capabilitiesbutdid not enforce the contract. Two
create_media_buycalls with the same keyand same payload created two distinct media buys; key reuse with a different
payload succeeded instead of returning
IDEMPOTENCY_CONFLICT. Buyer SDKsintegrating against the reference agent shipped without ever exercising
their retry/conflict code paths.
A new middleware at
server/src/training-agent/idempotency.tsimplements thenormative behavior from
docs/building/implementation/security.mdx§"Request Safety" and the
static/compliance/source/universal/idempotency.yamlstoryboard:
^[A-Za-z0-9_.:-]{16,255}$regex checkedbefore the cache lookup on all 26 mutating tools; the
MUTATING_TOOLSsetis drift-guarded by a test that introspects
tools/listand asserts everytool whose schema requires
idempotency_keyis listed.canonicalizenpm package +SHA-256. Excludes
idempotency_key,context,governance_context, andpush_notification_config.authentication.credentials. Hash comparisonuses
timingSafeEqualon decoded digests.inner response with
replayed: trueon the envelope (omitted on freshexecutions so strict
additionalProperties: falseschemas keep passing).IDEMPOTENCY_CONFLICTwith code + message only; no cached payload, hash, or
fieldpointer(any shape hint would turn key-reuse into a read oracle).
IDEMPOTENCY_EXPIREDand evict.responses (
{ errors: [...] }) are not cached.(auth principal, account scope, idempotency_key). Thepublic sandbox token is shared across all callers, so scoping only by
auth principal would make the three-state response observable
cross-caller (security.mdx §"three-state response"); per-account
partitioning closes that oracle.
expired entries on overflow. When the cap is still hit, responds with
RATE_LIMITED(rule 8) rather than silently dropping the insert andletting a retry re-execute.
Review cycle
Both
code-reviewerandsecurity-reviewerpointed out real issues thatlanded in the final diff:
structuredContenton replay path (code-review M2)security M1 + M2)
RATE_LIMITED(code M5 / security S2)field: idempotency_keyfrom conflict body (code M7)timingSafeEqualfor hash compare (security S1)''(code N10)MUTATING_TOOLSvstools/list(code M1)Test plan
npm run typecheck— cleannpx vitest run server/tests/unit/— 1569 pass (56 new inidempotency.test.ts+training-agent-idempotency.test.ts)npm run test:unit— 587 passnpm run test:schemas,test:json-schema— pass🤖 Generated with Claude Code