Skip to content

fix(v2): stop telling callers something the server did not do - #6676

Merged
waleedlatif1 merged 1 commit into
stagingfrom
fix/v2-caller-truthfulness
Aug 13, 2026
Merged

fix(v2): stop telling callers something the server did not do#6676
waleedlatif1 merged 1 commit into
stagingfrom
fix/v2-caller-truthfulness

Conversation

@waleedlatif1

Copy link
Copy Markdown
Collaborator

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

input document list search
number "" = 0 filter dropped — whole KB, 200
boolean "TRUE" = true = false, inverted
number "0x10" = 16 = 0
contains "50%off" literal (escaped) % is a wildcard
date " 2026-08-13" dropped — whole KB, 200

Two of these answer 200 with the entire knowledge base, and /knowledge/search is 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} with scope: personal stored 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 no permissions row for the credential fan-out to find. The obvious fix — widening getUserWorkspaceIds — 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/import and Copilot both wrote through name-only checks. Every write now passes the same guard the response schema is derived from.

The workspaceId descriptions claimed a personal secret lives in one workspace. It does not — deletePersonalEnvCredentialForUser has no workspaceId predicate 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/tables accepted workflowGroupId on 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 createTable runs 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-columns pulled the executable tool registry into the tables page graph — 1,767 modules to 6,999 — which check:audits caught 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 try and was an OrchestrationError, not an HttpError, 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

  • Full suite: 1,857 files / 24,695 tests passed, 0 failed
  • type-check 23/23, biome, check:api-validation, check:audits 26/26, check:openapi — all clean
  • OpenAPI regenerated; the spec diff contains only the corrected secret descriptions and the removed workflowGroupId

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`.
@vercel

vercel Bot commented Aug 13, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
docs Ready Ready Preview Aug 13, 2026 9:18pm

Request Review

@cursor

cursor Bot commented Aug 13, 2026

Copy link
Copy Markdown

PR Summary

Medium Risk
Touches knowledge query semantics, secret write responses, table create validation, and upload error mapping—behavior changes for edge-case inputs, but aligned with documented contracts and regression tests; no auth bypass, though tag-filter errors now fail requests instead of silently widening results.

Overview
Closes a recurring gap where inputs were validated for shape but meaning was re-derived downstream, so filters behaved differently than validation implied or writes succeeded while responses reported failure.

Knowledge tag filters now coerce values once via coerceTagFilterValue for both document lists and search; LIKE patterns escape %/_; filters that cannot compile after validation raise instead of being dropped (which previously returned the whole KB on 200, including billed search).

Personal secrets no longer infer success from a per-workspace credential mirror—getPersonalEnvCredentialMetadata and projected metadata fix 500s after committed writes when the caller has inherited access without a local mirror. OpenAPI/contract text clarifies that personal secrets are user-global, not workspace-scoped.

Custom tools add assertStorableCustomToolSchema on v2 updates, upserts, and import persistence so stored schemas match what list/update responses can serialize (avoids commit-then-500 and broken list pages).

Tables: v2 create drops workflowGroupId on initial columns; createTable runs assertValidSchema like later mutations. Schema invariants move to schema-invariants.ts to avoid pulling the tool registry into table UI bundles.

Upload multipart maps out-of-range part numbers from expectedUploadPartSize to 400 via v2CaughtOrchestrationError on the local data-plane route.

Reviewed by Cursor Bugbot for commit cf6a14a. Configure here.

@greptile-apps

greptile-apps Bot commented Aug 13, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

The PR makes v2 write and filter responses reflect the operation the server actually performed, while tightening table-schema and upload-part validation.

  • Centralizes typed knowledge-tag coercion and aligns document-list and search SQL compilation.
  • Validates custom-tool schemas before persistence across all write ingresses.
  • Returns personal-secret metadata without requiring a workspace-local mirror.
  • Rejects dangling workflow-group references during table creation and extracts schema invariants into a lightweight leaf module.
  • Maps invalid multipart part numbers to the documented client-error response and updates generated OpenAPI descriptions.

Confidence Score: 5/5

The 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.

Important Files Changed

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]
Loading

Reviews (1): Last reviewed commit: "fix(v2): stop telling callers something ..." | Re-trigger Greptile

@waleedlatif1
waleedlatif1 merged commit e2b7335 into staging Aug 13, 2026
24 checks passed
@waleedlatif1
waleedlatif1 deleted the fix/v2-caller-truthfulness branch August 13, 2026 21:19
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant