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 7 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. Removal was chosen over repair because the integration's auth flow was non-functional — it could not be connected at all. The rest of Bugbot's "High Risk" summary is accurate and is enumerated 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 audits and repairs knowledge-base connectors, focusing on safe deletion reconciliation, pagination, document hydration, content hashing, and provider API correctness. It also removes the non-functional Evernote integration and hardens connector credential handling.
Confidence Score: 5/5The PR appears safe to merge because the only previous Greptile finding is fully resolved and no blocking failure remains. No blocking failure remains.
|
| Filename | Overview |
|---|---|
| apps/sim/tools/jira/utils.ts | The prior ADF type-safety issue is fixed; both reviewed helpers now accept unknown values and narrow them safely without changing runtime behavior. |
| apps/sim/connectors/utils.ts | Shared connector utilities are updated as part of the fleet-wide reconciliation, hashing, and content-processing repairs. |
| apps/sim/lib/knowledge/documents/secure-fetch.server.ts | Sensitive Set-Cookie metadata is attached non-enumerably so development error formatting does not expose it. |
| apps/sim/connectors/sftp/sftp.ts | SFTP synchronization now requires host-key verification before credentials are used. |
| apps/sim/connectors/registry.server.ts | The non-functional Evernote connector is removed from the server runtime registry. |
Reviews (3): Last reviewed commit: "fix(connectors): act on the final valida..." | Re-trigger Greptile
8d8f343 to
0932597
Compare
0932597 to
7116c40
Compare
7116c40 to
35a979c
Compare
|
@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. |
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.
95d6fc9 to
299c5f2
Compare
A 403 from a key without incident_updates permission, or a 404, returns on every sync. Marking those incomplete appended :partial to a hash that then never matched the listing stub, so the incident re-hydrated forever without converging. Only a transient failure may mark content incomplete now, which matches how greenhouse already treats the same class.

