chore: release package#1716
Merged
Merged
Conversation
6684422 to
5726a0f
Compare
09bd4af to
dc98800
Compare
dc98800 to
fe81757
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.
This PR was opened by the Changesets release GitHub action. When you're ready to do a release, you can merge this and the packages will be published to npm automatically. If you're not ready to do a release yet, that's fine, whenever you add more changesets to main, this PR will be updated.
Releases
@adcp/sdk@7.2.0
Minor Changes
77c1022: feat(errors):
AuthenticationRequiredError.challengesurfacesWWW-Authenticatescheme for non-Bearer 401s (closes AuthenticationRequiredError should surface non-Bearer WWW-Authenticate challenges with scheme-specific remediation #1722)When an MCP or A2A agent responds with a 401, the SDK now probes for the
WWW-Authenticateheader and parses the challenge before throwingAuthenticationRequiredError. The parsed challenge rides on the error soevery consumer (the CLI, LLM agents wrapping the SDK, dashboards,
programmatic callers) can branch on the auth scheme without re-fetching
or grep-matching error messages.
The error gains:
challenge?: AuthChallengeInfo—{ scheme, realm?, scope?, error?, error_description? }, lowercased scheme per RFC 9110 §11.6.1.suggestedScheme: string | undefinedgetter — the lowercased scheme,intended for
error.suggestedScheme === 'basic'checks.Scheme-aware default message: a Basic challenge produces a message
naming both the SDK shape (
createTestClient({ auth: { type: 'basic', username, password } })) and the CLI shape (--auth user:pass --auth-scheme basic); a Digest / Negotiate / NTLM challenge producesa generic "not natively supported" message with the scheme name; the
legacy "provide auth_token" fallback is preserved for the no-challenge
path so existing consumers don't regress.
Why this matters: before PR feat(cli): --auth-scheme bearer|basic for HTTP Basic auth (RFC 7617) #1719,
AuthenticationRequiredErroralwayssaid "No OAuth metadata available — provide auth_token in agent config."
That was right when Bearer/OAuth were the only options. After PR feat(cli): --auth-scheme bearer|basic for HTTP Basic auth (RFC 7617) #1719
added CLI support for HTTP Basic, the same error surfaced for gateway-
fronted agents (Apigee, Kong, AWS API GW, nginx
auth_basic) and themessage led adopters down a doomed OAuth path. The CLI's 401 handler
gained a Basic hint in feat(cli): --auth-scheme bearer|basic for HTTP Basic auth (RFC 7617) #1719, but only because it parses the error
envelope itself. Every other consumer — including LLM agents using the
SDK directly — still saw the misleading message.
Constructor signature is back-compat: the new
challengeparameteris positional argument 4 (after
agentUrl,oauthMetadata,message)and defaults to
undefined. Existing call sites (3 in the SDK; anyadopter code passing 1–3 args) work unchanged. The scheme-aware default
message only fires when a challenge is passed AND its scheme is
non-Bearer — Bearer challenges fall through to the OAuth-metadata branch
exactly as before.
New helper:
probeAuthChallenge(agentUrl, options)exported from@adcp/sdk/auth/oauth— fires a single unauthenticatedtools/listandreturns the parsed challenge (or
null). Reuses the same SSRF gate andtimeout policy as
discoverAuthorizationRequirementsso adopterdeployments don't need a second SSRF policy.
Wired into three throw sites:
SingleAgentClient.discoverMCPEndpoint(the MCP discovery walk)SingleAgentClient.discoverA2AEndpoint(the A2A agent-card path)ProtocolClient.callA2ATool(the A2A in-flight 401 path)All three now probe for the challenge when
discoverAuthorizationRequirementsreturns null (non-Bearer or PRM-missing 401) and pass it through to the
error envelope.
Tests added:
test/lib/authentication-required-error.test.js: 6 new tests coveringthe Basic message shape, the non-Bearer-non-Basic generic message, the
Bearer + OAuth-metadata fallthrough, the no-challenge legacy fallback,
the custom-message override, and the
suggestedSchemegetter.test/lib/probe-auth-challenge.test.js: 5 tests against local 401servers covering Basic, Bearer, 200 OK, 401 without
WWW-Authenticate, and unreachable host.44/44 cross-suite regression passing (
cli-auth-scheme.test.js,cli-oauth-flag.test.js,authentication-required-error.test.js).Source: protocol-expert review follow-up from PR feat(cli): --auth-scheme bearer|basic for HTTP Basic auth (RFC 7617) #1719.
a2f9238: feat(cli):
--auth-scheme bearer|basicfor HTTP Basic auth (RFC 7617)The CLI's
--auth TOKENflag was bearer-only — it always emittedAuthorization: Bearer …and silently dropped any-H Authorization=…override via the reserved-header filter. Any agent fronted by an API
gateway that requires
Authorization: Basic <base64(user:pass)>(Apigee,Kong, AWS API Gateway with a BasicAuthentication policy, or just a
self-hosted nginx with
auth_basic) was therefore unreachable fromadcp <alias> <tool>even though the underlying library already supportedbasic via
createTestClient({ auth: { type: 'basic', … } }).--auth-scheme bearer|basicopts into the alternate scheme, applied acrossevery CLI surface that takes
--auth:adcp <alias> <tool>(the directmcp/a2ainvocation path)adcp test <agent>adcp storyboard run <agent> …(single-instance, multi-instance, andmulti-agent routing)
adcp storyboard step <agent> …Usage:
Env-var form:
ADCP_AUTH_SCHEME=basic(overridden by the flag).Behavior:
--auth <user:pass>is RFC 7617-validated at register time and againat use time — colon-less, empty-username, CR/LF, and non-printable
ASCII inputs are rejected with a clear stderr message before any
request leaves the CLI. A typo doesn't get persisted only to surface
as a confusing decode error on every later call.
When basic auth is in effect, the CLI injects the encoded
Authorization: Basic …header viaagentConfig.headers(which theprotocol layer at
src/lib/protocols/mcp.tsandprotocols/a2a.tsspreads BEFORE the SDK's bearer header). The bearer path is suppressed
so there's no scheme collision on the wire.
--list-agentssurfaces the scheme (Auth: token configured (basic (user:pass))) so operators can tell at a glance whether an aliasspeaks bearer or basic.
Bearer remains the default; existing aliases and CI commands behave
identically. Saved bearer aliases do NOT gain an
auth_scheme: "bearer"field on rewrite (only
"basic"is persisted) to keep config diffsclean.
Mutually exclusive with
--oauthand the client-credentials flags — theCLI rejects combinations at parse time rather than silently picking one.
c3e5b96: fix(discovery): per-agent property resolution from adagents.json (bug: TypeScript SDK doesn't implement per-agent property resolution per spec #1721)
The TS SDK now honors the per-entry
authorization_type+ selector onauthorized_agents[]inadagents.json, matching the schema(
schemas/cache/3.0.11/adagents.json) and the Python SDK's_resolve_agent_properties. Pre-fix, the TS SDK treatedauthorized_agents[]as a presence list and attributed everytop-level property to every listed agent — giving different answers
than the Python SDK for the same input file.
Bug
For the file shape called out in bug: TypeScript SDK doesn't implement per-agent property resolution per spec #1721:
{ "authorized_agents": [ {"url": "https://wonderstruck.sales-agent.scope3.com", "authorized_for": "..."}, {"url": "https://interchange.io", "authorized_for": "..."} ], "properties": [{"property_id": "main_site", "property_type": "website", ...}] }main_site(1 property each).authorization_type).oneOfvariant matches).The pre-fix TS behavior produced silently-divergent authorization
answers across SDKs. The fix makes the TS SDK fail closed when the
file omits
authorization_typeor its selector — same as Python.New public API
resolveAgentProperties(adAgents, agentUrl)dispatches onauthorization_type:property_ids→ filter top-levelproperties[]byproperty_idproperty_tags→ filter top-levelproperties[]by tag intersectioninline_properties→ return the agent entry's ownproperties[]publisher_properties→ return cross-publisher selectors for thecaller to resolve against other publishers' files
signal_ids/signal_tags→ no property output (signals agents)listAgentPropertyMap(adAgents)returns{ byAgent, unresolved, cross_publisher }so consumers can iteratethe full per-agent map.
canonicalizeAgentUrl(url)is exported for callers doing their ownper-agent matching (e.g., TMP
seller_agent_urlvalidation).URL comparison follows the AdCP URL canonicalization rules — case,
default port, percent-encoded unreserved chars, and fragment all
normalized; userinfo, non-http(s) schemes rejected.
Behavior changes
PropertyCrawler.crawlAgents()now attributes properties peragent using
resolveAgentProperties. Agents that don't appear inthe publisher's
authorized_agents[](or whose entry is missingauthorization_type/selector) get zero properties instead ofthe file's entire
properties[].PropertyCrawler.fetchAdAgentsJson()andfetchPublisherProperties()now also surface the raw parsed
AdAgentsJson(alongside thenormalized
properties) so external callers can run the resolverthemselves.
authorized_agentsbutno
propertiesarray (the crawler infers a default property), theinferred property is still attributed to every claiming agent. The
fallback only fires for non-conformant files.
Types
AuthorizedAgentwidened to exposeauthorization_type(required byschema; typed optional here for backward compat with pre-schema-3
fixtures),
property_ids,property_tags,properties,publisher_properties,signal_ids,signal_tags.AdAgentsPublisherPropertySelector— theadagents.jsonvariant of
PublisherPropertySelector(discriminated byselection_type: 'all' | 'by_id' | 'by_tag'). Exposed under adistinct name to avoid clobbering the existing registry-flat
PublisherPropertySelector.85bdd89: feat(discovery):
validateAdAgentswith ads.txtMANAGERDOMAINone-hop fallback (support managerdomain fallback #1717)Implements RFC 4175 / Rfc: ads.txt
managerdomainfallback foradagents.jsondiscovery adcp#4175 — new publicvalidateAdAgents(publisherDomain, options?)helper that resolves apublisher's
adagents.jsonvia:https://{publisher}/.well-known/adagents.json(direct).authoritative_location(and no inlineauthorized_agents), followone redirect — reports
discovery_method: 'authoritative_location'.https://{publisher}/ads.txtfor aMANAGERDOMAIN=directive and attempthttps://{managerdomain}/.well-known/adagents.json— reportsdiscovery_method: 'ads_txt_managerdomain'and populatesmanager_domain.Per the #4173 resolution of the RFC's open questions:
MANAGERDOMAIN=example.comcounts; thecomment form
# managerdomain=example.comis rejected.MANAGERDOMAINlines: last-wins (rather than the RFC'sfail-closed default — IAB-aligned).
Other safety rules carry through from the RFC:
refusals stay terminal on the direct path.
publisher → publishercycles are rejected.#noagentstrailing comment on aMANAGERDOMAINline excludes thatentry from fallback discovery (publisher-side opt-out).
validation failure, never a silent pass.
New public API
Exports added from
@adcp/sdk:validateAdAgents(domain, options?)— main entrypoint.parseManagerDomain(adsTxt)— directive-only parser, exported fordirect unit testing and adopters who want to consume MANAGERDOMAIN
outside the validator.
DiscoveryMethod,AdAgentsValidationResult,ValidateAdAgentsOptions.Routes through
ssrfSafeFetchfor DNS-pin / SSRF-policy defense (sameposture as
PropertyCrawlerandNetworkConsistencyCheckerpost-security(discovery): wire network-consistency-checker + property-crawler through ssrfSafeFetch (#1627 cross-cutting follow-up) #1633).Patch Changes
1aa097a: fix(cli): basic-auth UX polish (closes CLI --auth-scheme basic: connection cache key, list-agents pretty-print, env-var warn, helper extraction #1723 — CLI bundle)
Final slice of CLI --auth-scheme basic: connection cache key, list-agents pretty-print, env-var warn, helper extraction #1723 follow-up to PR feat(cli): --auth-scheme bearer|basic for HTTP Basic auth (RFC 7617) #1719. Picks up the CLI-side review
items that didn't fit the SDK-layer cache-key fix (landed separately).
Refactor-safety:
injectBasicAuthHeaderhelper. The basic-auth pathrelies on a subtle invariant: the encoded
Authorizationheader isinjected into
mergedHeadersAFTERmergeHeaders()runs, so thereserved-key filter (case-insensitive
authorizationstrip) doesn'tdrop it. The invariant lived in prose only. Extract a tiny helper with
the warning baked into the docstring, and add an end-to-end invariant
test (
test/lib/cli-auth-scheme.test.js) that hand-edits a savedconfig to smuggle an
authorizationheader, then asserts the mergefilter strips it on read. A future refactor that moved injection
inside or before
mergeHeaderswould fail this test before reachingthe wire test that catches the symptom.
Env-var asymmetry warning.
ADCP_AUTH_SCHEME=basicis silentlyno-op when no token resolves to the request — adopters wouldn't see
why their Basic gateway keeps 401ing. Surface a stderr warning at the
direct-invocation site when
ADCP_AUTH_SCHEMEis set in the env butthe resolved scheme didn't end up applied. The inverse (token-without-
scheme → silent bearer) is the safe direction and stays silent.
--auth-scheme=basicsingle-token form. Pre-existinginconsistency: the long-form path treated
--auth-scheme=basicas anunknown arg and silently fell through to env-var lookup. Equals-form
is now first-class at both the top-level
parseAuthSchemeFlagand the--save-authflag parser. Source label in error messages distinguishesflag vs env-var when validation fails so operators know which to fix.
--list-agentspretty-print. Old format:Auth: token configured (basic (user:pass))— nested parens, inner placeholder non-informative.New format:
Auth: HTTP Basic (user=<username>)for basic,Auth: bearer token configuredfor bearer. The username is already on diskin cleartext; surfacing it makes multi-tenant aliases immediately
distinguishable. The password stays hidden. Regression test asserts
the password value NEVER appears in
--list-agentsoutput.--save-authhonorsADCP_AUTH_SCHEME. CI scripts that setADCP_AUTH_SCHEMEglobally previously had to repeat--auth-scheme basicon everyadcp --save-authinvocation. The env var now feedsthe save path as well. The CLI flag wins on conflict (consistent with
the runtime path).
Root
--helpdensity. Collapsed the 4-line--auth-schemeblockin
printUsageto 3 lines and pointed atadcp --save-auth --helpfor full detail. Keeps the niche case from competing with
--oauthfor visual weight in the main usage screen.
Decode-source message clarity.
buildResolvedAuthOption'ssecond-line decode now uses the source label
resolved basic credential (saved alias or --auth)instead of the genericauth credential, so amalformed hand-edited config surfaces with the right hint.
Tests added (
test/lib/cli-auth-scheme.test.js):--list-agentspretty-print (HTTP Basic with user, bearer plainlabel, no nested parens, no password leak)
--auth-scheme=basicsingle-token form parsingADCP_AUTH_SCHEMEenv var feeds the save pathEnv-var ineffective warning fires (spawns a local 401 server)
Helper invariant:
mergeHeadersstrips smuggledauthorization(refactor-safety guard)
24/24 cross-suite passing (cli-auth-scheme + cli-header-flag).
Source: code-reviewer (helper extraction), DX-reviewer (list-agents,
help density, env warn), security-reviewer L3 (equals-form parsing),
from PR feat(cli): --auth-scheme bearer|basic for HTTP Basic auth (RFC 7617) #1719 follow-ups.
c566612: test(comply): regression guard for storyboard-runner ↔ comply aggregation parity (Evaluator divergence: comply suite and CLI runner produce materially different grades on the same agent #1708)
Locks the post-7.1.0 attribution invariants so future refactors of
comply()'sextractFailuresaggregation can't silently reintroduce theBidMachine misattribution shape (adcp#4419).
What's locked:
response_schemafailure(prepended by the runner per Harness error attribution: Zod validation rejects surface as failures on unrelated downstream assertions #1709 / PR fix(runner): attribute Zod schema rejects to response_schema (#1709) #1712) and an
assertionentrysurfaces
validation.check === 'response_schema'inComplianceResult.failures— never'assertion'. This is theattribution that was silently broken pre-7.1.0 (Zod rejects fell
through to the next invariant, canonically
context.no_secret_echo).passed: trueentries the runner emits whenshort-circuiting invariants downstream of a schema failure per fix(runner): attribute Zod schema rejects to response_schema (#1709) #1712)
are correctly filtered out — only failed validations surface in
failures. A future change that includedpassed: trueentries wouldcrowd out the real failure.
authorizationfield onpassing
no_secret_echoper no_secret_echo invariant flags spec-legitimate field names on structured-value fields #1713 / PR fix(invariant): no_secret_echo only fails on string-valued suspect-named fields (#1713) #1714) produces zero failuresthrough the aggregation layer.
(storyboard_id, step_id, validation.check)tuples — A failed, B clean,C failed produces exactly two
failuresentries with stable attribution.API change (minor):
extractFailures(previously file-internal) is nowexport-ed fromsrc/lib/testing/compliance/comply.tsso the regressiontest can call it directly with synthetic
StoryboardResultfixtures.Functionally identical; just visibility.
Scope correction relative to the original Evaluator divergence: comply suite and CLI runner produce materially different grades on the same agent #1708 framing: the
"cross-evaluator divergence" symptom was version-driven (different
@adcp/sdkversions hitting no_secret_echo invariant flags spec-legitimate field names on structured-value fields #1713 and Harness error attribution: Zod validation rejects surface as failures on unrelated downstream assertions #1709 differently), not a trueparity gap between
comply()andrunStoryboard(). Both root causesshipped in 7.1.0; this test is the durable guard for the
aggregation-layer invariants those fixes depend on.
7714410: docs(llms): add Transport auth section clarifying operator-private posture (closes docs: clarify that AdCP transport auth is operator-private (not advertised in get_adcp_capabilities) #1724)
Add a "Transport auth" section to
docs/llms.txt(immediately after QuickStart) clarifying that AdCP is auth-scheme-agnostic at the transport
layer. The protocol carries JSON-RPC over HTTP; how the outer envelope
is gated is an operator-private deployment choice — bearer, OAuth, mTLS,
AWS SigV4 at the edge, IP allow-lists, or RFC 7617 HTTP Basic for
gateway-fronted agents (Apigee, Kong, AWS API Gateway, nginx
auth_basic).
get_adcp_capabilitiesdoes NOT advertise accepted authschemes; coupling the protocol to infrastructure permutations would
invite "auth_methods" PRs every time someone adds a new gateway shape.
Calls out the discovery vector explicitly:
WWW-Authenticate(RFC 9110§11.6.1) and PRM (RFC 9728), already consumed by
src/lib/auth/oauth/diagnose.ts. Basic-fronted agents emitWWW-Authenticate: Basic realm="…"on a 401; consumers should branch onthe challenge scheme rather than retrying Bearer indefinitely.
Closes the documentation gap surfaced by the protocol-expert review of
PR feat(cli): --auth-scheme bearer|basic for HTTP Basic auth (RFC 7617) #1719 (
--auth-scheme bearer|basicfor the CLI). Pre-empts the"should we add
auth_methodsto capabilities?" PR that someone willeventually open.
Doc-only change. No code or behavior impact.
11ebe09: fix(protocols): MCP/A2A connection cache key disambiguates non-Bearer credentials (closes CLI --auth-scheme basic: connection cache key, list-agents pretty-print, env-var warn, helper extraction #1723 / Security L1)
Both protocol caches keyed on
agentUrl + hash(authToken). When the callerused a non-Bearer scheme — RFC 7617 Basic from the CLI's
--auth-scheme basicshape landed in feat(cli): --auth-scheme bearer|basic for HTTP Basic auth (RFC 7617) #1719, or any future caller-injectedAuthorizationheader —authTokenwas undefined and the cache keycollapsed to just
agentUrl + signingCacheKey. Two callers withdifferent
user:passcredentials targeting the same agent URL wouldsilently share a single cached MCP/A2A transport, and the transport
closed over whichever credential it saw first.
For the single-process CLI this was a non-issue (process boundary
isolates credentials). But the SDK is also consumed by long-lived
multi-tenant hosts (
createTestClient-fronted services serving Nprincipals), and there a credential cross-leak across the connection
boundary is a real bug —
tenant-A's next call could ridetenant-B's transport.Fix: when
authTokenis unset, bothconnectionCacheKey(MCP) anda2aCacheKey(A2A) now derive the cache fingerprint from theAuthorizationheader on the outgoing request (case-insensitivelookup, since header keys vary by call site). The hash prefix
(
createHash('sha256').update(fingerprint).digest('hex').slice(0, 16))stays byte-equivalent with the bearer path so existing cache entries
work unchanged, and same-credential callers still share a single
cached transport — the key only changes when the credential differs.
A new helper
extractAuthHeader(mirrored on both protocols, keptprivate to each module so they don't share a runtime import) does the
case-insensitive lookup.
A2A also gets the same fix at the eviction site (
is401Errorcachedelete) so a 401 on a non-Bearer call evicts the right entry instead
of the bearer-keyed entry.
Tests:
test/lib/mcp-connection-cache-basic-auth.test.jsspins up alocal minimal MCP server, calls it twice with different
Authorization: Basic …headers viacallMCPTool, and asserts BOTHcredentials reach the wire. The pre-fix shape (cache key matches on
just
agentUrl) fails the first assertion — only tenant-A'scredential ever reaches the wire because tenant-B's call gets a cache
hit on tenant-A's transport. Second test asserts no cross-test
contamination (same-credential calls don't leak prior-test
credentials into the same connection).
47/47 cross-suite regression passing (cli-auth-scheme +
cli-header-flag + cli-oauth-flag + authentication-required-error +
probe-auth-challenge + mcp-connection-cache-basic-auth).
Source: security-reviewer L1 from PR feat(cli): --auth-scheme bearer|basic for HTTP Basic auth (RFC 7617) #1719 follow-up.
e4e38f8: docs(signing): note that Authorization is not in SIGNING_RESERVED_HEADERS (closes signing: doc note that Authorization is not in SIGNING_RESERVED_HEADERS #1725)
Add a block comment to
src/lib/signing/fetch.ts(and the mirrored line infetch-async.ts) explaining thatauthorizationis intentionally NOT inSIGNING_RESERVED_HEADERS. AdCP's RFC 9421 profile doesn't cover theAuthorizationheader (seeMANDATORY_COMPONENTSinsrc/lib/signing/types.ts), so caller-supplied Bearer / RFC 7617 Basicheaders pass through
createSigningFetchunmodified.Surfaces two latent gotchas in the comment so future contributors don't
have to rediscover them:
authorizationtocovered_components,the canonicalizer must read the FINAL outgoing value (post-
mergeHeadersinjection in
bin/adcp.jsfor the CLI Basic-auth path landed in feat(cli): --auth-scheme bearer|basic for HTTP Basic auth (RFC 7617) #1719).createSigningFetchalready readsinit.headersat fetch time so thearchitecture survives this — the comment makes that explicit.
credentials don't rotate, so signing over them creates a re-attack
surface. Any future
covers_authorizationpolicy knob needs thatsecurity consideration in scope.
Comment-only change. No code, test, or behavior impact. Source: protocol-
expert review of feat(cli): --auth-scheme bearer|basic for HTTP Basic auth (RFC 7617) #1719.
a96dd42: fix(discovery): validateAdAgents polish from feat(discovery): validateAdAgents with ads.txt MANAGERDOMAIN fallback (#1717) #1718 review (closes validateAdAgents follow-ups from #1718 expert review #1720)
Follow-ups from the multi-reviewer pass on PR feat(discovery): validateAdAgents with ads.txt MANAGERDOMAIN fallback (#1717) #1718 (
validateAdAgents):Real fix surfaced by new test:
nulland was crashingthe
data.authoritative_locationcheck. Added acoerceAdAgentsObjectguard at all three entry points (direct,
authoritative_locationfollow,manager-domain hop) that rejects non-object JSON values and returns a
clean
parse_error-shaped failure instead of throwing.Code-quality cleanups:
describeOutcomeacceptsFetchFailureonly (was a wider union with adead
'ok'arm).parseManagerDomainusescharCodeAt(0) === 0xfeff+sliceinstead of a literal-U+FEFF regex.New JSDoc safety warnings on
AdAgentsValidationResult:manager_domain— chained callers re-invokingvalidateAdAgentsareresponsible for their own loop guard. The one-hop guarantee is
per-call, not per-chain.
adagents— counterparty-controlled JSON. Treat as untrusted inputbefore splicing into LLM prompts, log indices, or any text-as-instruction
context.
New regression tests:
invalid JSONfailure on direct pathauthoritative_location— NOTrecursed (RFC's "one hop only" rule)