fix(v2): stop telling callers something the server did not do - #6676
Conversation
A works-as-advertised sweep of the v2 surface found one defect class in five
places: input is validated for shape, then its meaning is re-derived
independently by each consumer — so a filter compiles differently than it
validated, or a write commits and the response then reports failure.
Knowledge tag filters were validated once and re-parsed three times. The
document list read `Number()`, search read `parseFloat()`; the list matched
booleans case-insensitively, search compared against the literal `'true'`; the
list escaped LIKE metacharacters, search did not; and the date pattern was
tested against the untrimmed string the validator had already trimmed. Two of
those paths dropped the predicate entirely and answered 200 with the whole
knowledge base — on a billed endpoint. Values are now coerced once, where the
resolved field type is known, and both builders consume the result. A builder
that cannot compile an already-validated filter now raises instead of silently
widening the result set.
`PUT /api/v2/secrets/{name}` with `scope: personal` committed the secret and
then answered 500, because a user-global write was reported through a
workspace-scoped mirror lookup, and an org admin's inherited access has no
`permissions` row for the fan-out to find. The personal path no longer decides
success from a per-workspace mirror. The `workspaceId` descriptions said a
personal secret lives in one workspace; it does not, and they now say so.
A custom tool could be stored with a schema the read path cannot serialize —
`POST /workflows/import` and Copilot both wrote through name-only checks — so
one row made the whole workspace list 500, and a title-only PATCH committed,
audited, then reported failure. Every write now passes the same guard the
response schema is derived from.
`POST /api/v2/tables` accepted `workflowGroupId` on an initial column. Nothing
can populate it legitimately, and it made every later column-add and group-add
fail with no way to clear it. The key is refused at the boundary, and
`createTable` now runs the invariant every later mutation already runs, closing
the internal and v1 ingresses too. Those invariants moved to a leaf module:
reaching them through `workflow-columns` pulled the executable tool registry
into the tables page graph, taking it from 1,767 modules to 6,999.
An out-of-range upload part number answered 500 rather than the 400 its
published contract promises, because the throw happened above the route's
try/catch and was not an `HttpError`.
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
PR SummaryMedium Risk Overview Knowledge tag filters now coerce values once via Personal secrets no longer infer success from a per-workspace credential mirror— Custom tools add Tables: v2 create drops Upload multipart maps out-of-range part numbers from Reviewed by Cursor Bugbot for commit cf6a14a. Configure here. |
Greptile SummaryThe PR makes v2 write and filter responses reflect the operation the server actually performed, while tightening table-schema and upload-part validation.
Confidence Score: 5/5The PR appears safe to merge, with no concrete changed-code defect remaining after review. The shared coercion paths compile supported tag filters consistently, write guards run before persistence, personal-secret metadata no longer depends on a workspace mirror, table creation enforces existing invariants, and upload validation errors are mapped without broadening unexpected-error handling.
|
| Filename | Overview |
|---|---|
| apps/sim/lib/knowledge/tags/utils.ts | Centralizes deterministic text, number, date, and boolean filter coercion plus SQL LIKE escaping. |
| apps/sim/lib/knowledge/documents/tag-filter.ts | Compiles document-list predicates from shared typed coercion and refuses filters it cannot compile. |
| apps/sim/lib/knowledge/search/queries.ts | Aligns search predicate coercion and literal LIKE escaping with document-list behavior. |
| apps/sim/lib/custom-tools/schema.ts | Adds the shared guard ensuring persisted custom-tool schemas satisfy the response declaration contract. |
| apps/sim/lib/custom-tools/application/use-cases.ts | Applies schema validation before custom-tool updates so committed writes remain renderable in API responses. |
| apps/sim/lib/secrets/application/use-cases.ts | Decouples successful personal-secret responses from the existence of a workspace-local credential mirror. |
| apps/sim/lib/table/schema-invariants.ts | Extracts schema consistency checks into a dependency-light leaf module shared by table mutations. |
| apps/sim/lib/table/service.ts | Enforces workflow-group schema invariants before newly created tables are persisted. |
| apps/sim/app/api/v2/uploads/[uploadId]/parts/[partNumber]/route.ts | Converts classified part-size validation failures into canonical v2 client responses while preserving generic handling for unexpected errors. |
| apps/sim/lib/uploads/upload-session/service.ts | Classifies invalid multipart part numbers as validation failures for consistent route-level error mapping. |
| apps/sim/lib/api/contracts/v2/tables.ts | Removes client-supplied workflowGroupId from initial v2 columns, preventing dangling server-owned group references. |
Flowchart
%%{init: {'theme': 'neutral'}}%%
flowchart LR
Input[API input] --> Validate[Resolve type and validate meaning]
Validate --> Normalize[Coerce once]
Normalize --> Consumer{Consumer}
Consumer --> DocumentList[Document-list SQL]
Consumer --> Search[Search SQL]
Consumer --> WriteGuard[Write schema guard]
WriteGuard --> Persist[(Persist)]
Persist --> Response[Contract-valid response]
Validate --> Reject[Actionable client error]
Reviews (1): Last reviewed commit: "fix(v2): stop telling callers something ..." | Re-trigger Greptile
A works-as-advertised sweep of the v2 surface found one defect class in five places: input is validated for shape, then its meaning is re-derived independently by each consumer. So a filter compiles differently than it validated, or a write commits and the response then reports failure.
Each fix was verified red-then-green — the source change reverted, the test watched to fail, then restored.
Knowledge tag filters — validated once, parsed three times
""= 0"TRUE"= true= false, inverted"0x10"= 16= 0contains "50%off"%is a wildcard" 2026-08-13"Two of these answer 200 with the entire knowledge base, and
/knowledge/searchis billed, so a dropped filter means paying for a search over everything.Fixed at the root rather than by making one builder match the other: values are coerced once, where the resolved field type is known, and both builders consume the result. A builder that cannot compile an already-validated filter now raises instead of silently widening. Six regression guards pass against both old and new code, proving correct filters still match what they matched. v1's search route builds these filters itself and is fixed by the same change.
Write commits, response reports failure — two more instances
The enrichment-group regression fixed in #6671 had two siblings.
PUT /v2/secrets/{name}withscope: personalstored the secret, then answered 500: a user-global write was reported through a workspace-scoped mirror lookup, and an org admin's inherited access has nopermissionsrow for the credential fan-out to find. The obvious fix — wideninggetUserWorkspaceIds— was rejected: it would fan every personal secret's metadata into every workspace of every org the caller administers. The personal path no longer decides success from a per-workspace mirror.PATCH /v2/custom-tools/{id}committed and audited, then 500'd on the response parse; one bad row also 500'd the entire workspace list.POST /workflows/importand Copilot both wrote through name-only checks. Every write now passes the same guard the response schema is derived from.The
workspaceIddescriptions claimed a personal secret lives in one workspace. It does not —deletePersonalEnvCredentialForUserhas noworkspaceIdpredicate at all. The descriptions were wrong, not the semantics: a workspace-scoped delete would leave the encrypted value present and strip one mirror.Table bricking, closed on all three ingresses
POST /v2/tablesacceptedworkflowGroupIdon an initial column. Nothing can populate it legitimately — v2 mints group ids server-side — so any value names a group that does not exist, and every later column-add and group-add then 400s permanently with no way to clear it.The key is now refused at the boundary, and
createTableruns the invariant every later mutation already runs, closing the internal and v1 ingresses too. Verified against production: 0 of 39,722 tables are affected, so this is prevention, not repair.Those invariants moved to a leaf module. Reaching them through
workflow-columnspulled the executable tool registry into the tables page graph — 1,767 modules to 6,999 — whichcheck:auditscaught and lazy importing did not fix.Upload data plane
An out-of-range part number answered 500 rather than the documented 400: the throw happened above the route's
tryand was anOrchestrationError, not anHttpError, so typed dispatch missed it. Reachable by editing the trailing segment of a legitimately issued part URL.Deliberately not changed
The release's intentional compatibility changes;
isValidCustomToolSchema(tightening it would silently delete tools from users' workflow graphs); and the custom-tools read path — every page-preserving option is worse than the failure, and with the write side closed the residue is two known production rows, a data-repair problem rather than an API-shape one.Verification
check:api-validation,check:audits26/26,check:openapi— all cleanworkflowGroupId