fix(api): withhold internal failure messages from internal route responses - #7015
Conversation
…onses An orchestration result carrying `errorCode: 'internal'` holds whatever text the fault happened to have — `workflow-lifecycle.ts` catch-alls return `toError(error).message`, which is the driver's failed SQL. Three application helpers projected that straight into an `OrchestrationError`, and the internal route policy rendered its message into a 500 body, so raw SQL reached clients. The v2 envelope already scrubbed the same failures; internal routes did not. `messageForOrchestrationError` already encoded the rule and two sites honored it. The three that hand-rolled it disagreed, and `workflow-vfs` disagreed with itself: it defaulted the code with `?? 'internal'` but compared the raw `errorCode` against `'internal'`, so an uncoded failure was classified internal and still rendered its own message. Pair the two in `throwOrchestrationFailure` so a code and its message cannot disagree, and scrub at the internal route boundary as well, matching v2 — no call site authors a curated `internal` message, so nothing legitimate is masked, and site N+1 cannot reopen this by forgetting the rule.
|
The latest updates on your projects. Learn more about Vercel for GitHub. |
PR SummaryMedium Risk Overview Adds
Reviewed by Cursor Bugbot for commit 017abbf. Configure here. |
Greptile SummaryThe PR prevents internal and unclassified orchestration failures from exposing underlying infrastructure messages while preserving classified client-facing errors and their status mappings.
Confidence Score: 5/5The PR appears safe to merge, with internal failure details consistently withheld and classified client-facing errors preserved. The shared helper and route boundary apply the intended sanitization without changing status mappings, and the reachable orchestration producers preserve actionable messages for non-internal classifications.
|
| Filename | Overview |
|---|---|
| apps/sim/lib/api/server/routes/internal-json-route.ts | Sanitizes internal orchestration messages at the route boundary while preserving existing status mapping and classified messages. |
| apps/sim/lib/core/orchestration/types.ts | Adds a centralized throwing helper that keeps orchestration error classification and message policy consistent. |
| apps/sim/lib/core/orchestration/types.test.ts | Covers message withholding, classified-message preservation, missing-message fallback behavior, and helper output. |
| apps/sim/lib/workflows/application/transition-result.ts | Delegates failed workflow transition conversion to the shared orchestration failure helper. |
| apps/sim/lib/workflows/application/transition-result.test.ts | Verifies successful transitions, internal-message scrubbing, and preservation of classified failure status metadata. |
| apps/sim/lib/workflows/application/workflow-folders.ts | Replaces hand-rolled folder mutation error conversion with the centralized policy. |
| apps/sim/lib/workflows/application/workflow-vfs.ts | Fixes uncoded folder failures that previously received an internal code while retaining their underlying message. |
| apps/sim/lib/knowledge/application/folders.ts | Prevents uncoded knowledge-folder failures from propagating raw underlying error text. |
Flowchart
%%{init: {'theme': 'neutral'}}%%
flowchart TD
A[Orchestration result] --> B{Success?}
B -->|Yes| C[Continue]
B -->|No| D[throwOrchestrationFailure]
D --> E{Internal or uncoded?}
E -->|Yes| F[Use operation fallback]
E -->|No| G[Preserve classified message]
F --> H[OrchestrationError]
G --> H
H --> I[Internal route policy]
I --> J{Internal code?}
J -->|Yes| K[Return generic 500 message]
J -->|No| L[Return classified message and mapped status]
Reviews (1): Last reviewed commit: "fix(api): withhold internal failure mess..." | Re-trigger Greptile
…7047) `check-utils-enforcement.ts` scanned line by line, and every idiom it bans is a multi-token expression the formatter wraps at 100 columns. So it printed `✓ No banned patterns found` while eleven files carried the wrapped form of e instanceof Error ? e.message : fallback which CLAUDE.md mandates `getErrorMessage` for. The same class as the two blind spots already fixed in check-react-query-patterns. Patterns now run against the whole file, with match offsets mapped back to line numbers by binary search over the line-start table — verified against every offset of a multi-line fixture. Eight of the eleven are now `getErrorMessage(error, fallback)`. `auto-layout-utils` collapses a redundant `instanceof ApiClientError` arm on the way, since that class extends `Error`; `upgrade.ts` keeps its `rawBody ?? message` arm, which the helper cannot express, and only its tail collapses. The other three stay, because the helper genuinely does not fit, and they carry a `// utils-lint-allow: <reason>` annotation — the same escape hatch check-react-query-patterns already has, which this gate lacked: - the two auth routes return the message to an unauthenticated caller, so a non-Error throw must surface the fixed copy rather than its own text. `getErrorMessage` passes a thrown string straight through, which is the disclosure shape #7015 closed. - `e2b.ts` probes E2B's own error shape — a record-like carrying `message` or `value` — which has no equivalent. An annotation with no reason does not suppress, so the hatch cannot be used to silence a finding without saying why. Also corrects the header, which claimed Biome's `noRestrictedImports` covers "crypto named imports". It lists only `nanoid` and `uuid`. Named crypto imports pass both gates deliberately — server code building cipher IVs wants node's crypto, not the cross-context wrapper — and the comment asserting otherwise would mislead the next person auditing this. Verified the gate can fail in both directions: reintroducing a wrapped ternary reports it, and emptying an annotation's reason reports it too.
What
An orchestration result carrying
errorCode: 'internal'holds whatever text the fault happened to have. Theworkflow-lifecycle.tscatch-alls (:366,:514,:636) returntoError(error).message, which for a failed transaction is the driver's SQL:Three application helpers projected that straight into an
OrchestrationError, andinternalOrchestrationErrorPolicyrenderedclassified.messageinto a 500 body — so raw SQL reached clients. The v2 envelope already scrubbed the same failures (app/api/v2/lib/response.ts:481); internal routes did not.Why it drifted
messageForOrchestrationErroralready encoded the rule, and its TSDoc states it plainly: an unclassified failure "carries whatever text the fault happened to have — a driver's failed SQL, say — so the caller gets the route's own generic wording instead."Two sites honored it. Three hand-rolled it and disagreed.
workflow-vfs.tsdisagreed with itself:An uncoded failure was therefore classified
internal(a 500) while still rendering its own message.The fix
Two seams, deliberately both:
throwOrchestrationFailure(lib/core/orchestration/types.ts) pairs the code with the message the policy permits for it, so the two cannot disagree. All four hand-rolled helpers collapse onto it. This is the semantic seam — it still knows which text was caller-written.The boundary guard has no false-positive cost:
new OrchestrationError('internal', …)has zero matches repo-wide, so no call site authors a curated internal message that could be masked.Behavior change
internal-coded failures on internal routes now return the route's fallback wording instead of the underlying message. Status codes are unchanged. Twoworkflow-vfs/workflow-foldersfallback strings collapse to one each; no test pinned them.Testing
messageForOrchestrationErrorandthrowOrchestrationFailurenow have direct tests (previously untested despite 8 call sites).transition-result.tsreproduces the leak, with the rawduplicate key value violates unique constrainttext in the assertion output.bun run type-checkclean;bun run check:api-validationpasses.app/api; 585 across the touchedlib/areas.Provenance
Found by running mattpocock/skills
code-review(Fowler smell baseline) over the codebase. Six sibling findings from the same sweep were refuted under adversarial verification and deliberately left out — this was the one that survived.