Skip to content

fix(connectors): validate and repair the knowledge-base connector fleet - #6757

Open
waleedlatif1 wants to merge 6 commits into
stagingfrom
chore/connector-validation-audit
Open

fix(connectors): validate and repair the knowledge-base connector fleet#6757
waleedlatif1 wants to merge 6 commits into
stagingfrom
chore/connector-validation-audit

Conversation

@waleedlatif1

@waleedlatif1 waleedlatif1 commented Aug 16, 2026

Copy link
Copy Markdown
Collaborator

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

shouldReconcileDeletions hard-deletes any stored document absent from a listing that did not set syncContext.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:

  • firefliesfirefliesGraphQL ended in return 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.
  • linear — same shape via (data.issues || {}) on a non-nullable IssueConnection!.
  • gmail — a failed labels.list was swallowed, producing a query matching nothing across every label-scoped source.
  • github — pass 2's lastDot <= 0 change 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.
  • sharepoint — the cap gate missed in-page truncation. Now documents.length < stubs.length, mutation-verified.

Cap-at-exhaustion

The inverse error: setting listingCapped when 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

SecureFetchHeaders declares private setCookies: string[], but TypeScript private is compile-time only — at runtime it is an own enumerable property. @sim/logger copies own enumerable properties into the formatted line and the retry loop logs { error } on every failed attempt, so newly-attached response headers wrote upstream Set-Cookie values 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 (externalId unchanged in all three, so these are in-place updates — no deletes, no re-creates, no re-auth):

Connector Sources Docs Why
notion 17 9,596 tables/toggles/callouts previously indexed empty
google-docs 10 1,675 tabbed documents previously unread
hubspot 1 211 raw HTML now stripped

Other behavior changes worth watching on first sync:

  • airtable — records with attachment/collaborator/barcode/button cells re-index once. This ends permanent churn: the old hash included 2-hour-expiring attachment URLs, so those records were re-indexing on every sync.
  • asana — archived projects are now excluded; tasks living only under archived projects reconcile away.
  • github — file ceiling rises 15,000 → 100,000. Repos previously hitting it had deletions permanently suppressed and will now reconcile normally. An add wave, not a delete wave. Worth watching peak stub memory on the largest source (22 sources / 102,995 docs).
  • google-drivegetDocument now 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.
  • confluence — previously-truncated label-filtered sources will index their full corpus once.
  • mondayMONDAY_API_VERSION extraction silently bumps two selector routes from an inline 2024-10 pin to 2026-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 shared htmlToPlainText entity-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 classifies unchanged next run. It should converge over roughly four to seven scheduled syncs.

jira's deletion reconciliation will run for the first time. PAGE_SIZE 50 → 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 set listingTruncated and 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 rtf from 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 set listingCapped and 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-connector pass 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:

  • ashby, azure-devopsgetDocument returned null on an API-shape fault. Both connectors set contentDeferred, so these paths were live: a fulfilled null records no failure and no log, and the document silently vanishes.
  • dropbox — 409 covers Dropbox's entire LookupError union, and restricted_content / locked both mean the file still exists. Only not_found is absence now.
  • docusignfetchFormValues swallowed every non-OK status, baking a permanently incomplete document (the hash is metadata-only, so the next sync sees it as unchanged forever).
  • typeformall 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 sends the widest documented set, which is strictly wider than staging's effective behavior.
  • github — a utf-8 blob branch was added on a misattributed quote. That sentence describes the encoding request 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 under vnd.github+json 403 rather than returning encoding: 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 htmlToPlainText blast radius independently: exactly microsoft-teams and mintlify both decode HTML and hash the result, verified by intersecting the 26 htmlToPlainText importers against the 7 computeContentHash callers.

hubspot additionally moved to a shared anchored HTML-detection helper. The loose pattern matched angle-bracketed prose (Reply from John <john@acme.com>), and htmlToPlainText deletes 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 the hubspot: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, getDocument absence semantics, contentHash parity, and pagination shape. All six have zero live production sources.

Findings from that pass:

  • typeform / zendesk getDocument returned null on missing required config (formId, subdomain). null reads as documented absence, so on an add the document is dropped with neither a failure counter nor a log. Unreachable today — both listDocuments paths throw on the same condition, aborting the sync first — but inconsistent with the invariant and live the moment getDocument gains a second caller. Fixed: both now throw.
  • Cap gating is correct in all six. typeform (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 own totalMatches — both are stronger than the fleet norm.
  • No page-number taper: zendesk follows next_page link URLs through an origin check, and the rest are cursor- or token-paginated.

One pre-existing issue found, deliberately not fixed here. youtube builds its contentHash at two call sites from different source fields — the stub uses playlistItems.contentDetails.videoPublishedAt, the hydrated document uses videos.list snippet.publishedAt. Because the stub sets contentDeferred: true, getDocument runs 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 .readonly variant by string suffix, which would be unsound if any provider had a foo.readonly scope where foo is not a superset. Checked and clear. Exactly three .readonly requiredScopes 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 of ediscovery.readonly. The other two cannot trigger it — bare meetings.space and forms.responses are not real scopes and appear in no provider list (Meet grants meetings.space.created, which is not a superset). No false-positive path exists.

Two structural issues found but deliberately not fixed here:

  1. outlook passes a conversation date from listDocuments to getDocument through an untyped syncContext key. Correct today — sync-engine.ts:978 is the only caller — but the fallback fails silently and permanently if that coupling breaks, re-indexing on every sync with no error. connectors/types.ts documents syncContext as a cache, not a correctness-critical handoff.
  2. "Full resync" is destructive on capped sources. fullSync overrides listingCapped and skips the tombstone grace period, hard-deleting hidden documents in one pass. Correct semantics, but it deserves a UI warning.

@vercel

vercel Bot commented Aug 16, 2026

Copy link
Copy Markdown

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

1 Skipped Deployment
Project Deployment Actions Updated (UTC)
docs Skipped Skipped Aug 16, 2026 3:31am

Request Review

@cursor

cursor Bot commented Aug 16, 2026

Copy link
Copy Markdown

PR Summary

High Risk
Wide connector sync and deletion-reconciliation behavior changes can cause one-time re-indexing, new failed sync rows, or reconciliation of previously capped sources; Evernote removal breaks any workflows still using that block.

Overview
This PR removes the Evernote integration end-to-end (workflow block, tool API routes, Thrift client, icons, and integration docs) and updates knowledge-base connector docs to 60 built-in connectors (Evernote and SFTP host-key fingerprint wording).

The bulk of the change is connector sync correctness around listings, caps, and hydration. Connectors now set listingCapped only when a cap or filter knowingly hides more data, so deletion reconciliation does not mass-tombstone documents on transient failures or truncated listings. getDocument paths rethrow non-404 errors (Asana, Ashby, Box, Azure DevOps files) so failed hydrations stay visible instead of looking like missing content. Pagination fixes keep page size constant when reusing opaque cursors (Airtable, Asana, Ashby) and parse CQL search cursors correctly in Confluence.

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 include-labels on single-item GETs; Box root-folder access failures and representation polling errors surfacing. DocuSign resolves userinfo via getDocusignOAuthUrl. Monday selector routes share MONDAY_API_URL / mondayHeaders.

Docs-only tweaks: Ashby Temporary employment type, minor MDX formatting, LogRocket output table spacing.

Reviewed by Cursor Bugbot for commit 6f37771. Configure here.

@cursor cursor 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.

Cursor Bugbot has reviewed your changes using high effort and found 2 potential issues.

Fix All in Cursor

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

Comment thread apps/sim/connectors/confluence/confluence.ts
Comment thread apps/sim/lib/auth/connectors/providers.ts
@waleedlatif1

Copy link
Copy Markdown
Collaborator Author

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: listingCapped gating in apps/sim/connectors/*/ (a listing that omits documents without setting it causes hard deletion), and getDocument returning null for anything other than documented absence.

@greptile-apps

greptile-apps Bot commented Aug 16, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

The PR comprehensively repairs knowledge-base connector listing, pagination, hydration, reconciliation, content extraction, and security behavior while removing the nonfunctional Evernote integration.

  • Makes connector failures reject rather than silently producing incomplete or empty results.
  • Corrects cap and pagination handling to preserve deletion reconciliation without skipping records.
  • Improves content extraction and hashing across multiple providers.
  • Prevents secure response headers from leaking through enumerable error properties and requires SFTP host-key verification.
  • Completes the previously requested Jira ADF helper typing fix.

Confidence Score: 5/5

The PR appears safe to merge because the previously reported Jira ADF typing issue is fixed and no blocking failure remains.

No blocking failure remains.

Important Files Changed

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

Comment thread apps/sim/tools/jira/utils.ts Outdated
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.
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.
@waleedlatif1

Copy link
Copy Markdown
Collaborator Author

@greptile-apps review — this exceeds the 100-file limit, so no automatic review is produced.

@cursor review

Both bots last reviewed 6f37771; the branch is now at 95d6fc9346 and the changes since have had no review. Your three earlier findings are all fixed and resolved (confluence opaque-cursor page size, monday API version split, jira ADF any).

What is new since then, and where scrutiny is most valuable:

getDocument returning null for something that is not documented absence. This is the defect class the whole PR is about: the engine hydrates under Promise.allSettled, and only a rejected promise increments docsFailed/failedExternalIds. A fulfilled null on an add hits neither branch, so the document silently disappears with no counter and no log. Fixed in ashby and azure-devops (both set contentDeferred, so those paths are live), dropbox (409 covers the whole LookupError union — restricted_content and locked mean the file still exists), and docusign (fetchFormValues swallowed every non-OK, baking a permanently incomplete document because the hash is metadata-only).

typeformall was sending response_type=started,partial,completed, but Typeform documents only partial and completed. Staging omitted the parameter entirely, so an unknown enum member risking a 400 was a regression introduced here. Now sends the widest documented set.

github — removed a utf-8 blob branch justified by a misattributed quote: that sentence describes the encoding request parameter of Create a blob, while the GET response is documented as always base64.

connectors/utils.ts — new exported looksLikeHtml, now used by hubspot and granola. Worth checking the anchoring: htmlToPlainText strips tags and collapses all whitespace, so a false positive deletes the bracketed span and flattens line structure rather than passing the value through.

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.

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