From 570871a447cbf90d124a8008c716855b5636537f Mon Sep 17 00:00:00 2001 From: Brian O'Kelley Date: Tue, 21 Apr 2026 18:40:19 -0400 Subject: [PATCH] fix(training-agent): uniform not-found on property-list handlers (#2739) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Get/update/delete/validate_property_delivery were returning `{ code: 'not_found', message: "No property list with id ''" }`, echoing the probed list_id into the error body. Two paired probes with distinct unresolvable ids produced distinguishable responses — a cross-tenant enumeration oracle the AdCP 3.0 uniform-error-response MUST forbids. Now returns `{ code: 'REFERENCE_NOT_FOUND', message: 'Property list not found', field: 'list_id' }`, byte-identical across probes. REFERENCE_NOT_FOUND is the spec's canonical fallback for property lists (error-code.json enum description names property lists explicitly); error.field is the caller-supplied parameter name, not a resolved type. Adds paired-probe tests for all four handlers to lock the invariant. Co-Authored-By: Claude Opus 4.7 (1M context) --- ...ning-agent-property-list-uniform-errors.md | 6 +++ .../src/training-agent/property-handlers.ts | 8 ++-- server/tests/unit/training-agent.test.ts | 44 +++++++++++++++++++ 3 files changed, 54 insertions(+), 4 deletions(-) create mode 100644 .changeset/training-agent-property-list-uniform-errors.md diff --git a/.changeset/training-agent-property-list-uniform-errors.md b/.changeset/training-agent-property-list-uniform-errors.md new file mode 100644 index 0000000000..0063bfa1a2 --- /dev/null +++ b/.changeset/training-agent-property-list-uniform-errors.md @@ -0,0 +1,6 @@ +--- +--- + +Training agent: `get_property_list`, `update_property_list`, `delete_property_list`, and `validate_property_delivery` now return a uniform error on unresolved `list_id` — `error.code` is `REFERENCE_NOT_FOUND` (was lowercase `not_found`), `error.message` is the generic `"Property list not found"` (was `"No property list with id ''"`), and `error.field` is `list_id`. Closes #2739. + +The probed id is no longer echoed back on the error path, so paired-probe uniform-response invariants (e.g. `@adcp/client` fuzz) now pass against the public test agent. This matters because the training agent is the reference implementors point conformance runs at; an echoing message there invalidates the signal for every downstream seller running the same invariant. diff --git a/server/src/training-agent/property-handlers.ts b/server/src/training-agent/property-handlers.ts index 3fde648464..8739656c01 100644 --- a/server/src/training-agent/property-handlers.ts +++ b/server/src/training-agent/property-handlers.ts @@ -257,7 +257,7 @@ export async function handleGetPropertyList( const state = session.propertyLists.get(req.list_id); if (!state) { - return { errors: [{ code: 'not_found', message: `No property list with id '${req.list_id}'` }] }; + return { errors: [{ code: 'REFERENCE_NOT_FOUND', message: 'Property list not found', field: 'list_id' }] }; } const domains = extractDomains(state.baseProperties); @@ -280,7 +280,7 @@ export async function handleUpdatePropertyList( const state = session.propertyLists.get(req.list_id); if (!state) { - return { errors: [{ code: 'not_found', message: `No property list with id '${req.list_id}'` }] }; + return { errors: [{ code: 'REFERENCE_NOT_FOUND', message: 'Property list not found', field: 'list_id' }] }; } if (req.name) { @@ -324,7 +324,7 @@ export async function handleDeletePropertyList( const existed = session.propertyLists.delete(req.list_id); if (!existed) { - return { errors: [{ code: 'not_found', message: `No property list with id '${req.list_id}'` }] }; + return { errors: [{ code: 'REFERENCE_NOT_FOUND', message: 'Property list not found', field: 'list_id' }] }; } return { list_id: req.list_id, deleted: true }; @@ -343,7 +343,7 @@ export async function handleValidatePropertyDelivery( const state = session.propertyLists.get(req.list_id); if (!state) { - return { errors: [{ code: 'not_found', message: `No property list with id '${req.list_id}'` }] }; + return { errors: [{ code: 'REFERENCE_NOT_FOUND', message: 'Property list not found', field: 'list_id' }] }; } const records = req.records || []; diff --git a/server/tests/unit/training-agent.test.ts b/server/tests/unit/training-agent.test.ts index e8c603d0ba..2f60630b5e 100644 --- a/server/tests/unit/training-agent.test.ts +++ b/server/tests/unit/training-agent.test.ts @@ -6860,6 +6860,50 @@ describe('get_brand_identity handler', () => { }); }); +describe('property-list uniform not-found response (issue #2739)', () => { + beforeEach(async () => { + await clearSessions(); + }); + afterEach(async () => { + await clearSessions(); + stopSessionCleanup(); + }); + + // Paired-probe: two distinct unresolvable list_ids must produce byte-identical + // error bodies, otherwise the probed id is a cross-tenant enumeration oracle. + const PROBE_TOOLS = ['get_property_list', 'update_property_list', 'delete_property_list'] as const; + for (const toolName of PROBE_TOOLS) { + it(`${toolName} returns byte-identical errors for two distinct unresolvable list_ids`, async () => { + const server = createTrainingAgentServer(DEFAULT_CTX); + const account = { brand: { domain: 'uniform-probe.example' }, operator: 'pinnacle-agency.com' }; + + const probeA = await simulateCallTool(server, toolName, { account, list_id: 'd7aff8ea-136c-498f-b70f-a69582ad3bec' }); + const probeB = await simulateCallTool(server, toolName, { account, list_id: '221acd34-cd2c-4763-ae0a-321c1e85fb2b' }); + + expect(probeA.isError).toBe(probeB.isError); + expect(probeA.result).toEqual(probeB.result); + expect(probeA.result.code).toBe('REFERENCE_NOT_FOUND'); + expect(probeA.result.message).toBe('Property list not found'); + expect(probeA.result.field).toBe('list_id'); + }); + } + + it('validate_property_delivery returns byte-identical errors for two distinct unresolvable list_ids', async () => { + const server = createTrainingAgentServer(DEFAULT_CTX); + const account = { brand: { domain: 'uniform-probe.example' }, operator: 'pinnacle-agency.com' }; + const records = [{ identifier: { type: 'domain', value: 'x.example' }, impressions: 1 }]; + + const probeA = await simulateCallTool(server, 'validate_property_delivery', { account, list_id: 'd7aff8ea-136c-498f-b70f-a69582ad3bec', records }); + const probeB = await simulateCallTool(server, 'validate_property_delivery', { account, list_id: '221acd34-cd2c-4763-ae0a-321c1e85fb2b', records }); + + expect(probeA.isError).toBe(probeB.isError); + expect(probeA.result).toEqual(probeB.result); + expect(probeA.result.code).toBe('REFERENCE_NOT_FOUND'); + expect(probeA.result.message).toBe('Property list not found'); + expect(probeA.result.field).toBe('list_id'); + }); +}); + describe('cross-machine session persistence', () => { beforeEach(async () => { await clearSessions();