Five review passes over all 61 knowledge-base connectors, validated against each provider's live documentation or machine-readable spec. Passes 3 through 5 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:startand readtotalSize; neither exists on/content/search.hasMorecould never be true, so it only ever returned the first page, and that truncated listing fed deletion reconciliation on every sync.labels.listwas swallowed, producing a query matching nothing. An empty page arriving with anextPageTokenalso ended the listing early, producing a short-but-complete-looking listing that reconciliation acted on. Separately, the labels selector stores label ids whilevalidateConfigcompared against label names, so any user label was rejected at save time and could never be configured at all (system labels, where id equals name, worked).listingCappedwas set nowhere, andmaxEventsdefaults to 500. Any calendar with more than 500 events in the configured window listed only the first ~500 and reconciliation hard-deleted the rest — on the default configuration, with nothing for the user to misconfigure.listingCappednowhere, so every stored document beyond the cap was hard-deleted on every run. wordpress additionally paged by pure offset, which re-numbers mid-run when a post is published, skipping posts.formatDateused local-time getters against an API documented in UTC, so on any non-UTC host the window boundary shifted by a day and that day's recordings could fall outside every window and never be listed. Separately, both bounds are inclusive but the span was computed asto - WINDOW_DAYS, producing 31-day windows that overlapped by a day at each seam — seam-day recordings were listed twice and consumed the budget twice.LIMITbounded the entire result set with nonextRecordsUrl, so any org larger than that limit had the remainder hard-deleted every sync.listingCappedwas set nowhere, so a repo truncated by the Trees API's own 100,000-entry / 7 MB limit, or by amaxFilescap, produced a short listing and reconciliation hard-deleted the stored documents outside it.documents.length < stubs.length, mutation-verified.maxMessagesproducedNaN, the loop never ran, and the empty listing hard-deleted the stored document.Other production faults repaired
API_VERSIONwas'v62.0'substituted into a{version}placeholder that already carries thev, yielding/services/data/vv62.0/. Every REST call 404'd.incompleteSearchwas read in code but never named in the Drivefieldsmask, so the partial-corpus guard was dead code.listingCappedwas set nowhere, so any source withmaxIssuesbelow its real issue count listed only the top N byupdated DESCand hard-deleted everything that fell out of that window, every sync. (Correcting an earlier draft of this description: the/search/jqlmigration is not in this diff — staging already used that endpoint. OnlyPAGE_SIZEchanged.)Cap-at-exhaustion, and the deletion wave it implies
The inverse error: setting
listingCappedwhen the cap lands exactly on source exhaustion permanently blocks reconciliation, so documents deleted upstream are stranded in the knowledge base forever. Fixed across gmail, ashby, azure-devops, grain, granola, greenhouse, incidentio and others.This has a visible consequence on the first sync. Those connectors have been over-flagging, so deletion reconciliation has been suppressed on them — possibly since setup. Once the gate is correct, the documents that were stranded become eligible for removal and will reconcile away. That is the fix working as intended, and it is two-phase (tombstone, then delete only on a second consecutive absence, behind the suspect-listing guard), but it will read as a deletion wave.
Two connectors were worse than over-flagging: hubspot and intercom never set
listingCappedat all, so every capped sync hard-deleted every stored document beyond the cap, on every run. Both now set it.Pagination
"Last-page precision" (shrinking page size to the remaining budget) is only valid when the cursor is position-independent. Over an opaque cursor token no provider documents that a cursor survives a changed page size, and the cap was already enforced server-side — so it bought nothing and risked a mid-sync iterator error. Reverted on airtable, asana, ashby and confluence.
To be precise about scope, since an earlier draft of this description overstated it: no taper was ever committed for greenhouse or intercom. Both were audited as page-number APIs (Harvest documents
pageas "the n-th chunk ofper_pageobjects", and Intercom's/articlesis the same shape), and both already held their page size constant on staging. The only change there is a comment recording why it must stay constant — there was nothing to revert.Regressions this audit introduced and fixed within it
Net zero versus staging. Listed because the record should be honest about which findings were pre-existing and which were our own — a later pass reviewed each earlier pass's edits, which is the only reason these did not ship.
.catch(() => null)on the JSON parse plusdata?.data ?? {}, turning an unparseable 2xx into a confident empty listing. Staging returneddata.databare and threw loudly, so this was never a production fault.matchesExtensionin a way that dropped dotfiles, and separately added autf-8blob branch justified by a misattributed quote. Both reverted; net versus staging is TSDoc only.:partialto a hash that then never converged.404 → breakin the newfetchParticipants; staging had no such function.startedenum member; staging omitted the parameter entirely.secure-fetch— retry headers attached enumerably, so the logger's own-enumerable copy wrote upstreamSet-Cookievalues into sync logs. Staging attached no headers at all.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.SFTP now requires a host-key fingerprint on all sources, failing closed before the credential is sent (verified by mutation: 7 tests go red). DocuSign's OAuth host is allowlisted to demo/production so a misconfigured value cannot redirect the flow and its bearer token to another origin.
Fabricated doc citations
Eight TSDoc comments quoted provider sentences that do not exist (github ×2, jira, jsm, linear, google-meet, dropbox, youtube) — two of them introduced by this audit. 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 that 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: the only surviving references are prose in agent-tooling markdown; both icon registries are de-registered. No knowledge-base source, no deployed workflow, and no executed workflow references it.
Customer impact
One-time re-index, deliberate.
externalIdis unchanged in all three, so these are in-place updates — no deletes, no re-creates, no re-auth:Other behaviour 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 rise in reported failures; this is the intended invariant, not a defect.MONDAY_API_VERSIONmoves four call sites (both selector routes, and webhook create/delete) from an inline2024-10pin to2026-04. To be precise:2024-10is in Maintenance, not deprecated, so it was still being served as declared — this is not a deprecation fix. What was genuinely wrong is the divergence: the selector routes listed boards at one schema version while the tools operating on those boards ran at another, inside a single user-visible flow. The consolidation is right, but it jumps roughly five version bumps at those four sites, and monday fails silently here (an unrecognized version resolves to Current rather than erroring). Worth a reviewer's eye rather than being read as a pure refactor.rtffrom the default extension list and excludes keys containing./..path segments. Both are intentional scope filters, so they correctly do not setlistingCappedand the affected documents reconcile away.Operational notes for the first post-deploy sync
Two things will look alarming in logs and are not incidents.
notion's re-index will not finish in one task window. The corpus must re-hydrate against a documented ~3 req/s average while the engine hydrates 5 documents concurrently (
SYNC_BATCH_SIZE = 5) against a 30-minute budget (maxDuration: 1800). Expect the run to be killed mid-sync with heavy 429 backoff first. 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 several scheduled syncs.jira's deletion reconciliation will run for the first time on mid-sized projects.
PAGE_SIZE50 → 100 raises the effective listing ceiling from 25,000 to 50,000 (MAX_PAGES = 500). A project in the 25,001–50,000 band previously always hit the ceiling, which setlistingTruncated— an unconditional, non-overridable block on reconciliation — so stale rows have accumulated since setup. Those projects now list completely and will tombstone that backlog on the first full sync. This is the correct outcome and it is two-phase behind the suspect-listing guard, but note the honest framing: raising the page size enables deletion for that band rather than fixing a deletion bug. A project larger than 50,000 truncates again and still fails safe.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 behaviour.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).The sweep also confirmed the
htmlToPlainTextblast radius independently: exactly microsoft-teams and mintlify both decode HTML and hash the result, verified by intersecting thehtmlToPlainTextimporters against thecomputeContentHashcallers.The corollary matters more than the re-index does: for the other ~22 connectors that call
htmlToPlainText, the hash is metadata-keyed, so the entity-decode fix is forward-only — already-indexed documents keep their literal’text until the upstream record's own timestamp changes. The same is true of the Jira ADF extraction improvements, since bothjiraandjsmhash on metadata. Correcting the existing corpus would need a deliberate rehydrate, not this deploy.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. Worth fixing now rather than later precisely because thehubspot:v2:bump rewrites every affected document once.The one item I'd want a decision on before merge
sentry— the endpoint migration narrows scope and will delete issues. Moving from the deprecated/projects/{org}/{project}/issues/to/organizations/{org}/issues/?project=is correct on its own terms, and it fixes a genuine bug alongside it (staging sorted on Sentry's defaultdate, a mutable key, so under cursor paging an issue whoselastSeenadvanced mid-sync jumped the cursor, was skipped, and was hard-deleted as absent).But the organization endpoint always resolves a date range, and with no
statsPeriod/start/endit defaults to roughly 90 days. Issues last seen before that window are absent from the listing and reconcile away. That is a scope narrowing this PR introduces, not one it inherits — the in-code comment currently rationalizes it as the pre-existing "aged out of the query window" semantic, which understates it.I did not set an explicit window because I could not verify Sentry's maximum accepted value, and a wrong one 400s the endpoint. There are no live Sentry sources, so nothing is at risk today. Options are: accept it as intended scope, pin an explicit widest-supported window, or set
listingCappedwhen the window is the limiting factor. Worth an owner's call rather than my guess.Known gaps
All 61 connectors have been through the final pass, verified against the four invariants — cap gating,
getDocumentabsence semantics,contentHashparity, and pagination shape.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. No false-positive path exists.Issues found and deliberately not fixed here, each with a reason:
outlookroutes a correctness-critical value betweenlistDocumentsandgetDocumentthrough an untypedsyncContextkey thatconnectors/types.tsdocuments as a cache. Correct today — the engine has exactly onegetDocumentcaller — but the fallback fails silently and permanently if that ever changes.listingCappedand skips the tombstone grace period, hard-deleting hidden documents in one pass. Correct semantics; deserves a UI warning.gongmaps a 404 to an empty page unconditionally, which can hard-delete a source over two consecutive syncs. The fix requires matching a documented error string that could not be verified — Gong's API reference is not publicly fetchable — and guessing at it would be worse than the known behaviour.rootlyswallows timeline-fetch failures into an empty array, baking a permanently timeline-less document (the hash isupdated_at-keyed, so it never self-repairs). Pagerduty solved this class with an explicit failure flag; rootly is the remaining instance.gitlabandnotionflaglistingCappedwhen a count merely reaches the cap without confirming more remained. Errs safe — over-suppression, never over-deletion — and pre-existing.servicenowsorts by a mutable key under offset pagination, so a record updated mid-sync can slide past the window and read as deleted. Sentry's equivalent was fixed here by switching to an immutable sort key.youtubebuildscontentHashfrom different fields on the stub and hydrated paths. Tracing the engine shows it settles todocsUnchangedrather than re-indexing, so the cost is a wasted API call per video per sync — quota burn, not churn.Verification
29 audits (
check:audits), 24/24 type-check, 1,332 tests, CodeQL clean, biome clean.