Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/projection-keeps-envelope.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@adcp/sdk': patch
---

Version translation no longer removes protocol envelope fields. When a response was converted down to an older release whose task schema didn't compose the protocol envelope (the 3.2 proposal-flow responses, which AdCP 3.2.x and 3.3 fix as DR-0036 correction (b)), the schema projection treated `status`, `message`, `context` and the other envelope fields as newer-only and removed them, so a 3.2 buyer could receive a 3.3 server's `list_products` response without its `status`. The projection now keeps every property the release's protocol and version envelopes declare at the payload root.
52 changes: 50 additions & 2 deletions src/lib/adapters/version/schema-projection.ts
Original file line number Diff line number Diff line change
Expand Up @@ -261,7 +261,8 @@ function project(
target: SchemaNode[],
path: string,
dropped: ProjectionDrop[],
depth: number
depth: number,
protectedKeys: ReadonlySet<string> = NO_PROTECTED_KEYS
): unknown {
if (depth > MAX_DEPTH || source.length === 0 || target.length === 0) return value;
const sourceExpansions = source.map(node => expand(node, value, depth));
Expand Down Expand Up @@ -323,6 +324,10 @@ function project(
const sourceSchemas = schemasForKey(sourceIndex, key) ?? nonEmpty(sourceIndex.additional);
const targetSchemas = schemasForKey(targetIndex, key) ?? nonEmpty(targetIndex.additional);
const entryPath = joinPath(path, key);
if (protectedKeys.has(key)) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Medium: The protection is unconditional — it continues before recursing, so structured envelope keys (context, governance_context, push_notification_config, payload) are kept wholesale even when the target release composes the envelope and could project their contents. For a 3.3 → 3.2.8 down-conversion (DR-0036 correction (b) backports the envelope into 3.2.8, so 3.2.8 declares these keys), a newer-only sub-field of context/governance_context now leaks unprojected and, because this path skips dropped.push, unreported. The witness-not-translator contract requires every removed-or-undeliverable field to surface as drift; this short-circuit silently keeps them instead. The surgical shape protects a key only when the target does not declare it — when the target declares the envelope, normal projection already keeps the key and still reports nested drift. Same set is also applied to request projection (translate.ts:179), so envelope-keyed request fields bypass the mutating-refusal gate; that's benign for the current field set but widens the blast radius. No test covers nested envelope-field drift.

out[key] = entry;
continue;
}
if (sourceIndex.declares && targetIndex.declares && schemasForKey(sourceIndex, key) && !targetSchemas) {
dropped.push({ path: entryPath, kind: 'field' });
changed = true;
Expand Down Expand Up @@ -371,10 +376,53 @@ export function projectToolPayload<T>(
const target = toolRoot(toolName, direction, toVersion);
if (!source || !target || !isObject(payload)) return { value: payload, dropped: [] };
const dropped: ProjectionDrop[] = [];
const projected = project(payload, [source], [target], '', dropped, 0);
const projected = project(payload, [source], [target], '', dropped, 0, envelopeKeys(toVersion));
return { value: (projected === OMIT ? payload : projected) as T, dropped };
}

const NO_PROTECTED_KEYS: ReadonlySet<string> = new Set();

/** Envelope fields the protocol has defined since 3.0, used when a bundle omits the envelope schemas. */
const BASELINE_ENVELOPE_KEYS = [
'adcp_error',
'adcp_major_version',
'adcp_version',
'context',
'context_id',
'governance_context',
'message',
'payload',
'push_notification_config',
'replayed',
'status',
'task_id',
'timestamp',
];

const envelopeKeyCache = new Map<string, ReadonlySet<string>>();

/**
* Root-level fields of the protocol and version envelopes. The envelope
* applies to every task payload whether or not a task schema composes it
* (some 3.2 proposal-flow responses did not, which DR-0036 correction (b)
* fixes), so the projection never removes them.
*/
function envelopeKeys(version: string): ReadonlySet<string> {
const cached = envelopeKeyCache.get(version);
if (cached) return cached;
const keys = new Set(BASELINE_ENVELOPE_KEYS);
for (const ref of ['core/protocol-envelope.json', 'core/version-envelope.json']) {
try {
const properties = getSchemaDocumentByRef(ref, version)?.schema.properties;
if (isObject(properties)) for (const key of Object.keys(properties)) keys.add(key);
} catch {
// A bundle without the envelope schemas keeps the baseline set.
}
}
envelopeKeyCache.set(version, keys);
return keys;
}

/** Whether the release's schema bundle defines a request schema for the tool. */
export function releaseDefinesTool(toolName: string, version: string): boolean {
try {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
{
"description": "Cross-version translation between consecutive AdCP minor releases (DR-0032, DR-0033): request down-conversion and response up-conversion on the client, response down-conversion on the server, release negotiation, and one-to-one account-key reduction.",
"description": "Cross-version translation between AdCP 3.2 and 3.1 (DR-0032, DR-0033), chained to 3.0 where noted: client request down-conversion and response up-conversion, server response down-conversion, release negotiation, and one-to-one reduction of the account key 3.2 changed on schemas 3.1 published closed.",
"reference_implementation": "@adcp/sdk (adcontextprotocol/adcp-client#3372, #3376)",
"version_pair": ["3.2", "3.1"],
"cases": [
Expand Down Expand Up @@ -1072,5 +1072,5 @@
]
}
],
"status": "proposed (DR-0032 and DR-0033 pending ratification; DR-0034 escalated)"
"status": "proposed (DR-0032, DR-0033)"
}
Loading
Loading