feat: resolve 3.1 blocked SDK issues#2040
Conversation
There was a problem hiding this comment.
LGTM. Five distinct unblocks shipped under one changeset is more surface than I'd usually fold together, but each is contained, the expert passes (DX, protocol, code, security) converge on sound-with-caveats, and the test additions actually exercise the new behavior rather than rubber-stamping it.
Things I checked
- Changeset is right shape.
.changeset/resolve-3-1-blocked-issues.md—minoragainst 8.1.0-beta.13 fits the new public surface (productCatalogfield on capabilities,RequiredToolFamily, raw/digest discriminatedRecordedCall,sponsored-intelligenceas a first-class specialism,required_any_of_toolsstoryboard gate). The companion edit to.changeset/1853-1818-capability-projection.mdkeeps the 1818 narrative coherent — explicit acknowledgement that thesupported_targetsportion now ships here rather than being deferred. supported_targetspreservation matches spec verbatim.src/lib/types/v3-1-beta/tools.generated.ts:19950says verbatim "When omitted, only target-less event goals are guaranteed; sellers MAY accept specific target kinds but buyers should not rely on it." The changeset language mirrors it;from-platform.ts:1010passes through without injecting a default. No translator behavior added.- SOM rollup honors the spec contract. Spec at
tools.generated.ts:19909requires sellers keepsupported_optimization_metricssynced with the catalog union.rollupOptimizationMetricsFromProductsmechanizes it. Explicit declaration wins atfrom-platform.ts:990— right precedence. - Runner-side digest computation is authoritative, not controller-trusted.
runner.ts:5203-5224resolves identifier paths against runner-known plaintext (requestPayload/sampleRequest), computes SHA-256 in-process, sends digest to controller, andvalidations.ts:3010-3017pairs runner's known digest against controller'sfound. Controller cannot smuggle a "yes" past the runner. attestation_mode_required: 'raw'downgrade-to-not_applicableatvalidations.ts:2740-2750correctly refuses to silently pass a raw-introspection assertion against a digest-only attestation. Fail-closed.- Sandbox-authority gate not loosened.
src/lib/server/test-controller.tsdeltas are typed-params widening and dead-comment cleanup;comply-controller.ts:557-575routing throughsandboxGateis untouched. Phase-2 of #1435 still load-bearing. - Identifier-proof field shape matches the cached 3.1 spec (
identifier_value_sha256 + found), not thedigest_sha256 + pathshape my notes had wrong. Confirmed againsttools.generated.ts:23127-23135andupstream-recorder/types.ts:59-67. - SI specialism wiring is symmetric.
RequiredPlatformsFor(platform.ts:362),SPECIALISM_REQUIREMENTS(validate-platform.ts:56), camelCase platform methods (initiateSession/sendMessage/terminateSession→ wiresi_*) all line up.examples/hello_si_adapter_brand.ts:543now claims the specialism instead of carrying the prior workaround comment — right direction. - Raw-mode redaction covers the canonical set.
recorder.ts:8 → redact-secrets.ts:27:authorization|cookie|set-cookie|api-key|bearer|token|access_token|refresh_token|session_id|session_token|secret|credentials|password|offering_token. Form-urlencoded bodies hitredactFormUrlEncoded; binary bodies are marker-substituted (no raw bytes in the record buffer).
Follow-ups (non-blocking — file as issues)
- Pin the canonical-JSON form for
payload_digest_sha256upstream.recorder.ts:466uses lexicographic sorted-key JSON before SHA-256; the cached 3.1 spec is silent on canonicalization. Today this is benign because the runner compares identifier digests on string leaves, not the payload digest. The day any consumer cross-checkspayload_digest_sha256between two compliant controllers, they'll diverge. File againstadcontextprotocol/adcpreferencing the empty field-level prose at the digest field — RFC 8785 (JCS) is the obvious citation. - Tighten the digest-mode warning to call out low-entropy identifier vectors.
recorder.ts:563warns about the in-memory buffer; it should also call out that unsalted SHA-256 over short numeric IDs / ISO dates / country codes / boolean-as-string is an offline preimage oracle. The spec already mandates "synthetic vectors only" but the SDK doesn't enforce it. Two options: gateattestation_mode: 'digest'projection behind an explicitallowDigestProjection: truerecorder option, or move to HMAC with a per-session key (runner controls both sides, so a session-scoped salt is free and breaks dictionary precomputation). - Cap total leaf count in
jsonStringLeafDigests.recorder.ts:444caps depth at 256 but not total visited nodes; a 16K-leaf body inside the recorder amplifies per-request CPU. The runner-side collector caps at 64; the recorder-side digest set should similarly cap and emit a[truncated digest set]marker. payload_lengthandpayload_digest_sha256measure different byte streams. Length is computed at record-time oversafeStringify(redacted)(insertion-order); the digest is computed overcanonicalJsonStringify(sorted-key). For non-trivial objects the two disagree. No verifier cares today; flag for awareness if one ever does.- Non-string identifier vectors silently drop in digest mode.
runner.ts:5218skips non-string leaves. If a storyboard'sidentifier_pathsresolves to a number/boolean, the controller receives no digest, returns no proof, andvalidations.ts:3010-3017marks missing rather thannot_applicable. Either signalnot_applicablefor non-string leaves, or forceattestation_mode_required: 'raw'when any identifier vector is non-string. DigestRecordedCall.identifier_match_proofsdefined twice —src/lib/server/test-controller.ts:255-258andsrc/lib/upstream-recorder/types.ts:62-65. Drift surface. Re-export one from the other.- Generated
RawAttestation/DigestAttestationZod/TS shapes are stubs — only theattestation_modediscriminator survives codegen; branch-specific required fields are dropped,passthrough()masks the gap. Pre-existing, not introduced here, but the newpromoteConditionalParamPropertieshelper inscripts/generate-types.ts:575is scoped toparams.allOf[].then.properties.paramsand won't fix it — separate codegen follow-up. required_any_of_toolsskip emitspassed: true.runner.ts:1254-1305returnsoverall_passed: trueon arequirement_unmetskip.skipped_count: 1+skip_reason: 'requirement_unmet'are correct, but rollups that only consultoverall_passedwill read a tool-missing agent as compliant for storyboards testing absent tools. Exposeeffective_passedexcludingrequirement_unmetskips, or document the rollup contract.- Skip-cause aggregation test is synthetic.
test/lib/compliance-summary.test.js:508-549hand-builds aresultshape; the regex prefix extraction insummary.ts:272-278will silently rot if the runner's warning string format ever drifts. Add an integration-shaped test that pipes runner output into the summary.
Minor nits (non-blocking)
- Empty
payload: {}for undefined-body GETs.recorder.ts:138-141—applyRedactionToPayload(undefined)now returns{ value: {}, bytes: 2 }rather than{ value: undefined, bytes: 0 }, materializing an object the adopter never supplied. Load-bearing because the 3.1 schema requirespayloadin raw mode (test/lib/upstream-recorder-spec-shape.test.js:91-100makes this explicit). Defensible but worth a one-line comment at the branch explaining the spec constraint, so the next reader doesn't read it as drift from the witness-not-translator rule. - Explicit empty
supported_optimization_metrics: []coerced to undefined.from-platform.ts:991collapsessomCandidate?.length === 0into "omit field." Wire-equivalent today, but an adopter explicitly declaring "no metrics supported" gets normalized to "field absent." At minimum, log whenexplicitSom?.length === 0so the coercion is visible.
Safe to merge.
There was a problem hiding this comment.
LGTM. Clean implementation of the three 3.1 unblocks — query_upstream_traffic digest attestation, the required_any_of_tools gate, and the SI specialism promotion — all derive cleanly from observed data without violating the witness invariant.
Things I checked
- Witness invariant.
projectRecordedCall(src/lib/upstream-recorder/recorder.ts:399-456) andcollectUpstreamIdentifierDigests(src/lib/testing/storyboard/runner.ts:5203-5225) only project / hash data already in the recorded payload or the storyboard'ssample_request. No fabrication, no normalization, no placeholder substitution. The SHA-256 canonical-JSON projection is a deterministic transform of bytes already on hand. - Digest is over the redacted payload, not the original.
recorder.ts:215storesapplyRedactionToPayload(...).valueandprojectRecordedCallreadscall.payloadfrom that buffer. No de-redaction path. - 64-digest cap enforced at the producer.
normalizeIdentifierDigests(recorder.ts:428-437) caps before the per-call leaf walk and dedups. Bounds the membership-bits a runner can extract per call. - Discovery fallback respects the opt-out.
runner.ts:1817-1837only fires when!options.agentToolsANDclients[0].getAgentInfois callable AND_profile.toolsdidn't already populate. The_client-without-tools escape hatch still works. - SI specialism wired end-to-end.
'sponsored-intelligence'inAdCPSpecialism(tools.generated.ts:20293),RequiredPlatformsFor(platform.ts:362),validatePlatformSPECIALISM_REQUIREMENTS (validate-platform.ts:58), the compile-timedecisioning.type-checks.ts:152positive test, the runtimetest/server-decisioning-from-platform.test.js:3290negative test, the example claim, and the skill prose all agree. Pre-PR an adopter could claim the specialism without the platform field and silently no-op; the runtime tightening closes that hole without breaking adopters who couldn't dispatch anyway. - Encoding parity. All three
sha256Hexsites usecreateHash('sha256').update(value, 'utf8').digest('hex'). Runner-shipped digests match recorder-computed digests byte-for-byte. force_upstream_unavailablewired end-to-end. Enum atinline-enums.generated.ts:137,308, dispatcher attest-controller.ts:566(SCENARIO_MAP), advertisement atcomply-controller.ts:521.- Wire-shape pin.
test/lib/upstream-recorder-spec-shape.test.jsnow compiles against the cachedschemas/cache/3.1.0-beta.5/compliance/comply-test-controller-response.jsoninstead of the inline fixture — drift surfaces vianpm run sync-schemas. - Changeset present and correctly typed.
.changeset/resolve-3-1-blocked-issues.mdisminor; theRecordedCalldiscriminated-union widening is structurally additive (payload?: neveron the digest branch keepscall.payloadaccessible asunknownfor existing consumers), and the SI tightening only narrows a previously-permissive silent path.
Follow-ups (non-blocking — file as issues)
- 64-cap silent drop on identifier vectors. When
identifier_pathsresolves to more than 64 distinct string vectors,collectUpstreamIdentifierDigeststruncates butvalidateUpstreamTrafficstill iterates every resolved vector. Past the cap,valueDigestisundefined,sawApplicableCallistrue(digest-mode calls were seen), andanyMatchedCallEchoesValuereturns{ satisfied: false, not_applicable: false }— graded as a missing-identifier failure rather thannot_applicable. Either warn at truncation time or grade over-cap vectors asnot_applicable. Exactly the silent-failure mode the recorder otherwise avoids. min_count === 0semantics flip.validations.ts:2760changes the meaning from "no lower bound" (always-pass) to "must observe exactly zero." Spec-correct and tested, but the changeset prose doesn't call it out. Worth a migration line.payload_digest_sha256is producer-only / non-portable.canonicalJsonStringify(recorder.ts:466-484) sort-keys andJSON.stringifys — not RFC 8785 / JCS (no number canonicalization, no normative escape rules).identifier_match_proofsare portable because both ends hash raw UTF-8 strings, butpayload_digest_sha256is opaque per-controller. Either align to JCS or document explicitly in the SDK type's JSDoc that consumers must not cross-validate digests across controllers.RecordedCallstructural duplication.src/lib/testing/test-controller.ts:243-261andsrc/lib/upstream-recorder/types.ts:51-67both exportRawRecordedCall/DigestRecordedCall/RecordedCallbut the test-controller version has[key: string]: unknownindex signatures and the recorder version doesn't. Consolidate to a single source.promoteConditionalParamPropertiescodegen helper (scripts/generate-types.ts:575-587+scripts/generate-3-1-beta-types.ts:275-289) walks only top-levelallOf[].then.properties.params. Doesn't recurse intothen.allOf, nestedif/then/else, orthen.oneOf. Document the limitation or extend.- Mock-server SI reference.
src/lib/mock-server/sponsored-intelligence/seed-data.ts:108still points atcompliance/cache/latest/protocols/sponsored-intelligence/index.yaml. If 3.1.0-beta.5 also publishesspecialisms/sponsored-intelligence/, the seed should look at the specialism path. - Negative test gaps. No test for malformed
identifier_value_digestsrejection, orphan match-proofs (controller returns a proof for a digest the runner didn't ship), or a raw-attestation call that erroneously setspayload_digest_sha256. Discriminated-union TS catches the last at compile but no runtime guard.
Approving on the strength of the witness-invariant integrity through the digest projection plus the end-to-end SI specialism wiring.
There was a problem hiding this comment.
LGTM. Five cohesive 3.1 follow-ups behind one changeset, all keyed to the cached schema and pinned by tests against the cached comply-test-controller-response.json instead of the inline fixture. Wire-side payload_length and the attestation_mode discriminator are modeled correctly as a ?: never discriminated union — TS narrowing flows through RecordedCall consumers without losing the runtime invariant.
Things I checked
RawRecordedCall | DigestRecordedCall(src/lib/upstream-recorder/types.ts:51-67) —payload_digest_sha256?: neveron the raw arm andpayload?: neveron the digest arm are load-bearing for TS narrowing. Matches the cached 3.1 schema; theupstream-recorder-spec-shape.test.jsswitch to the cache-backed schema (vs the inline fixture) is the right forcing function.prefetchUpstreamTraffic(src/lib/testing/storyboard/runner.ts:5099-5215) — identifier digests are computed viasha256(plaintext)runner-side and only the hashes leave the process. Plaintext identifiers don't go on the wire. 64-vector cap is a sensible DoS guard. Raw mode is forced wheneverpayload_must_containis present, so digest-mode regression for payload-introspection assertions can't sneak in by default.- Gate ordering in
executeStoryboardPass(src/lib/testing/storyboard/runner.ts:1875-1900) —required_any_of_toolsevaluates beforerequires_capabilityand the synthetic skip step (buildRequiredAnyOfToolsMissingResult) matches the runner-output-contract v2 vocabulary used by every otherrequirement_unmetreason.skip-causeaggregation insummary.ts:269-281extracts the family prefix correctly. - SI specialism promotion —
'sponsored-intelligence'appears in the generatedAdCPSpecialismunion atsrc/lib/types/tools.generated.ts:20293andsrc/lib/types/v3-1-beta/tools.generated.ts:4967. Both the compile-timeRequiredPlatformsForbranch (platform.ts:362) and runtimeSPECIALISM_REQUIREMENTSentry (validate-platform.ts:56-58) line up. DeletedRequiredPlatformsForProtocols/SupportedProtocolare not referenced elsewhere. productCatalogrollup precedence (from-platform.ts:982-991) — explicitsupported_optimization_metricsalways wins over the derived catalog. Test pins both arms.- Changeset (
.changeset/resolve-3-1-blocked-issues.md) — minor bump matches the additive surface under 8.1.0-beta.RecordedCallbecoming a discriminated union is a TS-narrowing change for downstream consumers but the runtime shape stays raw-by-default, and we're still in pre-1.0 prerelease. - min_count=0 semantics — runner test pins that a raw-required digest downgrade still fails when
min_count: 0is violated (storyboard-runner-output-contract-v2.test.js:230-258). The newrawRequiredDigestDowngradeshort-circuit is gated onmatched.length > 0, so a zero-match window can't be masked.
Follow-ups (non-blocking — file as issues)
-
anyMatchedCallEchoesValuenon-string identifier vectors in digest mode.src/lib/testing/storyboard/validations.ts:3007-3018— when a vector resolves to a non-string,collectUpstreamIdentifierDigestsskips it (no digest sent), but the digest-mode branch still flipssawApplicableCall = trueand returns{ satisfied: false, not_applicable: false }. The vector ends up inmissingIdentifierValuesand fails the assertion instead of grading not_applicable. Bounded in practice — identifier paths in storyboards resolve to hashed strings — but worth aif (!valueDigest) continue;so the loop treats non-hashable vectors as inapplicable in digest mode. -
Document the conditional-params TS widening.
scripts/generate-types.ts:572-587andscripts/generate-3-1-beta-types.ts:275-288promoteallOf[].then.properties.params.propertiesinto the flat base. Fields that were previously discriminated underif: {properties: {request_type: {const: 'single'}}}now ship as flat-optional in the generated TS — Ajv still enforces the conditional at runtime, but TS callers lose the narrowing. Worth a one-liner in the changeset (or migration note) so adopters readingbuild_creative/sync_creativesparams don't think the field is unconditionally optional. -
fwLogger.infodev-mode block has no try/catch.src/lib/server/decisioning/runtime/from-platform.ts:1024-1034— an adopter-supplied logger that throws would crash server boot in dev. Wrap the dev-mode block.
Minor nits (non-blocking)
-
{}substitution for undefined payloads regardless of content_type.src/lib/upstream-recorder/recorder.ts:137-141— required by the 3.1 schema'spayloadrequired field, but for aGET/HEAD/DELETEwith no body the recordedpayloadis now a 2-byte JSON object that downstream JSON walkers (containsValueAnyDepth,jsonStringLeafDigests) will traverse. Mild witness/translator deviation; consider''for non-JSON content_types and reserve{}for JSON. -
normalizeAgentToolNamescollapses[]toundefined.src/lib/testing/storyboard/runner.ts:1318— an agent that legitimately advertises zero tools is indistinguishable from an un-discovered profile, sorequired_any_of_toolsandrequired_toolsgates silently no-op. Near-zero real-world impact (every agent shipsget_adcp_capabilities), but the asymmetry hides a broken-agent category. -
somCandidate.length > 0drops explicit[].src/lib/server/decisioning/runtime/from-platform.ts:986-988— adopter intent of "I support none of these metrics" projects the same as "not specified." Probably intentional; one-line comment would pin it. -
promoteConditionalParamPropertiesfirst-wins assumption.scripts/generate-types.ts:572-587andscripts/generate-3-1-beta-types.ts:275-288— comment that the function ignoreselsebranches and uses first-promote-wins so a future schema author with twoif/thenbranches defining the sameparams.Xdoesn't get silent data loss.
Safe to merge.
There was a problem hiding this comment.
Substantively right shape — 3.1 spec alignment lines up with adcp#3816 / adcp#3961 / adcp#4325. Discriminated RecordedCall, SI specialism enforcement, required_any_of_tools gating, productCatalog derivation all check out. Blocking on changeset prose, not code.
Things I checked
RecordedCalldiscriminated union mirrored insrc/lib/upstream-recorder/types.ts:51-67and the publicsrc/lib/testing/test-controller.ts:241-260. Spec-shape test compiles the cached schema directly.- SI specialism wired compile-time (
RequiredPlatformsFor<'sponsored-intelligence'>insrc/lib/server/decisioning/platform.ts:362) and runtime (src/lib/server/decisioning/runtime/validate-platform.ts:58).AdCPSpecialismenum confirmed in cache. productCatalogderivation gates onexplicitSom ?? derivedSom, drops empty arrays before emit (src/lib/server/decisioning/runtime/from-platform.ts:984-991).required_any_of_toolsloader rejects[]and single-tool families (src/lib/testing/storyboard/loader.ts:78-104); summary aggregates by missing-family name (src/lib/testing/compliance/summary.ts:269-279).- New tests cover digest-mode path, raw-required downgrade, family gate, capability projections.
Must fix — changeset ships two breakages as minor
RecordedCall.payloadwas required; now branch-dependent. Any adopter writing(c: RecordedCall) => c.payloadfailstscpost-bump.payload_lengthis also new+required, so literal constructors (test fixtures, custom controller stores) need updating too. Same shape mirrored in the publicRecordedCallatsrc/lib/testing/test-controller.ts:241.validatePlatformnow throwsPlatformConfigErrorwhen'sponsored-intelligence'is inspecialisms[]without thesponsoredIntelligencefield. Pre-PR the validator short-circuited unknown specialisms (validate-platform.ts:102—if (!required) continue).
Either bump the changeset to major or add explicit **BREAKING** bullets the way .changeset/1853-1818-capability-projection.md already does. The current five-line resolve-3-1-blocked-issues.md reads as additive; both surfaces above fail that promise. Either fix unblocks.
Follow-ups (non-blocking — file as issues)
- Identifier-digest collection is request-payload-only (
src/lib/testing/storyboard/runner.ts:5203-5225). Storyboards usingidentifier_pathsagainst response or$context.*values end up requesting digest mode with empty digests; the validation gradesnot_applicable— silent fail-open. Either reject those paths at load or extend the walker. payload_digest_sha256is computed over canonical-JSON re-serialization (src/lib/upstream-recorder/recorder.ts:466-484) — key-sorted, post-redaction. A third party holding the runner's emitted bytes can't reproduce this digest from the wire form. adcp#3816 doesn't explicitly license canonical form. Pin in the spec or document the choice in the recorder's JSDoc.promoteConditionalParamPropertiesis first-wins, no conflict detection (scripts/generate-types.ts:575,scripts/generate-3-1-beta-types.ts:275). No keys collide today; a future schema author adding a collidingthen.params.properties.xsilently loses the second branch's narrowing. Throw on overwrite.- Test-timeout bump 60s → 180s in
package.json. PR body acknowledges this is a workaround for two CLI files that pass individually. Tripling the global default hides slow tests rather than fixing them — file a perf issue and revert to 60s, or scope per-file. agentToolslate discovery on reused clients (runner.ts:~1820) now issuesgetAgentInfo()when_profile.toolsis absent. Previously a no-op. Adds a network round-trip to a path orchestrators chose to avoid re-discovery — worth an opt-out flag.projectRecordedCalldrops adopter-set extension fields on the digest path (recorder.ts:399-425). Public-APIRecordedCallcarries[key: string]: unknown(test-controller.ts:249,260); the digest projection enumerates fields explicitly. Tighten the index signature or spreadcall.
Minor nits (non-blocking)
productCatalogname is misleading (src/lib/server/decisioning/capabilities.ts:148-156). Sounds like the full product surface; it's actually "thing we rollup metrics from." Rename or document explicitly that onlymetric_optimizationis read.jsonStringLeafDigestsonly digests string leaves (recorder.ts:439-456). Numeric identifiers (retail SKUs as ints) won't surface inidentifier_match_proofs. Spec text says "string tokens" — defensible, but worth a JSDoc line.anyMatchedCallEchoesValuemixed raw/digest grading is correct (src/lib/testing/storyboard/validations.ts:3000-3034), butrunValidationsis exported and direct callers can exercise the mixed-batch path. Add a unit test pinning the grade.
Code is the right shape; the failure mode for adopters is concrete — tsc breaks on the first rebuild after the bump. Bump the changeset to major or add explicit BREAKING bullets and we're done.
|
Triage note — fold candidate from #2046 Issue #2046 ("Document and bound recorder digest attestation edge cases") tracks four follow-ups surfaced during review of this PR: JSDoc on low-entropy/unsalted SHA-256 digest risks, a leaf-count cap on the recorder-side All four items touch files this PR already modifies — the upstream-recorder, Suggest folding before merge to keep the recorder's attestation guarantees self-contained. If the diff is already at a size where expansion would materially delay review, a focused follow-up PR immediately after merge is fine — just leave #2046 open until then. Triaged by Claude Code. Session: https://claude.ai/code/cse_01QkV4aBjnwwFeZzykHPKhvY Generated by Claude Code |
There was a problem hiding this comment.
Approving. Closes out 3.1 with the right shape — RecordedCall as a raw/digest discriminated union mirrors adcp#3816, the SI specialism gate becomes mechanically enforced via SPECIALISM_REQUIREMENTS, and the supported_optimization_metrics projection implements the rollup the spec already requires sellers to maintain (witness, not translator — derivation from adopter-supplied productCatalog, no fabrication).
Things I checked
- Changeset vs. wire impact.
.changeset/resolve-3-1-blocked-issues.mdisminorwith explicit**BREAKING**:prose onRecordedCallandvalidatePlatform. In pre-release mode (pre.jsonmode:"pre", currently8.1.0-beta.13with 56 changesets accumulating), this is the established convention for this package — the exit-pre bump composes the stack. Not a block; the BREAKING prose carries the adopter signal. - Witness invariant.
from-platform.ts:982-999rollup unionsproductCatalog[].metric_optimization.supported_metrics, sorts, dedups, and omits the field entirely when the result is[](somCandidate != null && somCandidate.length > 0 ? somCandidate : undefined). No[]on the wire. Spec attools.generated.ts:20620describes this as the rollup the seller maintains — the framework is implementing it, not synthesizing it. required_any_of_toolsskip-cause aggregation. The synthetic skip frombuildRequiredAnyOfToolsMissingResultputs the detail inskip.detail. The bridge atstoryboard-tracks.ts:124copiesskip?.detailintowarnings[0], and summary aggregator atsummary.ts:273matches^(missing_required_tool_family: needs [^;(]+)against that. Path is intact end-to-end.- Discriminated union exhaustiveness.
upstream-recorder/types.ts:51-67usespayload_digest_sha256?: never/payload?: nevercorrectly.test-controller.ts:213-261does the same but adds[key: string]: unknownindex signatures on each arm — see follow-up #3 below. - Schema cache alignment.
attestation_mode,identifier_value_digests,payload_digest_sha256,identifier_match_proofsall present insrc/lib/types/v3-1-beta/tools.generated.ts:22493-22499andtools.generated.ts:22346-22352.'sponsored-intelligence'is in the AdCP 3.1AdCPSpecialismenum atenums.generated.ts:22. - Backward compat. Underscore protocol form
'sponsored_intelligence'is still derived from the registered SI tools viaTOOL_PROTOCOL_MAP— 3.0-era consumers keep seeingsupported_protocols: ['sponsored_intelligence']without needing to claim the specialism. - Test-plan honesty. PR body is candid that local aggregate
npm testhit 60s file-level timeouts under load on two CLI test files; both pass individually. Thepackage.jsonslow-path split (180s timeout) plus CItimeout-minutes: 15 → 25is the right fix shape.
Follow-ups (non-blocking — file as issues)
payload_lengthvs. canonicalized digest bytes.recorder.ts:420forwardscall.payload_lengthunchanged into the digest projection, butpayload_digest_sha256at line 401 is computed overcanonicalJsonStringify(payload)(sorted-keys canonical form). The two refer to different byte representations — for any object with non-alphabetic keys, the canonical bytes differ in length fromJSON.stringify(redacted). Either recomputepayload_lengthfromcanonicalPayloadBytes(...)in the digest branch, or document thatpayload_lengthis the raw-projection length and the digest stands alone.allIdentifiersNotApplicableerases payload-must-contain credit.validations.ts:2773-2778: a validation that has BOTHpayload_must_contain(passing) ANDidentifier_paths(digest-only matches →not_applicable) currently grades the whole validationnot_applicable, swallowing the passing payload checks. Tighten the predicate to require both arms to be unused-or-N/A together.- Index signatures defeat union exhaustiveness in the test-controller mirror.
test-controller.ts:249, 260add[key: string]: unknownto bothRawRecordedCallandDigestRecordedCall. A literal carrying bothpayload_digest_sha256andpayloadwould satisfy both arms at compile time —upstream-recorder/types.tsdoesn't have this issue. Drop the index signatures;UpstreamTrafficSuccessResponse.recorded_calls: ReadonlyArray<unknown>already gives adopters the pass-through. - 64-vector identifier cap silently clips.
runner.ts:~5219(collectUpstreamIdentifierDigests) andrecorder.ts:434(normalizeIdentifierDigests) both cap at 64. A storyboard with >64identifier_paths-resolved vectors silently drops the overflow on the wire AND fails the correspondinganyMatchedCallEchoesValuechecks becausevalueDigestcomes backundefined. Surface a runner notice when the cap clips. - Default attestation-mode policy.
runner.ts:5145-5149falls back to'digest'whenever there are identifier digests and no raw-introspection peer. Consider explicit opt-in via a storyboard-levelpreferred_attestation_moderather than inference — fail-closed default israw. jsonStringLeafDigestsdepth cap is asymmetric with the digest.recorder.ts:439-456caps the identifier-leaf walk at depth 256;canonicalizeJson(line 474) recurses unbounded. A hostile payload at depth 257 produces a valid digest but escapes identifier matching. Either cap both or document the inversion.
Minor nits (non-blocking)
- Regex trailing-space capture.
summary.ts:273matches[^;(]+and captures the trailing space before(rationale)..trim()on line 275 normalizes it — works, but tighten to[^;(]+?(?=\s*\(|;|$)to keep the capture honest. host/pathstrictness asymmetry.upstream-recorder/types.ts:29-30has both required;test-controller.ts:217-218has both optional. The cached spec treats them as required. The consumer-side permissive shape is the safe direction, but aligning would let the type system enforce the strict contract.
Safe to merge.
Summary
Verification
Note: local aggregate npm test hit 60s whole-file timeouts under load for two CLI test files on one run; both files pass individually and the direct node suite passed in the captured run.