Skip to content

client.reports.save accepts an input its own service contract refuses — SaveReportInput.query is required and neither the SDK nor the route checks it #11926

Description

@os-zhuang

Found while implementing #8140, which narrowed return types only. This is on the input side
and was deliberately not ridden into that PR.

Measured at 1f6d04703.

What is wrong

client.reports.save declares its parameter as any:

save: async(report: any): Promise<SavedReport>=>{constres=awaitthis.fetch(`${this.baseUrl}/api/v1/reports`,{method: 'POST',body: JSON.stringify(report??{}),});returnthis.unwrapResponse<SavedReport>(res);},

The type the route's service method actually takes is SaveReportInput
(packages/spec/src/contracts/report-service.ts:89-97), on which query: ReportQuery is
required
:

exportinterfaceSaveReportInput{id?: string;name: string;description?: string;object: string;query: ReportQuery;// ← requiredformat?: ReportFormat;ownerId?: string;}

And the route does not check it either — POST /reports forwards the body straight through
(packages/rest/src/rest-server.ts:10293):

constrow=awaitsvc.saveReport(req.body??{},context??{});

So a caller can send a report definition with no query at all; whether that is refused, stored
half-built, or throws depends entirely on which reports implementation is mounted.

How it was measured — this is not a code-reading claim

While implementing #8140 I briefly bound the parameter to SaveReportInput. tsc immediately
failed on this repo's own test, packages/client/src/client.test.ts:352:

src/client.test.ts(352,35): error TS2345: Argument of type '{ name: string; object: string; }'
is not assignable to parameter of type 'SaveReportInput'.
Property 'query' is missing in type '{ name: string; object: string; }' but required in
type 'SaveReportInput'.

That call is client.reports.save({ name: 'Pipeline', object: 'lead' }) — a fixture that has been
constructing an input the contract refuses, invisibly, because the parameter is any. I reverted
the parameter narrowing (out of #8140's declared scope) rather than adjust the fixture to make a
type error go away, since the fixture is evidence, not noise.

⚠️The fixture is not itself the defect and should not be "fixed" alone: it is a mock-transport
test that never reaches a real service, so adding a query to it silences the signal without
closing anything. The defect is that nothing on the path — SDK parameter, route, or a schema at the
door — states the requirement.

Adjacent, same class

Seven public SDK methods take a parameter typed any (packages/client/src/index.ts):

$ grep -nE '^\s+[a-zA-Z_]+: async \(.*: any[,)]' packages/client/src/index.ts
1161: query: async (payload: any) => {
1185: explain: async (payload: any) => {
3039: upload: async (file: any, scope: string = 'user'): Promise<FileUploadResponse> => {
3264: trigger: async (triggerName: string, payload: any) => {
3302: create: async (name: string, definition: any): Promise<any> => {
3318: update: async (name: string, definition: any): Promise<any> => {
4185: save: async (report: any): Promise<SavedReport> => {

reports.save is the one with a measured consequence, so it is the subject of this card; the
other six are listed so a fix can consider whether the same reachable-contract argument applies
(automation.create / update at 3302 / 3318 are the input half of #11924's routes).

Why it matters beyond typing

This is the contract-first shape from AGENTS.md Prime Directive #12 read from the producer side: an
authoring surface that accepts off-spec input and passes it to a service that requires more. The
right fix is to reject it loudly at the door — a schema at the route, or the parameter typed to the
contract, or both — rather than to keep the SDK permissive and let each reports implementation
decide what a query-less report means.


Generated by Claude Code

Metadata

Metadata

Assignees

Type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions