Skip to content

fix(v2): stop six responses reporting less than the layer beneath them knew - #7097

Merged
waleedlatif1 merged 3 commits into
stagingfrom
fix/v2-release-findings
Aug 26, 2026
Merged

fix(v2): stop six responses reporting less than the layer beneath them knew#7097
waleedlatif1 merged 3 commits into
stagingfrom
fix/v2-release-findings

Conversation

@waleedlatif1

Copy link
Copy Markdown
Collaborator

Summary

The remaining real findings from cubic's review of the v0.8.12 release PR (#7090), all on the v2 API surface. Verified each against the code before fixing; two needed no change.

Six of the seven share a shape worth naming: the layer underneath already knew, and the response threw it away.

  • includeFileBase64 silently did nothing without includeOutput. The contract has always said it requires it, and the read honours that — files are projected inside the includeOutput branch alone. Nothing enforced it, so the flag parsed, was accepted, and was then dropped: a 200 carrying no files and no reason why. Now a 400 naming the missing flag, matching how GET /billing/logs refuses a window bound its period will not read. base64MaxBytes rode on the same projection and is covered too.
  • POST /files/{fileId}/unzip answered 500 for a bad archive. ArchiveError had no arm in the v2 error policy, though the internal extract route beside it has mapped the same failures to 400/413 all along. statusForArchiveError stays the single place a reason is classified.
  • Both workflow-MCP lists truncated silently. The use cases were already returning truncated, and the copilot handler already publishes it — only the v2 presenters dropped it, while nextCursor: null told a reconciling caller the set was complete. The tools list now returns truncated; the servers list returns toolNamesTruncated, kept separate from "there is another page" so it does not fire on every page with a successor.
  • A filtered table dispatch was indistinguishable from an unfiltered one. rowIds: undefined documented itself as "every eligible row", but a select-all dispatch has no row list either — and POST on that same path accepts filter and excludeRowIds, so a caller could create a scope the resource then denied having. The compiled filter stays unpublished (it is stored in a different grammar from the predicate the request was written in, and a test had deliberately locked that in); selectAll and excludeRowIds name the distinction without reproducing it.
  • GET /files/uploads/{uploadId} always returned file: null, though the contract promises the registered file after finalization and completedFileId was sitting on the record. That endpoint exists for a caller who lost track of a transfer — the one caller who cannot get the id any other way.
  • HEAD on a run file 200'd for ids that do not exist. It resolved the file downstream of authorization, so it disagreed with the GET beside it. The lookup moved into context resolution, matching how readTableDispatch resolves its dispatch id.
  • v2 bulk table delete audited a display path as resourceId. It projected from the caller-keyed list, so FOLDER_DELETED recorded /Sales where DELETE /api/folders/[id] records the canonical id — two spellings of one resource that no query could join.

Needed no change

  • Cross-organization audit-log cursor replay. The scenario needs one caller authorized for two organizations. member carries uniqueIndex('member_user_id_unique') and resolveEnterpriseAuditAccess refuses any organization the caller is not a member of, so it cannot arise. Reasoning posted on the thread.
  • Both documentation findings were already fixed on staging by docs: correct two statements that contradict what they describe #7092, after cubic's review ran.

Type of Change

  • Bug fix

Testing

  • A test per fix, each verified to fail against the unfixed code. Several of these survived because the existing tests could not catch them: the unzip tests all raised OrchestrationError (which every policy already renders) rather than the ArchiveError the use case actually throws, and the HEAD test mocked the use case and made authorize reject — proving only that the route renders a rejection. The new HEAD coverage sits at the use-case level, where the real decision is made.
  • Full suites green: apps/sim 33,288 tests, sim-cli 607 tests.
  • bun run lint, all 33 audits (check:audits), and type-check in both workspaces pass. generate:openapi rejected the new response fields until their examples were updated, which is the gate working.

Checklist

  • Code follows project style guidelines
  • Self-reviewed my changes
  • Tests added/updated and passing
  • No new warnings introduced
  • I confirm that I have read and agree to the terms outlined in the Contributor License Agreement (CLA)

…m knew

Findings from cubic's review of the v0.8.12 release PR, all on the v2 surface.

- `GET /workflows/{id}/runs/{runId}` documented `includeFileBase64` as
  requiring `includeOutput`, and the read honours that — files are projected
  inside the `includeOutput` branch alone. Nothing enforced it, so the flag
  parsed, was accepted, and was then dropped: a 200 carrying no files and no
  reason why. Now a 400 naming the missing flag, matching how `GET
  /billing/logs` refuses a window bound its period will not read.
- `POST /files/{id}/unzip` rendered a malformed or over-cap archive as 500.
  `ArchiveError` had no arm in the v2 policy, though the internal extract route
  beside it has mapped the same failures to 400/413 all along.
- Both workflow-MCP lists cut their tool inventory at a ceiling and published
  `nextCursor: null` regardless. The use cases were already returning
  `truncated`; only the v2 presenters dropped it, while the copilot handler
  published it. A reconciling caller read a partial set as the complete one.
- A table dispatch scoped by filter reported neither the filter nor its
  exclusions, and `rowIds: undefined` documented itself as "every eligible
  row" — so a filtered run and an unfiltered one were indistinguishable. The
  compiled filter stays unpublished, as before; `selectAll` and
  `excludeRowIds` name the distinction.
- `GET /files/uploads/{id}` promised the registered file after finalization and
  passed null unconditionally, so a caller polling a transfer it lost track of
  could watch a session reach `completed` and never learn what it created.
- `HEAD` on a run file resolved the file downstream of authorization, so it
  answered 200 for an id the `GET` beside it would 404.
- v2 bulk table delete audited `FOLDER_DELETED.resourceId` from the
  caller-keyed projection, writing a display path where the single-folder
  delete writes the canonical id.

Two further findings needed no change: the audit-log cursor scope is bound by
`member_user_id_unique` and a membership check, so the cross-organization
replay it described cannot arise; and both documentation findings had already
been fixed on staging by #7092.
@vercel

vercel Bot commented Aug 26, 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 26, 2026 3:55am

Request Review

@greptile-apps

greptile-apps Bot commented Aug 26, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

The PR corrects several v2 API responses that previously discarded information already available from underlying application layers.

  • Preserves table-dispatch narrowing through separate filtered and excludeRowIds response fields.
  • Exposes MCP inventory truncation and finalized upload-file information.
  • Aligns archive errors and run-file existence checks with their documented HTTP behavior.
  • Uses canonical folder identifiers for bulk-delete audit records and validates dependent workflow-run query flags.

Confidence Score: 5/5

The PR appears safe to merge.

No blocking failure remains.

Important Files Changed

Filename Overview
apps/sim/app/api/v2/tables/presenters.ts Publishes filtered and exclusion-based dispatch narrowing while preserving the distinction from unrestricted and explicit-row runs.
apps/sim/lib/api/contracts/v2/tables.ts Extends the public dispatch-scope contract with optional filtering and exclusion metadata.
apps/sim/lib/uploads/upload-session/application.ts Resolves the registered file for completed upload sessions after reauthorization.
apps/sim/lib/workflows/application/download-workflow-run-file.ts Resolves the addressed run file during context loading so HEAD and GET agree on resource existence.
apps/sim/lib/workspace-files/api/route-policies.ts Maps malformed and over-limit archive failures to actionable client error responses.
apps/sim/lib/table/application/bulk.ts Separates caller-facing path projection from canonical identifiers used by deletion audits.

Reviews (2): Last reviewed commit: "fix(v2): let a failed file read fail, an..." | Re-trigger Greptile

Comment thread apps/sim/app/api/v2/tables/presenters.ts Outdated

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All reported issues were addressed across 26 files

Reply with feedback, questions, or to request a fix.

Re-trigger cubic

Comment thread apps/sim/app/api/v2/tables/presenters.ts Outdated
Comment thread apps/sim/lib/api/contracts/v2/workflow-mcp-servers.ts Outdated
Comment thread apps/sim/lib/uploads/upload-session/application.ts Outdated
Review follow-up. `selectAll` was set from the stored filter alone, but the
run rejects only `rowIds` *with* `excludeRowIds` — so an exclusion set with no
filter is a scope a caller can create, and the walk applies it. Those
dispatches published exclusions with no discriminator beside them, which is
the shape the flag existed to rule out.

One flag could not cover both: an exclusion-only scope is neither filtered nor
unnarrowed. So the two narrowings are reported as what they are — `filtered`
for the unpublished stored predicate, `excludeRowIds` for the deselections —
and every combination is now distinguishable from a run over every eligible
row. `excludeRowIds` mirrors the walk's own condition and is withheld beside
`rowIds`, where the dispatcher would ignore it.
… check

Two review follow-ups.

`getWorkspaceFile` logs and returns null on a read failure unless told
otherwise, so a transient database error would have reported a finalized
upload as fileless — to the one caller polling to learn what it created, who
would then stop, having been told there was nothing. Read with `throwOnError`
so the failure surfaces and the poll can retry, matching how the sibling
record read loads the same row. A file genuinely deleted still answers null,
which is a different question with a different answer.

The server list pointed callers at `/tools` for an authoritative inventory
without saying that endpoint applies the same ceiling. It now names the
`truncated` flag this PR added there, so "authoritative" has a condition
attached instead of being asserted.
@waleedlatif1

Copy link
Copy Markdown
Collaborator Author

@greptile

@waleedlatif1
waleedlatif1 merged commit 3645857 into staging Aug 26, 2026
29 checks passed
@waleedlatif1
waleedlatif1 deleted the fix/v2-release-findings branch August 26, 2026 04:03
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