feat(signals,media-buy): cache-scope isolation for wholesale-feed conditional fetch#5748
Conversation
…ditional fetch Closes adcontextprotocol#5739. The wholesale-feed token is documented as scope-keyed — a caller caches (cache_scope, wholesale_feed_version) pairs — but nothing exercised that a token minted under one cache_scope cannot short-circuit (unchanged: true) a request the agent resolves to another. The reference training agent advertises wholesale_feed_versioning.cache_scope_account: true yet keys conditional fetch on a scope-independent token, so it would silently answer unchanged across scopes. - schemas: add a cross-scope MUST-NOT to get-signals-response / get-products-response `unchanged`, scoped to the conditional-fetch comparator. - storyboards: new universal wholesale-feed-{signals,products}-scope-isolation, gated on cache_scope_account so public-only agents grade not_applicable. - reference agent: scope-key the wholesale feed/pricing tokens. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
There was a problem hiding this comment.
The automated review encountered an issue (possibly reached max turns, timed out, or failed to post the final gh pr review). A human reviewer should take this PR.
This is an automated message from the Argus AI review workflow.
…al parity tables
The universal-storyboard doc-parity lint (scripts/lint-universal-storyboard-doc-parity.cjs,
run inside build:compliance) requires every graded universal storyboard to have a row in
both the conformance index and the compliance catalog. Back-fill rows for the two new
wholesale-feed-{signals,products}-scope-isolation storyboards.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
There was a problem hiding this comment.
The automated review encountered an issue (possibly reached max turns, timed out, or failed to post the final gh pr review). A human reviewer should take this PR.
This is an automated message from the Argus AI review workflow.
The reference agent now folds cache_scope into the wholesale feed/pricing token (training-*-feed-v1 -> training-*-feed-v1.public[.<digest>]). Update the literal expectations in the public-scope wholesale tests accordingly. The standalone-pricing-token rejection cases keep their arbitrary literals (the reject path is value-independent). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
There was a problem hiding this comment.
The automated review encountered an issue (possibly reached max turns, timed out, or failed to post the final gh pr review). A human reviewer should take this PR.
This is an automated message from the Argus AI review workflow.
Closes #5739.
Problem
The wholesale-feed token is documented as scope-keyed —
get-signals-response.jsonand
get-products-response.jsonboth say a caller caches(cache_scope, wholesale_feed_version)pairs, not a global agent version. Butnothing in the conformance suite exercises that property: both
wholesale-feed-signals.yamlandwholesale-feed-products.yamlonly ever drivecache_scope: public, so an agent that keys conditional fetch on ascope-independent token passes today.
The reference training agent demonstrates the gap directly. It advertises
wholesale_feed_versioning.cache_scope_account: trueand resolves the reservedaccount-overlay.exampleidentity tocache_scope: account(
server/src/training-agent/task-handlers.ts), yet mints a scope-independenttoken and compares only the token value — so a
public-minted token echoed onan
account-scope read wrongly short-circuits tounchanged: true, and thecaller silently misses the account overlay.
Changes
Schemas — a cross-scope
MUST NOTappended to theunchangeddescriptionin
get-signals-response.jsonandget-products-response.json, scoped to theconditional-fetch comparator: it MUST key on
(cache_scope, wholesale_feed_version);a token minted for
cache_scope: 'public'cannot match the agent's current tokenfor
cache_scope: 'account'(or vice-versa), so such a request MUST return thefull feed for the resolved scope. This operationalizes the "scope-keyed" language
already present in
wholesale_feed_version.Storyboards — new universal
wholesale-feed-signals-scope-isolationandwholesale-feed-products-scope-isolation, gated onwholesale_feed_versioning.cache_scope_account: true. Each bootstraps the publicfeed, captures the token, echoes it on a read the agent resolves to
cache_scope: account, and assertsunchangedis absent,cache_scope: account,and rows are returned. Agents without per-account overlays grade
not_applicable,not failed.
Reference agent — scope-key the wholesale feed/pricing tokens (fold
cache_scopeinto the token value). The same-scopeunchangedpath still matches,so existing wholesale storyboards are unaffected; the cross-scope path now differs.
Validation (local)
npm run build:compliance✅ — 40 universal / 6 protocols / 21 specialismsnpm run test:schemas✅ 19/0 ·test:storyboard-branch-sets✅ 23/0 ·test:storyboard-scoping✅ 0 fail (parity with the training-agent handler map)On
introduced_in: "3.2"(deliberate, not a backport)This PR makes an existing 3.1 MUST testable — it does not add a new requirement.
The scope-keying contract is already in the 3.1 spec text (
get-{signals,products}-response.json:callers cache
(cache_scope, wholesale_feed_version)pairs). On accuracy alone"3.1"would be defensible.
It is nonetheless set to
"3.2", matching the milestone, because the change is notpatch-eligible: a previously-passing global-token build would newly fail the isolation
storyboard. Tightening an existing-but-untested MUST into a checkable conformance gate is
a minor-version event, so it lands as a 3.2.0 requirement rather than a 3.1 backport.
Single-scope agents (no
cache_scope_account) gradenot_applicable, so only agents thatactually publish account overlays are ever held to it.