CedarCopilot wants to merge 5 commits into staging from fix/sherlock-crm-link-owner-scope-20260827-180115
Live on prod, no production signal yet
Strict CloudWatch fallback found 6 recent prod failure log lines for [UpdateExternalCrmWorkflow] Driver update failed (fetch failed), but the same failure had 7 log lines in the comparable pre-deploy baseline. Libra is not blaming this PR intent without a post-deploy increase.
Libra has verdicts on 0 of 1 tracked behaviors on prod; 1 is still being checked. Libra checks hourly for 3 days after each deploy.
Cedar's periodic pull has never compared owner ids. getAllDeals sends the caller's email addresses plus include_unowned to Superglue's salesforce-get-deals, which resolves them to a Salesforce User Id and constrains by OwnerId. All four drivers do exactly this.
The link path called a different Superglue tool , crm-search-deals-by-keyword , which accepted no owner parameters at all:
superglueCrmSearchDealsByKeyword({ provider, keyword, access_token, instance_url });
So it returned every opportunity the OAuth token could read. For Listen Labs that is 4,750 opportunities; Willem can read the whole org and edit only his own. Cedar linked conversations to colleagues' records, and every field push after that failed:
HTTP 400 [{"message":"insufficient access rights on object id",
"errorCode":"INSUFFICIENT_ACCESS_OR_READONLY"}]
35 of Willem's 305 linked Salesforce deals were owned by nine other reps. UserRecordAccess for opportunity 006hm000000GKmpAAG (NRG, owned by Sean Hoag) returns HasEditAccess: false, MaxAccessLevel: "Read"; every deal Willem owns returns "All" and pushed fine in the same window.
The capability was missing from the search tool, so it was added there rather than reimplemented in Cedar.
Superglue crm-search-deals-by-keyword now accepts sf_user_emails and include_unowned, with the same names, semantics and User-lookup step as salesforce-get-deals:
fetchSalesforceUser step , SELECT Id FROM User WHERE Email IN (...)searchSalesforceDeals gains AND OwnerId IN (...)A OR B AND owner parses as A OR (B AND owner) and every Name match would have leaked through unscoped.Both inputs are optional. Omitted, the tool behaves exactly as before, so the HubSpot and Attio branches and any other caller are unaffected.
Cedar just passes them through, the way getAllDeals already does:
const includeUnowned = getFetchUnownedDeals(crmRow.metadata);
const hits = await driver.searchDealsByKeyword(userId, term, { includeUnowned });
fetchUnownedDeals on the connection remains the escape hatch, now read through a getFetchUnownedDeals helper sitting beside getDealSyncFilter so both sides read it one way.
The apostrophe escape emitted \'' where SOQL wants \', so any keyword containing an apostrophe returned HTTP 400. Verified against the unmodified live tool. Because trySearchAndLinkCrmDeal has no try/catch around searchDealsByKeyword, that exception aborted the entire auto-link run rather than one search , so Rothy's, L'Oreal, Not Your Mother's and every other apostrophe account silently never auto-linked.
80 lines across 5 files. The gate is one argument.
An earlier version of this PR added a getConnectedOwnerIds driver method, a crm-owner-scope.ts gate, a salesforce-identity.ts helper, and Salesforce 15/18-char id comparison logic , a second, Cedar-local mechanism for a question Cedar already answers elsewhere. All removed. That is also where both defects found in review lived: a lowercase comparison on the case-sensitive 15-char form, and a 15-vs-18 key mismatch.
The link-crm-deal tool gate is likewise dropped: it does not go through search, and keeping it would mean reintroducing the identity mechanism this removes. That path creates the opportunity it links, so the deal is owned by the creating user by construction. Worth revisiting if cross-owner links show up there , the fix would be adding the same two parameters to crm-fetch-deals-by-ids, left alone here because the reconcile depends on it returning unowned deals.
Already repaired out of band for the only affected account (Willem Ebbinge, 2026-08-28): **35 conversations downgraded to account-only linkage, 305 → 270 linked deals, 0 unowned rem
Libra has not measured any production surfaces for this change yet.