Skip to content

fix(v2): close the correctness gaps the release audit found - #6671

Merged
waleedlatif1 merged 1 commit into
stagingfrom
fix/v2-release-audit-findings
Aug 13, 2026
Merged

fix(v2): close the correctness gaps the release audit found#6671
waleedlatif1 merged 1 commit into
stagingfrom
fix/v2-release-audit-findings

Conversation

@waleedlatif1

Copy link
Copy Markdown
Collaborator

An end-to-end audit of the v0.8.1 release (#6646) surfaced one regression the release itself introduces, plus a set of filter and cursor gaps where a caller's spelling changed what a query answered. This closes them.

The regression

POST /api/v2/tables/{tableId}/groups with type:"enrichment" wrote a row that then broke every later read of that table's groups.

The request contract invites a caller to omit workflowId ("omit it for an enrichment group"). The write persisted caller input through an as WorkflowGroup cast, which hid the omission from the type checker. The response schema still required the field — and the v2 builder validates outbound bodies — so the write committed and then the response threw an opaque 500 that never returned the groupId. Because the list presenter maps every group through that schema, one such row made GET, PATCH and DELETE on that table's groups fail permanently, with no public way to remove it.

The cast is removed rather than papered over, so the same class of omission cannot recur silently. Verified: no table on staging currently carries a poisoned group, so this is fix-before-exposure.

Filters that silently answered a different question

Knowledge tag filters accepted any operator string (z.string().default('eq')) and then ignored an unrecognized one in opposite directions — the document list dropped the predicate and returned the whole knowledge base, while search fell through to equality. Proven live on staging: same tag and value, operator:"eq" → 1 item, operator:"nosuchop" → 100 items.

Both now reject at the boundary via a per-field-type operator enum, plus a between-requires-valueTo rule. .strict() is applied on the v2 chain only — v1 has always stripped unrecognized keys and is a shipped public API, and the between rule already closes the dangerous mis-cased valueTo case on both versions. A test pins the v1 leniency as deliberate.

Cursor scopes bound set-valued filters to the caller's ordering: all/any clause order and in/nin operand order in the table predicate, and the raw resourceType text on audit logs, whose query splits it into an inArray. Audit logs is canonicalized on both sides — canonicalizing the scope alone would have given two genuinely different result sets one fingerprint, which is worse than the bug.

Also

  • The exposed-header list reached only the fallback CORS policy, so all five matched rules — including the wildcard-origin execute route, the only one emitting X-Run-Id — could not hand a browser the run id or a 429's Retry-After. Rules now inherit the default and opt out explicitly.
  • A bulk row update reported an uncoercible value only when its filter happened to match, so an identical body returned 200 or 400 depending on match count.
  • The cost and duration windows accepted an inverted pair and answered with an empty page, reporting "those runs do not exist" for a caller mistake.
  • The sortless runs list advised callers to fix a sortBy its .strict() schema rejects.
  • Two dead v2ValidationError imports.

Deliberately not changed

The release's intentional compatibility changes are left alone — folderPath 404→empty 200, .strict() query params, the skills/billing policy changes, and strictWrite. Also unchanged: 405s lacking an Allow header (Next App Router default, needs middleware across the whole surface), and the four collections that emit nextCursor but reject limit (needs a direction chosen).

Two audit findings were retracted on closer reading rather than fixed: the upload PUT's parse-before-auth ordering (the contract declares no body, so parseRequest never touches the stream, and the token comes from the parsed headers — the ordering is forced), and audit-logs action binding raw (it is eq(...), a genuine exact match).

Verification

Every fix was confirmed red-then-green — the source change reverted, the test watched to fail, then restored. Two cursor tests pass in both states by design; they are the over-canonicalization guards, and a guard that only passes after the fix would not be guarding anything.

  • Full suite: 1850 files / 24,632 tests passed, 0 failed
  • type-check 23/23 workspaces, biome, check:api-validation, check:audits 26/26, check:openapi — all clean
  • OpenAPI regenerated; the spec diff contains only the intended description and bounds changes

An end-to-end audit of the v0.8.1 release surfaced one regression the
release itself introduced and a set of filter/cursor gaps that let a
caller's spelling change what a query answered.

An `enrichment` workflow group stored no `workflowId`. The public
contract invites a caller to omit it, the write persisted caller input
through an `as WorkflowGroup` cast that hid the omission from the type
checker, and the response schema still required it — so the write
committed and then the outbound parse threw. Because the list presenter
maps every group through that schema, one such row made GET, PATCH and
DELETE on that table's groups fail from then on, with no public way to
remove it. The cast is gone rather than papered over, so the same class
of omission cannot recur silently.

Knowledge tag filters accepted any operator string and then ignored an
unrecognized one in opposite directions: the document list dropped the
predicate and answered with the whole knowledge base, while search fell
through to equality and answered a different question. Both now reject
at the boundary. `.strict()` is applied on the v2 chain only — v1 has
always stripped unrecognized keys, and the `between`-requires-`valueTo`
rule already closes the mis-cased `valueTo` trap on both versions.

Cursor scopes bound set-valued filters to the caller's ordering:
`all`/`any` clause order and `in`/`nin` operand order in the table
predicate, and the raw `resourceType` text on audit logs, whose query
splits it into an `inArray`. Audit logs is canonicalized on both sides,
because canonicalizing the scope alone would have given two genuinely
different result sets one fingerprint.

Also: the exposed-header list reached only the fallback CORS policy, so
all five matched rules — including the wildcard-origin execute route,
the only one that emits `X-Run-Id` — could not hand a browser the run id
or a 429's `Retry-After`; a bulk row update reported an uncoercible
value only when its filter happened to match; the cost and duration
windows accepted an inverted pair and answered it with an empty page;
and the sortless runs list advised callers to fix a `sortBy` it rejects.
@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 7:16pm

Request Review

@cursor

cursor Bot commented Aug 13, 2026

Copy link
Copy Markdown

PR Summary

Medium Risk
Touches public v2 validation, pagination binding, and persisted table group shape—behavior changes for some previously accepted requests—but changes are narrowly scoped with broad test coverage and mostly reject mistakes instead of altering happy paths.

Overview
Closes correctness gaps from a v0.8.1 release audit where callers’ spelling or invalid inputs changed query results, broke pagination, or left poisoned data.

Regression: POST /api/v2/tables/{tableId}/groups with type: enrichment could omit workflowId; the write succeeded but later group reads failed. Omitted workflowId (and output blockId/path) now default to '' instead of relying on an unsafe cast.

Knowledge tag filters: v1/v2 schemas now reject unknown operators and between without valueTo; v2 tag filter objects are .strict() so mis-cased keys are not stripped. Resolution layer validates operators per field type instead of dropping predicates or coercing to equality.

Cursor / set-valued filters: Audit resourceType and table row predicates (all/any, in/nin) fingerprint membership, not order or raw comma text, with query and scope using the same parse. Workflow runs list uses UNREADABLE_CURSOR_MESSAGE for bad cursor keys instead of sort-related wording.

Logs list: minCost/maxCost bounded 0–1M; inverted cost/duration windows return 400 instead of empty pages.

Other: CORS applies default exposed headers (X-Run-Id, rate-limit headers, Retry-After) to all matched API rules; bulk table updates validate the patch before scanning rows; dead upload-route imports removed. OpenAPI descriptions updated accordingly.

Reviewed by Cursor Bugbot for commit 7be8c7b. Configure here.

@greptile-apps

greptile-apps Bot commented Aug 13, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

The PR closes v2 correctness gaps around table-group persistence, filter validation, cursor binding, bulk-update validation, CORS response-header visibility, and range-validation errors.

  • Normalizes enrichment groups into the persisted shape expected by later reads.
  • Validates knowledge-tag operators and numeric, duration, and between bounds.
  • Canonicalizes unordered cursor scopes consistently with query execution.
  • Validates bulk patches before scanning rows and exposes shared API response headers across CORS policies.
  • Updates tests and generated OpenAPI descriptions for the corrected behavior.

Confidence Score: 5/5

The PR appears safe to merge; no concrete changed-code defect remains after reviewing the corrected contracts, persistence paths, cursor semantics, bulk-update validation, and CORS behavior.

The new validation and normalization paths align with their downstream query, persistence, and presentation contracts, and the cursor and CORS changes preserve the relevant scope and credential boundaries.

Important Files Changed

Filename Overview
apps/sim/lib/table/application/groups.ts Replaces an unsafe cast with explicit normalization of enrichment-group workflow and output fields.
apps/sim/lib/api/contracts/v1/knowledge/index.ts Constrains tag operators and requires an upper bound for between while retaining v1 unknown-key leniency.
apps/sim/lib/api/contracts/v2/knowledge.ts Preserves base tag-filter refinements and makes the v2 filter element strict.
apps/sim/lib/knowledge/tags/filter-resolution.ts Validates operators and upper-bound values against resolved tag field types.
apps/sim/lib/api/contracts/v2/logs.ts Adds cost bounds and rejects inverted cost and duration windows.
apps/sim/lib/api/cursor-binding.ts Centralizes deterministic canonicalization for unordered array scopes.
apps/sim/lib/table/rows/cursor.ts Canonicalizes logically unordered predicate positions without flattening or changing order-sensitive operands.
apps/sim/lib/audit-logs/query.ts Uses the same resource-type set parser as cursor binding to keep query and cursor semantics aligned.
apps/sim/lib/table/rows/service.ts Prevalidates bulk-update patch values so invalid requests do not depend on whether rows match.
apps/sim/proxy.ts Applies the shared exposed-header list to matched CORS policies without changing their origin or credential rules.

Reviews (1): Last reviewed commit: "fix(v2): close the correctness gaps the ..." | Re-trigger Greptile

@waleedlatif1
waleedlatif1 merged commit 8d319a4 into staging Aug 13, 2026
24 checks passed
@waleedlatif1
waleedlatif1 deleted the fix/v2-release-audit-findings branch August 13, 2026 19:20
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