fix(connectors): validate and repair the knowledge-base connector fleet - #6757
fix(connectors): validate and repair the knowledge-base connector fleet#6757waleedlatif1 wants to merge 6 commits into
Conversation
|
The latest updates on your projects. Learn more about Vercel for GitHub. |
PR SummaryHigh Risk Overview The bulk of the change is connector sync correctness around listings, caps, and hydration. Connectors now set Per-connector behavior changes include: Airtable stable cell hashing and table-ID deep links; Asana batched project walks with a per-call request budget; Ashby feedback select-label rendering and tighter cap flags; Azure DevOps wiki/repo filter staleness and file metadata response shapes; Confluence labels from Docs-only tweaks: Ashby Reviewed by Cursor Bugbot for commit 6f37771. Configure here. |
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes using high effort and found 2 potential issues.
❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.
Reviewed by Cursor Bugbot for commit 6f37771. Configure here.
503f2a1 to
8d8f343
Compare
|
Addressing Bugbot's one substantive point: Evernote removal does not break any live workflow. No deployed workflow references the Evernote block, and no workflow containing it has ever been executed. The only reference is an undeployed scratch workflow with a zero run count. Combined with zero Evernote knowledge-base sources, removal is inert for users — which is why it was chosen over repair (the integration's auth flow was non-functional). The rest of Bugbot's "High Risk" summary is accurate and is enumerated with per-connector source/document counts under Customer impact in the description. Note the bot reviewed the pre-rebase commit; the branch has since been rebased onto current staging (conflicts were confined to the three generated tool artifacts, resolved by regeneration and verified idempotent). @greptile-apps review — this exceeded the 100-file limit, so no review was produced. Highest-value areas: |
Greptile SummaryThe PR comprehensively repairs knowledge-base connector listing, pagination, hydration, reconciliation, content extraction, and security behavior while removing the nonfunctional Evernote integration.
Confidence Score: 5/5The PR appears safe to merge because the previously reported Jira ADF typing issue is fixed and no blocking failure remains. No blocking failure remains.
|
| Filename | Overview |
|---|---|
| apps/sim/tools/jira/utils.ts | The previously reported helper-level ADF typing issue is fixed; both helpers now accept unknown and narrow safely before property access. |
| apps/sim/tools/jira/utils.test.ts | Adds broad coverage for ADF block structure, lists, inline nodes, and malformed input. |
| apps/sim/connectors/utils.ts | Expands shared connector content and pagination utilities used by the repaired connector fleet. |
| apps/sim/lib/knowledge/documents/secure-fetch.server.ts | Changes sensitive response-header attachment to avoid enumerable error metadata. |
| apps/sim/connectors/sftp/sftp.ts | Adds mandatory host-key fingerprint validation for SFTP connections. |
Reviews (2): Last reviewed commit: "fix(connectors): act on the final valida..." | Re-trigger Greptile
8d8f343 to
0932597
Compare
0932597 to
7116c40
Compare
Audits every KB connector against its provider's live API documentation and
fixes what the audit found. The dominant defect class is deletion
reconciliation: the sync engine hard-deletes any stored document absent from a
"full" listing, and most connectors had a path where a truncated or errored
listing failed to set `syncContext.listingCapped`.
Highest-impact fixes:
- linear: `getDocument` was dead code. The query declared `$id: ID!` where the
schema is `issue(id: String!)`, so every call failed variable validation.
- salesforce: `v62.0` was substituted into a `{version}` template that already
contains the `v`, so every REST call 404'd. SOQL `LIMIT` was also used as a
page size, silently capping every sync at 200 records.
- notion: only the first level of blocks was fetched, so tables indexed empty.
- microsoft-teams: `/messages` returns messages without replies, so no threaded
content was ever indexed.
- gmail: an empty page discarded `nextPageToken`, which reads as a complete
empty listing and hard-deletes every stored thread.
- confluence: the CQL path paginated with `start` and `totalSize`, neither of
which exists on that endpoint, so label-filtered syncs stopped after one page.
- box: `supportsRefreshTokenRotation` was unset, so Box's rotated refresh token
was discarded and every credential died on its second refresh.
Removes the Evernote integration entirely: the classic EDAM API is deprecated,
its sandbox is decommissioned, and developer tokens are no longer obtainable.
Makes SFTP host-key verification mandatory, and adds an attendee-PII opt-out to
google-calendar and google-meet (default on, so existing sources are unchanged).
Bumps the contentHash namespace for notion, google-docs and hubspot so existing
documents re-hydrate once and actually receive the content fixes above.
Ship-gate pass over the connector audit. Every finding was re-verified
against the provider's live documentation or machine-readable spec before
being acted on; several pass-3 edits were reverted rather than extended.
Correctness fixes:
- fireflies: a 2xx with an unparseable body returned an empty listing
instead of throwing. fireflies runs a full sync every time, so a fault
persisting across two syncs would have tombstoned all indexed docs.
- linear: same shape via `data.issues || {}` on a non-nullable connection.
- greenhouse: a 403 from a key without scorecard permission was treated as
transient, appending `:partial` to the hash. That never matches the list
stub, forcing full re-hydration of every candidate on every sync forever.
- google-meet: `fetchParticipants` carried a 404 swallow copied from its
transcript siblings, freezing every speaker as "Unknown".
- airtable, asana, ashby: reverted page-size tapers applied over opaque
cursor tokens. The cap was already enforced server-side.
- google-docs: response byte cap resolved to 800MB and could never fire.
- google-forms, google-vault, notion, sharepoint, dropbox: `getDocument`
now throws on transient failure instead of returning null, which the
engine reads as absence.
Security:
- Retry headers are attached non-enumerably. TypeScript `private` is
compile-time only, so `SecureFetchHeaders.setCookies` was an own
enumerable property that the logger serialized into sync logs.
Docs and dead code:
- Corrected six fabricated doc citations (github, jira, jsm, linear,
google-meet, dropbox) and removed the Evernote integration entirely.
…ead of returning null A null from getDocument reads as documented absence, so on an add the document is dropped with neither a failure counter nor a log. Both listDocuments paths already throw on the same missing config.
… API version The CQL search endpoint paginates by opaque cursor, and Atlassian does not document that a cursor issued against one limit survives a request asking for a different one. Narrowing limit to the remaining budget was the same pattern reverted on airtable, asana, and ashby. The page size is now constant and the cap is applied by trimming the returned page, which keeps the cap exact without varying the request. Monday OAuth getUserInfo hardcoded API-Version 2024-10 while every other monday surface reads MONDAY_API_VERSION, defeating the single-source pin.
7116c40 to
35a979c
Compare
Findings from a read-only /validate-connector pass over all 59 changed connectors, verified against provider specs before acting. Silent-drop fixes (a fulfilled null from getDocument records no failure and no log, so the document vanishes): - ashby: candidate.info returning success with an unusable payload. Ashby sets contentDeferred, so this path is live. - azure-devops: an unresolvable branch, likewise live. - dropbox: 409 covers the whole LookupError union, and restricted_content and locked both mean the file still exists. Only not_found is absence. - docusign: fetchFormValues swallowed every non-OK status, baking a permanently incomplete document since the hash is metadata-only. typeform: 'all' sent response_type=started,partial,completed, but Typeform documents only partial and completed. An unknown enum member risks a 400 that fails the whole sync, and staging omitted the parameter entirely, so this shipped as a regression. Now requests the widest documented set. github: removes a utf-8 blob branch justified by a misattributed quote — that sentence describes the encoding REQUEST parameter of Create a blob; the GET response is documented as always base64. Also corrects two comments that hid a real drop: >1 MB files under vnd.github+json 403 rather than returning encoding: none. hubspot: routes HTML detection through a shared anchored helper. The loose pattern matched angle-bracketed prose such as an email address, and htmlToPlainText deletes the span and collapses line structure. This matters now because the hubspot:v2: bump rewrites every live document once. youtube: drops an invented channel-ID format quote.
|
@greptile-apps review — this exceeds the 100-file limit, so no automatic review is produced. @cursor review Both bots last reviewed What is new since then, and where scrutiny is most valuable:
Full context, including the two operational notes for the first post-deploy sync (notion exceeding the 30-minute task budget, and jira's deletion reconciliation running for the first time), is in the description. |

Four review passes over all 61 knowledge-base connectors, validated against each provider's live documentation or machine-readable spec. Passes 3 and 4 reviewed the previous pass's edits, which is where most of what follows was caught — including regressions the audit itself introduced.
The dominant bug class
shouldReconcileDeletionshard-deletes any stored document absent from a listing that did not setsyncContext.listingCapped. So any path that turns a failure into an empty or short listing is a silent mass-delete. Most fixes here are instances of that one shape:firefliesGraphQLended inreturn data?.data ?? {}after a.catch(() => null)parse, so a 2xx with an unparseable body (gateway HTML, truncation) became a confident empty listing with no error and no flag. fireflies declares no incremental sync, so every run is a full sync: a fault persisting across two consecutive syncs would have tombstoned all 1,469 live documents. Now throws, with regression tests confirmed to fail on revert.(data.issues || {})on a non-nullableIssueConnection!.labels.listwas swallowed, producing a query matching nothing across every label-scoped source.lastDot <= 0change dropped dotfiles from listings. The revert is differential-tested at 15 paths × 8 configs (120 combos, 0 differences vs staging); the same harness reproduces the regression at 4 combos.documents.length < stubs.length, mutation-verified.Cap-at-exhaustion
The inverse error: setting
listingCappedwhen the cap lands exactly on source exhaustion permanently blocks reconciliation, so deleted documents are never cleaned up. Fixed across gmail, ashby, azure-devops, and others.Pagination
"Last-page precision" (shrinking page size to the remaining budget) is only valid for cursor/offset APIs. On page-number APIs it duplicates and permanently skips records. Reverted on greenhouse (Harvest) and intercom (
/articles), both confirmed page-numbered against their specs. Also reverted on airtable, asana, and ashby, where the page size varied across requests reusing an opaque cursor token — undocumented whether the token survives a changed page size, and the cap was already enforced server-side.Security
SecureFetchHeadersdeclaresprivate setCookies: string[], but TypeScriptprivateis compile-time only — at runtime it is an own enumerable property.@sim/loggercopies own enumerable properties into the formatted line and the retry loop logs{ error }on every failed attempt, so newly-attached response headers wrote upstreamSet-Cookievalues into connector-sync logs. Now attached non-enumerably. Scope is the dev/colorize branch only — production reduces errors to.message— so this is a local exposure, not a prod incident.SFTP now requires a host-key fingerprint on all sources (fails closed, verified by mutation: 7 tests go red). Zero live SFTP sources, so no migration.
Fabricated doc citations
Six TSDoc comments quoted provider sentences that do not exist (github, jira, jsm, linear, google-meet, dropbox). One inverted the usual failure mode: google-meet's code was correct and the comment invented a sentence to justify it, which nearly got a correct fallback reverted. Worth noting two of three "unverifiable" flags carried into the final pass turned out to be fully documented — re-checking beat reverting on suspicion.
Removed
Evernote, entirely — its auth flow was non-functional, so this is removal rather than repair. Verified complete: 6 surviving references, all prose in agent-tooling markdown, zero in shipped code; both icon registries de-registered.
No user impact. Zero Evernote knowledge-base sources exist, no deployed workflow references the Evernote block, and no workflow containing it has ever been executed.
Customer impact
One-time re-index, deliberate (
externalIdunchanged in all three, so these are in-place updates — no deletes, no re-creates, no re-auth):Other behavior changes worth watching on first sync:
getDocumentnow throws instead of returning null, so persistent content-read failures become visible failed rows. Expect a one-time bump in reported failures across 39 sources; this is the intended invariant, not a defect.MONDAY_API_VERSIONextraction silently bumps two selector routes from an inline2024-10pin to2026-04. Almost certainly a fix (the old pin was deprecated and resolving to Maintenance anyway; the affected queries are trivial), but it is a behavior change riding in what reads as a pure refactor.Verified against production rather than assumed: the linear page-size clamp has no live exposure — only one of six sources sets
maxIssues, at 200, a multiple of the 50-item page size, so the old and new code fetch identically. microsoft-teams and mintlify have zero sources, which is what makes the sharedhtmlToPlainTextentity-decoding change free.Operational notes for the first post-deploy sync
Two things will look alarming in logs and are not incidents. Both were measured against the code, not estimated.
notion's re-index will not finish in one task window. 9,596 documents must re-hydrate against a documented ~3 req/s average. The engine hydrates 5 documents concurrently (
SYNC_BATCH_SIZE = 5) and the task budget is 30 minutes (maxDuration: 1800), so expect the run to be killed mid-sync, with heavy 429 backoff before that. It is not destructive and needs no intervention: batches commit as they go, a page that exhausts its retries rejects and is therefore excluded from deletion reconciliation, and a re-hydrated page classifiesunchangednext run. It should converge over roughly four to seven scheduled syncs.jira's deletion reconciliation will run for the first time.
PAGE_SIZE50 → 100 raises the effective listing ceiling from 25,000 to 50,000 (MAX_PAGES = 500). At ~49,931 live documents the source previously always hit the ceiling, which setlistingTruncatedand unconditionally blocked reconciliation — so stale rows have been accumulating since setup. It now fits under the ceiling, and the first full sync may tombstone a large batch of issues genuinely deleted in Jira. That is the intended correction and it is two-phase (tombstone, then delete only on a second consecutive absence, behind the suspect-listing guard), but it is worth an eyes-on rather than a surprise. Note the margin is thin: if that source grows past 50,000 it truncates again and fails safe.Smaller one-time re-indexes, all deliberate: reddit (hash now keyed on edit revision + comment count, so edits are finally detected — the old hash was immutable), microsoft-teams (content-derived hash, and content now includes threaded replies), and outlook (conversations whose listing-derived and recomputed dates had diverged).
Two deliberate scope narrowings that delete already-indexed rows: s3 drops
rtffrom the default extension list (the indexed "text" was RTF control words) and excludes keys containing./..path segments. Both are intentional filters, so they correctly do not setlistingCappedand the affected documents reconcile away.Verification
27/27 audits (
check:audits), 24/24 type-check, biome clean, 1315 tests passing.Final validation sweep
A last read-only
/validate-connectorpass ran over all 59 changed connectors plus the shared files, verifying every quoted claim against provider specs. It found six real defects, all now fixed:getDocumentreturnednullon an API-shape fault. Both connectors setcontentDeferred, so these paths were live: a fulfillednullrecords no failure and no log, and the document silently vanishes.LookupErrorunion, andrestricted_content/lockedboth mean the file still exists. Onlynot_foundis absence now.fetchFormValuesswallowed every non-OK status, baking a permanently incomplete document (the hash is metadata-only, so the next sync sees it asunchangedforever).allsentresponse_type=started,partial,completed, but Typeform documents onlypartialandcompleted. An unknown enum member risks a 400 that fails the whole sync, and staging omitted the parameter entirely — so this shipped as a regression. Now sends the widest documented set, which is strictly wider than staging's effective behavior.utf-8blob branch was added on a misattributed quote. That sentence describes theencodingrequest parameter of Create a blob; the GET response is documented as always base64. Branch removed, and two comments corrected that were hiding a real drop (>1 MB files undervnd.github+json403 rather than returningencoding: none).That makes eight fabricated citations found across the audit, two of them introduced by the audit itself. Every one is now either corrected or removed.
The sweep also confirmed the
htmlToPlainTextblast radius independently: exactly microsoft-teams and mintlify both decode HTML and hash the result, verified by intersecting the 26htmlToPlainTextimporters against the 7computeContentHashcallers.hubspot additionally moved to a shared anchored HTML-detection helper. The loose pattern matched angle-bracketed prose (
Reply from John <john@acme.com>), andhtmlToPlainTextdeletes the span and collapses line structure — so a false positive loses data rather than passing it through. This was worth fixing now rather than later precisely because thehubspot:v2:bump rewrites every live document once.Known gaps
All 61 connectors have now been through the final pass. The last six (s3, typeform, x, youtube, zendesk, zoho-desk) were reviewed against the four invariants — cap gating,
getDocumentabsence semantics,contentHashparity, and pagination shape. All six have zero live production sources.Findings from that pass:
getDocumentreturnednullon missing required config (formId,subdomain).nullreads as documented absence, so on anaddthe document is dropped with neither a failure counter nor a log. Unreachable today — bothlistDocumentspaths throw on the same condition, aborting the sync first — but inconsistent with the invariant and live the momentgetDocumentgains a second caller. Fixed: both now throw.hitLimit && (slicedSome || sourceHasMore)), x (slicedByCap || nextToken || moreUsernames), youtube (hitMax && (trimmedByCap > 0 || nextPageToken)) and s3 (hitLimit && moreAvailable) all cover in-page truncation without firing at cap-at-exhaustion. zoho-desk uses an explicit probe-for-more, and zendesk additionally cross-checks the API's owntotalMatches— both are stronger than the fleet norm.next_pagelink URLs through an origin check, and the rest are cursor- or token-paginated.One pre-existing issue found, deliberately not fixed here.
youtubebuilds itscontentHashat two call sites from different source fields — the stub usesplaylistItems.contentDetails.videoPublishedAt, the hydrated document usesvideos.listsnippet.publishedAt. Because the stub setscontentDeferred: true,getDocumentruns for every video, so any divergence between those two fields would make the hydrated hash never match the stub hash and re-index every video on every sync — the same non-converging churn fixed in greenhouse here. It is unchanged from staging (not a regression from this PR), has zero live sources, and confirming whether the two fields actually diverge needs a live API call, so it belongs in its own change rather than an unreviewed edit at a ship gate.isScopeSatisfiedBy(lib/oauth/utils.ts) was the one shared-OAuth item flagged as needing scrutiny — it treats a granted non-readonly scope as satisfying its.readonlyvariant by string suffix, which would be unsound if any provider had afoo.readonlyscope wherefoois not a superset. Checked and clear. Exactly three.readonlyrequiredScopes exist fleet-wide, all Google, and all three appear verbatim in their provider scope lists, so exact match already satisfies them. The suffix rule fires for only one:ediscovery(bare) is grantable and is a genuine documented superset ofediscovery.readonly. The other two cannot trigger it — baremeetings.spaceandforms.responsesare not real scopes and appear in no provider list (Meet grantsmeetings.space.created, which is not a superset). No false-positive path exists.Two structural issues found but deliberately not fixed here:
outlookpasses a conversation date fromlistDocumentstogetDocumentthrough an untypedsyncContextkey. Correct today —sync-engine.ts:978is the only caller — but the fallback fails silently and permanently if that coupling breaks, re-indexing on every sync with no error.connectors/types.tsdocumentssyncContextas a cache, not a correctness-critical handoff.fullSyncoverrideslistingCappedand skips the tombstone grace period, hard-deleting hidden documents in one pass. Correct semantics, but it deserves a UI warning.