Conversation
…3524 stage 3) Badge labels now read 'Media Buy Agent 3.0 (Spec)' instead of 'Media Buy Agent (Spec)'. Legacy /badge/{role}.svg keeps serving the highest active version (auto-upgrades on new badges) so embedded badges in the wild aren't broken. New version-pinned routes: - GET /api/registry/agents/{url}/badge/{role}/{version}.svg - GET /api/registry/agents/{url}/badge/{role}/{version}/embed Path-segment form, not query string — cleaner shields.io-style cache keys. Version validates against ^[1-9][0-9]{0,3}\.[0-9]{1,3}$ at the route boundary so 400-on-invalid is distinguishable from 200-no-badge. renderBadgeSvg gains an options object with optional adcpVersion. A malformed value drops from the label rather than failing the image — unlike the JWT signer (which fails closed because a partial token is a downgrade vector), a missing badge image is worse for the buyer than a less-specific one. ETag covers role+version+modes. 8 new tests for the version segment: embed-when-set, omit-when-absent, drop-on-malformed including SQL-injection-shaped, reject-leading-zero major, reject-full-semver, double-digit minor preservation, and 'Not Verified' labels never carry a version. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Security review:
- HIGH: rate-limit all four badge routes. The legacy /badge/{role}.svg
+ /badge/{role}/embed pair was already missing agentReadRateLimiter
before this stage; the new version-pinned routes inherited the gap.
All four are anonymous and hit Postgres on every request, which made
them a DB enumerator across the registry. Fixed by wrapping each
route with agentReadRateLimiter (the same limiter already applied
to /compliance and /compliance/history).
- MED: filter the legacy ETag's adcp_version through the same shape
regex renderBadgeSvg uses. A poisoned DB row with control characters
(CR/LF, NUL) in the column would have crashed the response with
ERR_INVALID_CHAR when Node serialized the ETag header. Same DB-row
gracefulness the SVG path explicitly handles.
- LOW: new test pins the multi-version revoke transition invariant
(3.0 revoked while 3.1 active should leave 3.1 untouched, so the
legacy URL upgrades cleanly).
Code review:
- Tighten changeset wording: validation is shape-identical to the JWT
signer / DB CHECK but length-bounded only at the route boundary
(the SVG render-path filter is shape-only since its callers are
already gated upstream).
- Response-shape consistency: the .svg routes returned text/plain
("Invalid agent URL") but the embed routes returned JSON. Aligned
to JSON across the badge route family.
- escapeMdAltText forward defense in buildEmbedResponse so a future
caller passing user-controlled alt text can't break the markdown
link syntax with an unescaped `]`.
OpenAPI coverage: registered both new routes (.svg + embed) so the
coverage check stays green; CI caught the gap on the original push.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1ae81e7 to
480b8cf
Compare
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.
Summary
Stage 3 of #3524. Badges and embed code can now carry an explicit AdCP version.
The legacy URL `/badge/{role}.svg` still works — it serves the highest active version, so embedded badges in the wild auto-upgrade as agents earn newer versions.
New routes
Path-segment form (not query string) for clean shields.io-style cache keys. Version validates against `^[1-9][0-9]{0,3}.[0-9]{1,3}$` at the route boundary so 400-on-invalid is distinguishable from 200-no-badge.
What does NOT change
Defense in depth
`renderBadgeSvg` validates `adcpVersion` against the same regex as the JWT signer. A malformed value drops from the rendered label rather than failing the image. Asymmetry vs. the JWT signer (which fails closed) is deliberate: a missing badge image is worse for the buyer than a less-specific one. ETag covers `role + version + modes` so cache invalidates on transitions.
Test plan
Stage tracker
🤖 Generated with Claude Code