sf-intelligence 0.1.23 → 0.1.25
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/data/embedding-index.json +67943 -0
- package/dist/index.js +1631 -134
- package/package.json +3 -2
- package/server.json +2 -2
package/dist/index.js
CHANGED
|
@@ -964,6 +964,11 @@ var init_queries = __esm({
|
|
|
964
964
|
if (options?.recordTriggered === true) {
|
|
965
965
|
out += ` AND json_extract_string(properties_json, '$.triggerType') LIKE 'Record%'`;
|
|
966
966
|
}
|
|
967
|
+
if (options?.descriptionPresence === "present") {
|
|
968
|
+
out += ` AND coalesce(json_extract_string(properties_json, '$.description'), '') <> ''`;
|
|
969
|
+
} else if (options?.descriptionPresence === "absent") {
|
|
970
|
+
out += ` AND coalesce(json_extract_string(properties_json, '$.description'), '') = ''`;
|
|
971
|
+
}
|
|
967
972
|
return out;
|
|
968
973
|
};
|
|
969
974
|
parseProperties = (raw) => {
|
|
@@ -13056,8 +13061,22 @@ var init_enterprise_metadata = __esm({
|
|
|
13056
13061
|
});
|
|
13057
13062
|
return ok({ nodes: [node], edges: [...fieldRefEdges, ...childRefEdges, ...visibleToEdges] });
|
|
13058
13063
|
};
|
|
13059
|
-
extractReport = (path) => extractEnterpriseMetadata(path, {
|
|
13060
|
-
|
|
13064
|
+
extractReport = (path) => extractEnterpriseMetadata(path, {
|
|
13065
|
+
type: "Report",
|
|
13066
|
+
suffix: ".report-meta.xml",
|
|
13067
|
+
// Reports carry a top-level <description> in source. Capture it so
|
|
13068
|
+
// "which reports have no description" is answerable and get_component
|
|
13069
|
+
// can surface the report's stated purpose. Omitted when absent — the
|
|
13070
|
+
// "extracted, none present" signal (vs a not-modeled type).
|
|
13071
|
+
extraProperties: ["description"]
|
|
13072
|
+
});
|
|
13073
|
+
extractDashboard = (path) => extractEnterpriseMetadata(path, {
|
|
13074
|
+
type: "Dashboard",
|
|
13075
|
+
suffix: ".dashboard-meta.xml",
|
|
13076
|
+
// Dashboards carry a top-level <description>. Same capture rationale as
|
|
13077
|
+
// Report — omitted when absent.
|
|
13078
|
+
extraProperties: ["description"]
|
|
13079
|
+
});
|
|
13061
13080
|
extractListView = (path) => extractEnterpriseMetadata(path, {
|
|
13062
13081
|
type: "ListView",
|
|
13063
13082
|
suffix: ".listView-meta.xml",
|
|
@@ -13072,7 +13091,11 @@ var init_enterprise_metadata = __esm({
|
|
|
13072
13091
|
// Daily Summer '22") that distinguishes versioned clones from each other.
|
|
13073
13092
|
// Without this, makeNode always sets label: null and get_component has no
|
|
13074
13093
|
// human-readable display name to surface in vault Markdown.
|
|
13075
|
-
labelXmlElement: "label"
|
|
13094
|
+
labelXmlElement: "label",
|
|
13095
|
+
// ReportType XML carries a top-level <description> (nearly universal in
|
|
13096
|
+
// source). Capture it so custom report types disclose their purpose and
|
|
13097
|
+
// are queryable via missingDescription. Omitted when absent.
|
|
13098
|
+
extraProperties: ["description"]
|
|
13076
13099
|
});
|
|
13077
13100
|
extractCustomPermission = (path) => extractEnterpriseMetadata(path, { type: "CustomPermission", suffix: ".customPermission-meta.xml" });
|
|
13078
13101
|
FLEXIPAGE_PAGE_TYPES = /* @__PURE__ */ new Set([
|
|
@@ -13149,6 +13172,10 @@ var init_enterprise_metadata = __esm({
|
|
|
13149
13172
|
extractPermissionSetGroup = (path) => extractEnterpriseMetadata(path, {
|
|
13150
13173
|
type: "PermissionSetGroup",
|
|
13151
13174
|
suffix: ".permissionsetgroup-meta.xml",
|
|
13175
|
+
// PSGs carry a top-level <description>. Capture it so the group's stated
|
|
13176
|
+
// purpose is surfaced and queryable via missingDescription. Omitted when
|
|
13177
|
+
// absent.
|
|
13178
|
+
extraProperties: ["description"],
|
|
13152
13179
|
// A PSG's effective permissions are the UNION of its member permission
|
|
13153
13180
|
// sets' grants, minus the muting permission set's. Capture both so the
|
|
13154
13181
|
// permission analysis can flow god-mode / object grants through the group.
|
|
@@ -21564,13 +21591,13 @@ var init_auth = __esm({
|
|
|
21564
21591
|
stdout = result.stdout;
|
|
21565
21592
|
} catch (cause) {
|
|
21566
21593
|
const codeBag = cause;
|
|
21567
|
-
|
|
21594
|
+
const stderrOrMsg = codeBag.stderr ?? codeBag.message ?? String(cause);
|
|
21595
|
+
if (codeBag.code === "ENOENT" || stderrOrMsg.includes("is not recognized as an internal or external command")) {
|
|
21568
21596
|
return err({
|
|
21569
21597
|
kind: "sf-cli-missing",
|
|
21570
21598
|
message: "The `sf` CLI is not installed or not on PATH. Install it from https://developer.salesforce.com/tools/sfdxcli and run `sf org login web --target-org <alias>` to authenticate."
|
|
21571
21599
|
});
|
|
21572
21600
|
}
|
|
21573
|
-
const stderrOrMsg = codeBag.stderr ?? codeBag.message ?? String(cause);
|
|
21574
21601
|
if (stderrOrMsg.includes("No authentication for org") || stderrOrMsg.includes("NoOrgFound") || stderrOrMsg.includes("Unknown org")) {
|
|
21575
21602
|
return err({
|
|
21576
21603
|
kind: "org-not-found",
|
|
@@ -22484,9 +22511,13 @@ var init_picklist_literal_check = __esm({
|
|
|
22484
22511
|
// ../mcp/dist/src/tools/live-plane.js
|
|
22485
22512
|
var live_plane_exports = {};
|
|
22486
22513
|
__export(live_plane_exports, {
|
|
22514
|
+
GROUP_MEMBERS_DISCLOSURE: () => GROUP_MEMBERS_DISCLOSURE,
|
|
22487
22515
|
LICENSE_USAGE_DISCLOSURE: () => LICENSE_USAGE_DISCLOSURE,
|
|
22488
22516
|
LIVE_PLANE_DISCLOSURE: () => LIVE_PLANE_DISCLOSURE,
|
|
22517
|
+
PERMSET_HOLDERS_DISCLOSURE: () => PERMSET_HOLDERS_DISCLOSURE,
|
|
22489
22518
|
STALE_CHECK_TYPES: () => STALE_CHECK_TYPES,
|
|
22519
|
+
USER_PERMSETS_DISCLOSURE: () => USER_PERMSETS_DISCLOSURE,
|
|
22520
|
+
ZOMBIE_ACCOUNTS_DISCLOSURE: () => ZOMBIE_ACCOUNTS_DISCLOSURE,
|
|
22490
22521
|
apiPath: () => apiPath,
|
|
22491
22522
|
assertSoqlIdentifier: () => assertSoqlIdentifier,
|
|
22492
22523
|
checkVaultStaleness: () => checkVaultStaleness,
|
|
@@ -22513,6 +22544,8 @@ __export(live_plane_exports, {
|
|
|
22513
22544
|
liveFolderAccessInputSchema: () => liveFolderAccessInputSchema,
|
|
22514
22545
|
liveGroupCountHandler: () => liveGroupCountHandler,
|
|
22515
22546
|
liveGroupCountInputSchema: () => liveGroupCountInputSchema,
|
|
22547
|
+
liveGroupMembersHandler: () => liveGroupMembersHandler,
|
|
22548
|
+
liveGroupMembersInputSchema: () => liveGroupMembersInputSchema,
|
|
22516
22549
|
liveInactiveUsersHandler: () => liveInactiveUsersHandler,
|
|
22517
22550
|
liveInactiveUsersInputSchema: () => liveInactiveUsersInputSchema,
|
|
22518
22551
|
liveLicenseUsageHandler: () => liveLicenseUsageHandler,
|
|
@@ -22523,6 +22556,8 @@ __export(live_plane_exports, {
|
|
|
22523
22556
|
liveOrgLimitsInputSchema: () => liveOrgLimitsInputSchema,
|
|
22524
22557
|
liveOwnerBreakdownHandler: () => liveOwnerBreakdownHandler,
|
|
22525
22558
|
liveOwnerBreakdownInputSchema: () => liveOwnerBreakdownInputSchema,
|
|
22559
|
+
livePermsetHoldersHandler: () => livePermsetHoldersHandler,
|
|
22560
|
+
livePermsetHoldersInputSchema: () => livePermsetHoldersInputSchema,
|
|
22526
22561
|
liveRecentActivityHandler: () => liveRecentActivityHandler,
|
|
22527
22562
|
liveRecentActivityInputSchema: () => liveRecentActivityInputSchema,
|
|
22528
22563
|
liveReportUsageHandler: () => liveReportUsageHandler,
|
|
@@ -22539,13 +22574,17 @@ __export(live_plane_exports, {
|
|
|
22539
22574
|
liveStaleRecordsInputSchema: () => liveStaleRecordsInputSchema,
|
|
22540
22575
|
liveStorageByObjectHandler: () => liveStorageByObjectHandler,
|
|
22541
22576
|
liveStorageByObjectInputSchema: () => liveStorageByObjectInputSchema,
|
|
22577
|
+
liveUserPermsetsHandler: () => liveUserPermsetsHandler,
|
|
22578
|
+
liveUserPermsetsInputSchema: () => liveUserPermsetsInputSchema,
|
|
22579
|
+
liveZombieAccountsHandler: () => liveZombieAccountsHandler,
|
|
22580
|
+
liveZombieAccountsInputSchema: () => liveZombieAccountsInputSchema,
|
|
22542
22581
|
redactSecrets: () => redactSecrets,
|
|
22543
22582
|
resolveLiveAccess: () => resolveLiveAccess,
|
|
22544
22583
|
restGet: () => restGet,
|
|
22545
22584
|
runSfJson: () => runSfJson
|
|
22546
22585
|
});
|
|
22547
22586
|
import { z as z2 } from "zod";
|
|
22548
|
-
var MAX_SAMPLE_ROWS, SAMPLE_BYTE_BUDGET, LIVE_TABLE_ROW_CAP, liveEnabledSchema, isLivePlaneEnabled, liveTrust, resolveOrg, resolveLiveAccess, liveConsentRequiredError, gateLive, liveDescribeInputSchema, liveDescribeHandler, liveCountInputSchema, OBJECT_API_NAME_RE, resolveCountSoql, assertCountSoql, fromObjectOf, NO_VALIDATION, collectPicklistMismatches, scanSoqlForPicklistMismatchesSync, liveCountHandler, liveStaleCheckInputSchema, STALE_CHECK_TYPES, ISO_TIMESTAMP_RE, LIVE_STALE_BOUNDARIES, checkVaultStaleness, liveStaleCheckHandler, liveSampleInputSchema, capSampleSoql, liveSampleHandler, liveFieldPopulationInputSchema, liveFieldPopulationHandler, liveOrgLimitsInputSchema, liveOrgLimitsHandler, MAX_INACTIVE_USER_ROWS, DEFAULT_INACTIVE_USER_ROWS, INACTIVE_USERS_BYTE_BUDGET, DEFAULT_INACTIVE_DAYS, MS_PER_DAY, liveInactiveUsersInputSchema, soqlDateTime, fitInactiveUsers, liveInactiveUsersHandler, DEFAULT_LICENSE_INACTIVE_DAYS, MAX_RECLAIM_ROWS, LICENSE_USAGE_DISCLOSURE, liveLicenseUsageInputSchema, toUtilization, renderLicenseUsageMarkdown, liveLicenseUsageHandler, liveConsentInputSchema, consentTrust, liveConsentHandler, liveQuery, MAX_DETAIL_ROWS, daysAgoSoql, daysSince, livePlaneVaultState, UNAVAILABLE_ERROR, isBudgetExhaustedReason, BUDGET_SIGNAL, assertSoqlIdentifier, soqlLiteral, MAX_GROUP_BUCKETS, DEFAULT_STALE_DAYS, DEFAULT_RECENT_DAYS, liveGroupCountInputSchema, liveGroupCountHandler, liveStaleRecordsInputSchema, liveStaleRecordsHandler, liveRecentActivityInputSchema, liveRecentActivityHandler, buildEqualityWhere, aggregateCountFromRow, liveAggregateInputSchema, liveAggregateHandler, MAX_DUPLICATE_GROUPS, liveDuplicateCheckInputSchema, liveDuplicateCheckHandler, MAX_OWNER_BUCKETS, liveOwnerBreakdownInputSchema, liveOwnerBreakdownHandler, DEFAULT_REPORT_STALE_DAYS, liveReportUsageInputSchema, liveReportUsageHandler, liveFolderAccessInputSchema, liveFolderAccessHandler, DEFAULT_TEMPLATE_STALE_DAYS, CLASSIC_TEMPLATE_TYPES, liveEmailTemplateUsageInputSchema, liveEmailTemplateUsageHandler, DEFAULT_HEALTH_DAYS, LIMIT_RISK_THRESHOLD, liveOrgHealthInputSchema, liveOrgHealthHandler, liveStorageByObjectInputSchema, liveStorageByObjectHandler, DEFAULT_SKEW_THRESHOLD, liveDataSkewInputSchema, liveDataSkewHandler, DEFAULT_AUDIT_DAYS, liveSetupAuditTrailInputSchema, liveSetupAuditTrailHandler, liveSecurityExposureInputSchema, liveSecurityExposureHandler;
|
|
22587
|
+
var MAX_SAMPLE_ROWS, SAMPLE_BYTE_BUDGET, LIVE_TABLE_ROW_CAP, liveEnabledSchema, isLivePlaneEnabled, liveTrust, resolveOrg, resolveLiveAccess, liveConsentRequiredError, gateLive, liveDescribeInputSchema, liveDescribeHandler, liveCountInputSchema, OBJECT_API_NAME_RE, resolveCountSoql, assertCountSoql, fromObjectOf, NO_VALIDATION, collectPicklistMismatches, scanSoqlForPicklistMismatchesSync, liveCountHandler, liveStaleCheckInputSchema, STALE_CHECK_TYPES, ISO_TIMESTAMP_RE, LIVE_STALE_BOUNDARIES, checkVaultStaleness, liveStaleCheckHandler, liveSampleInputSchema, capSampleSoql, liveSampleHandler, liveFieldPopulationInputSchema, liveFieldPopulationHandler, liveOrgLimitsInputSchema, liveOrgLimitsHandler, MAX_INACTIVE_USER_ROWS, DEFAULT_INACTIVE_USER_ROWS, INACTIVE_USERS_BYTE_BUDGET, DEFAULT_INACTIVE_DAYS, MS_PER_DAY, liveInactiveUsersInputSchema, soqlDateTime, fitInactiveUsers, liveInactiveUsersHandler, DEFAULT_LICENSE_INACTIVE_DAYS, MAX_RECLAIM_ROWS, LICENSE_USAGE_DISCLOSURE, liveLicenseUsageInputSchema, toUtilization, renderLicenseUsageMarkdown, liveLicenseUsageHandler, liveConsentInputSchema, consentTrust, liveConsentHandler, liveQuery, MAX_DETAIL_ROWS, daysAgoSoql, daysSince, livePlaneVaultState, UNAVAILABLE_ERROR, isBudgetExhaustedReason, BUDGET_SIGNAL, assertSoqlIdentifier, soqlLiteral, MAX_GROUP_BUCKETS, DEFAULT_STALE_DAYS, DEFAULT_RECENT_DAYS, liveGroupCountInputSchema, liveGroupCountHandler, liveStaleRecordsInputSchema, liveStaleRecordsHandler, liveRecentActivityInputSchema, liveRecentActivityHandler, buildEqualityWhere, aggregateCountFromRow, liveAggregateInputSchema, liveAggregateHandler, MAX_DUPLICATE_GROUPS, liveDuplicateCheckInputSchema, liveDuplicateCheckHandler, MAX_OWNER_BUCKETS, liveOwnerBreakdownInputSchema, liveOwnerBreakdownHandler, DEFAULT_REPORT_STALE_DAYS, liveReportUsageInputSchema, liveReportUsageHandler, liveFolderAccessInputSchema, liveFolderAccessHandler, DEFAULT_TEMPLATE_STALE_DAYS, CLASSIC_TEMPLATE_TYPES, liveEmailTemplateUsageInputSchema, liveEmailTemplateUsageHandler, DEFAULT_HEALTH_DAYS, LIMIT_RISK_THRESHOLD, liveOrgHealthInputSchema, liveOrgHealthHandler, liveStorageByObjectInputSchema, liveStorageByObjectHandler, DEFAULT_SKEW_THRESHOLD, liveDataSkewInputSchema, liveDataSkewHandler, DEFAULT_AUDIT_DAYS, liveSetupAuditTrailInputSchema, liveSetupAuditTrailHandler, liveSecurityExposureInputSchema, liveSecurityExposureHandler, MAX_HOLDER_ROWS, DEFAULT_HOLDER_ROWS, HOLDERS_BYTE_BUDGET, MAX_HOLDER_BUCKETS, PERMSET_HOLDERS_DISCLOSURE, livePermsetHoldersInputSchema, holderNotFoundError, holderAmbiguousError, resolveHolderTarget, toPermsetHolder, renderPermsetHoldersMarkdown, aggregateNumber, livePermsetHoldersHandler, splitHolders, fitPermsetHolders, MAX_ZOMBIE_ROWS, DEFAULT_ZOMBIE_ROWS, ZOMBIE_BYTE_BUDGET, ZOMBIE_CLIENT_DIFF_CAP, ZOMBIE_ACCOUNTS_DISCLOSURE, liveZombieAccountsInputSchema, toZombieUser, renderZombieAccountsMarkdown, fitZombieUsers, liveZombieAccountsHandler, MAX_MEMBER_ROWS, DEFAULT_MEMBER_ROWS, MEMBERS_BYTE_BUDGET, MEMBER_ID_CHUNK, GROUP_MEMBERS_DISCLOSURE, liveGroupMembersInputSchema, idChunks, resolveMemberUsers, renderGroupMembersMarkdown, fitGroupMembers, liveGroupMembersHandler, MAX_USER_PERMSET_ROWS, DEFAULT_USER_PERMSET_ROWS, USER_PERMSETS_BYTE_BUDGET, USER_PERMSETS_DISCLOSURE, liveUserPermsetsInputSchema, renderUserPermsetsMarkdown, fitUserPermsets, liveUserPermsetsHandler;
|
|
22549
22588
|
var init_live_plane = __esm({
|
|
22550
22589
|
"../mcp/dist/src/tools/live-plane.js"() {
|
|
22551
22590
|
"use strict";
|
|
@@ -24184,6 +24223,855 @@ ${renderTrustFooter(trust)}`;
|
|
|
24184
24223
|
vaultState: livePlaneVaultState(ctx)
|
|
24185
24224
|
});
|
|
24186
24225
|
};
|
|
24226
|
+
MAX_HOLDER_ROWS = 500;
|
|
24227
|
+
DEFAULT_HOLDER_ROWS = 100;
|
|
24228
|
+
HOLDERS_BYTE_BUDGET = 36e3;
|
|
24229
|
+
MAX_HOLDER_BUCKETS = 50;
|
|
24230
|
+
PERMSET_HOLDERS_DISCLOSURE = "Read-only; point-in-time as of queriedAt \u2014 never cached beyond the live-session TTL; roster is CURRENT org state, unlike vault answers.";
|
|
24231
|
+
livePermsetHoldersInputSchema = liveEnabledSchema.extend({
|
|
24232
|
+
/** PermissionSet.Name / PSG DeveloperName / Profile.Name (labels accepted). */
|
|
24233
|
+
name: z2.string().min(1),
|
|
24234
|
+
/** What `name` names. Default 'auto': probe PermissionSet → PermissionSetGroup
|
|
24235
|
+
* → Profile by exact name/label; no-match or ambiguity is an honest error,
|
|
24236
|
+
* never a guess. */
|
|
24237
|
+
kind: z2.enum(["permissionSet", "permissionSetGroup", "profile", "auto"]).optional(),
|
|
24238
|
+
/** Include inactive assignees (default false — active users only). */
|
|
24239
|
+
includeInactiveAssignees: z2.boolean().optional(),
|
|
24240
|
+
/** For kind 'permissionSet': also count holders who receive the set through a
|
|
24241
|
+
* PermissionSetGroup containing it (default TRUE — see the PSG trap above). */
|
|
24242
|
+
includeViaGroups: z2.boolean().optional(),
|
|
24243
|
+
/** Aggregation buckets for the headline (default 'profile'). */
|
|
24244
|
+
groupBy: z2.enum(["none", "profile"]).optional(),
|
|
24245
|
+
/** Max detail rows (default 100, hard cap 500); a ~36 KB byte budget may trim
|
|
24246
|
+
* the page further. `totalAssignees` is always the TRUE deduped count. */
|
|
24247
|
+
limit: z2.number().int().min(1).max(MAX_HOLDER_ROWS).optional(),
|
|
24248
|
+
/** Keyset paging: return rows with Id > afterId (use `nextAfterId`). */
|
|
24249
|
+
afterId: z2.string().optional(),
|
|
24250
|
+
orgAlias: z2.string().min(1).optional()
|
|
24251
|
+
});
|
|
24252
|
+
holderNotFoundError = (name, probed) => ({
|
|
24253
|
+
kind: "component-not-found",
|
|
24254
|
+
message: `No exact match for '${name}' \u2014 probed ${probed.join(", ")} by exact name/label in the live org. Check the API name (sfi.resolve can fix typos against the vault), or pass \`kind\` explicitly.`
|
|
24255
|
+
});
|
|
24256
|
+
holderAmbiguousError = (name, kind, count) => ({
|
|
24257
|
+
kind: "invalid-query",
|
|
24258
|
+
message: `'${name}' matches ${count}+ ${kind} records in the live org \u2014 refusing to guess. Use the exact API name (Name/DeveloperName), not a label shared by several.`
|
|
24259
|
+
});
|
|
24260
|
+
resolveHolderTarget = async (org, name, kind, exec) => {
|
|
24261
|
+
const lit = soqlLiteral(name);
|
|
24262
|
+
const probes = [
|
|
24263
|
+
{
|
|
24264
|
+
kind: "permissionSet",
|
|
24265
|
+
// IsOwnedByProfile = false: every Profile owns a system permission set
|
|
24266
|
+
// with a matching name — matching one here would silently answer the
|
|
24267
|
+
// PROFILE question under the permissionSet kind.
|
|
24268
|
+
soql: `SELECT Id, Name FROM PermissionSet WHERE (Name = ${lit} OR Label = ${lit}) AND IsOwnedByProfile = false LIMIT 2`,
|
|
24269
|
+
nameField: "Name"
|
|
24270
|
+
},
|
|
24271
|
+
{
|
|
24272
|
+
kind: "permissionSetGroup",
|
|
24273
|
+
soql: `SELECT Id, DeveloperName FROM PermissionSetGroup WHERE (DeveloperName = ${lit} OR MasterLabel = ${lit}) LIMIT 2`,
|
|
24274
|
+
nameField: "DeveloperName"
|
|
24275
|
+
},
|
|
24276
|
+
{
|
|
24277
|
+
kind: "profile",
|
|
24278
|
+
soql: `SELECT Id, Name FROM Profile WHERE Name = ${lit} LIMIT 2`,
|
|
24279
|
+
nameField: "Name"
|
|
24280
|
+
}
|
|
24281
|
+
];
|
|
24282
|
+
const wanted = kind === void 0 || kind === "auto" ? probes : probes.filter((p2) => p2.kind === kind);
|
|
24283
|
+
const probedKinds = [];
|
|
24284
|
+
for (const probe of wanted) {
|
|
24285
|
+
probedKinds.push(probe.kind);
|
|
24286
|
+
const q2 = await liveQuery(org, probe.soql, exec);
|
|
24287
|
+
if (!q2.available) {
|
|
24288
|
+
return err({
|
|
24289
|
+
kind: "invalid-query",
|
|
24290
|
+
message: `Could not probe ${probe.kind} for '${name}': ${redactSecrets(q2.reason ?? "query failed").slice(0, 160)}`
|
|
24291
|
+
});
|
|
24292
|
+
}
|
|
24293
|
+
if (q2.records.length > 1)
|
|
24294
|
+
return err(holderAmbiguousError(name, probe.kind, q2.records.length));
|
|
24295
|
+
if (q2.records.length === 1) {
|
|
24296
|
+
const row = q2.records[0];
|
|
24297
|
+
return ok({
|
|
24298
|
+
kind: probe.kind,
|
|
24299
|
+
id: String(row.Id ?? ""),
|
|
24300
|
+
name: String(row[probe.nameField] ?? name)
|
|
24301
|
+
});
|
|
24302
|
+
}
|
|
24303
|
+
}
|
|
24304
|
+
return err(holderNotFoundError(name, probedKinds));
|
|
24305
|
+
};
|
|
24306
|
+
toPermsetHolder = (r2) => ({
|
|
24307
|
+
assignmentId: String(r2.Id ?? ""),
|
|
24308
|
+
userId: String(r2.AssigneeId ?? ""),
|
|
24309
|
+
name: String(r2.Assignee?.Name ?? ""),
|
|
24310
|
+
username: String(r2.Assignee?.Username ?? ""),
|
|
24311
|
+
isActive: r2.Assignee?.IsActive === true,
|
|
24312
|
+
profileName: r2.Assignee?.Profile?.Name ?? null,
|
|
24313
|
+
expirationDate: r2.ExpirationDate ?? null,
|
|
24314
|
+
viaGroup: r2.PermissionSetGroupId ? r2.PermissionSetGroup?.DeveloperName ?? r2.PermissionSetGroupId : null
|
|
24315
|
+
});
|
|
24316
|
+
renderPermsetHoldersMarkdown = (data) => {
|
|
24317
|
+
const kindLabel = {
|
|
24318
|
+
permissionSet: "permission set",
|
|
24319
|
+
permissionSetGroup: "permission set group",
|
|
24320
|
+
profile: "profile"
|
|
24321
|
+
};
|
|
24322
|
+
const direct = data.directHolders.length;
|
|
24323
|
+
const viaGroups = (data.viaGroupHolders ?? []).reduce((n2, g2) => n2 + g2.holders.length, 0);
|
|
24324
|
+
const lines = [
|
|
24325
|
+
`### Holders of ${kindLabel[data.target.kind]} \`${data.target.name}\``,
|
|
24326
|
+
"",
|
|
24327
|
+
`**${data.totalAssignees.toLocaleString("en-US")}** ${data.target.kind === "profile" ? "users hold this profile" : "distinct users hold this"}` + (data.target.kind === "permissionSet" ? ` (${direct} direct row(s), ${viaGroups} via-group row(s) on this page)` : "") + (data.expiredExcluded > 0 ? `; ${data.expiredExcluded} expired assignment(s) excluded` : "") + "."
|
|
24328
|
+
];
|
|
24329
|
+
if (data.buckets !== void 0 && data.buckets.length > 0) {
|
|
24330
|
+
lines.push("", "**By profile**", "", mdTable(["Profile", "Holders"], data.buckets.map((b2) => [b2.profileName ?? b2.profileId ?? "(none)", b2.holders])));
|
|
24331
|
+
}
|
|
24332
|
+
const holderRows = (holders) => mdTable(["User", "Username", "Profile", "Active", "Expires"], holders.map((h2) => [
|
|
24333
|
+
h2.name,
|
|
24334
|
+
h2.username,
|
|
24335
|
+
h2.profileName ?? "\u2014",
|
|
24336
|
+
h2.isActive ? "yes" : "no",
|
|
24337
|
+
h2.expirationDate ?? "\u2014"
|
|
24338
|
+
]));
|
|
24339
|
+
if (data.directHolders.length > 0) {
|
|
24340
|
+
lines.push("", data.target.kind === "permissionSet" ? "**Direct holders**" : "**Holders**", "", holderRows(data.directHolders));
|
|
24341
|
+
}
|
|
24342
|
+
for (const g2 of data.viaGroupHolders ?? []) {
|
|
24343
|
+
if (g2.holders.length > 0) {
|
|
24344
|
+
lines.push("", `**Via group \`${g2.groupName}\`**`, "", holderRows(g2.holders));
|
|
24345
|
+
}
|
|
24346
|
+
}
|
|
24347
|
+
if (data.capped) {
|
|
24348
|
+
lines.push("", `Showing ${data.returned} of ${data.totalAssignees} holder(s)` + (data.nextAfterId !== void 0 ? ` \u2014 pass \`afterId: "${data.nextAfterId}"\` for the next page.` : "."));
|
|
24349
|
+
}
|
|
24350
|
+
lines.push("", PERMSET_HOLDERS_DISCLOSURE, "", renderTrustFooter(data.trust));
|
|
24351
|
+
return lines.join("\n");
|
|
24352
|
+
};
|
|
24353
|
+
aggregateNumber = (records, alias) => {
|
|
24354
|
+
const row = records[0];
|
|
24355
|
+
if (row === void 0)
|
|
24356
|
+
return 0;
|
|
24357
|
+
return Number(row[alias] ?? row["expr0"] ?? 0);
|
|
24358
|
+
};
|
|
24359
|
+
livePermsetHoldersHandler = async (ctx, input2, exec = nodeExecFile2) => {
|
|
24360
|
+
const gate = await gateLive(ctx, input2);
|
|
24361
|
+
if (!gate.ok)
|
|
24362
|
+
return gate;
|
|
24363
|
+
const org = gate.value;
|
|
24364
|
+
const queriedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
24365
|
+
const limit = input2.limit ?? DEFAULT_HOLDER_ROWS;
|
|
24366
|
+
const groupBy = input2.groupBy ?? "profile";
|
|
24367
|
+
const includeViaGroups = input2.includeViaGroups ?? true;
|
|
24368
|
+
const nowLiteral = soqlDateTime(/* @__PURE__ */ new Date());
|
|
24369
|
+
const resolved = await resolveHolderTarget(org, input2.name, input2.kind, exec);
|
|
24370
|
+
if (!resolved.ok)
|
|
24371
|
+
return resolved;
|
|
24372
|
+
const target = resolved.value;
|
|
24373
|
+
if (input2.afterId !== void 0 && !/^[a-zA-Z0-9]{15,18}$/.test(input2.afterId)) {
|
|
24374
|
+
return err({
|
|
24375
|
+
kind: "invalid-query",
|
|
24376
|
+
message: "afterId must be a Salesforce record Id (the `nextAfterId` from the previous page).",
|
|
24377
|
+
path: "afterId"
|
|
24378
|
+
});
|
|
24379
|
+
}
|
|
24380
|
+
const afterClause = input2.afterId !== void 0 ? ` AND Id > '${input2.afterId}'` : "";
|
|
24381
|
+
if (target.kind === "profile") {
|
|
24382
|
+
const activeClause2 = input2.includeInactiveAssignees ? "" : " AND IsActive = true";
|
|
24383
|
+
const where2 = `ProfileId = '${target.id}'${activeClause2}`;
|
|
24384
|
+
const totalQ2 = await liveQuery(org, `SELECT COUNT() FROM User WHERE ${where2}`, exec);
|
|
24385
|
+
if (!totalQ2.available)
|
|
24386
|
+
return err(UNAVAILABLE_ERROR("User", org, totalQ2.reason));
|
|
24387
|
+
const totalAssignees2 = totalQ2.total;
|
|
24388
|
+
let activeAssignees2 = totalAssignees2;
|
|
24389
|
+
if (input2.includeInactiveAssignees) {
|
|
24390
|
+
const activeQ = await liveQuery(org, `SELECT COUNT() FROM User WHERE ProfileId = '${target.id}' AND IsActive = true`, exec);
|
|
24391
|
+
if (!activeQ.available)
|
|
24392
|
+
return err(UNAVAILABLE_ERROR("User", org, activeQ.reason));
|
|
24393
|
+
activeAssignees2 = activeQ.total;
|
|
24394
|
+
}
|
|
24395
|
+
const detailQ2 = await liveQuery(org, `SELECT Id, Name, Username, IsActive, Profile.Name FROM User WHERE ${where2}${afterClause} ORDER BY Id LIMIT ${limit}`, exec);
|
|
24396
|
+
if (!detailQ2.available)
|
|
24397
|
+
return err(UNAVAILABLE_ERROR("User", org, detailQ2.reason));
|
|
24398
|
+
const holders = detailQ2.records.map((r2) => ({
|
|
24399
|
+
assignmentId: null,
|
|
24400
|
+
userId: String(r2.Id ?? ""),
|
|
24401
|
+
name: String(r2.Name ?? ""),
|
|
24402
|
+
username: String(r2.Username ?? ""),
|
|
24403
|
+
isActive: r2.IsActive === true,
|
|
24404
|
+
profileName: r2.Profile?.Name ?? null,
|
|
24405
|
+
expirationDate: null,
|
|
24406
|
+
viaGroup: null
|
|
24407
|
+
}));
|
|
24408
|
+
return ok({
|
|
24409
|
+
data: fitPermsetHolders({
|
|
24410
|
+
target,
|
|
24411
|
+
totalAssignees: totalAssignees2,
|
|
24412
|
+
activeAssignees: activeAssignees2,
|
|
24413
|
+
expiredExcluded: 0,
|
|
24414
|
+
trust: liveTrust(queriedAt)
|
|
24415
|
+
}, holders, [], void 0),
|
|
24416
|
+
vaultState: livePlaneVaultState(ctx)
|
|
24417
|
+
});
|
|
24418
|
+
}
|
|
24419
|
+
let viaGroups = [];
|
|
24420
|
+
if (target.kind === "permissionSet" && includeViaGroups) {
|
|
24421
|
+
const psgcQ = await liveQuery(org, `SELECT PermissionSetGroupId, PermissionSetGroup.DeveloperName FROM PermissionSetGroupComponent WHERE PermissionSetId = '${target.id}'`, exec);
|
|
24422
|
+
if (!psgcQ.available) {
|
|
24423
|
+
return err(UNAVAILABLE_ERROR("PermissionSetGroupComponent", org, psgcQ.reason));
|
|
24424
|
+
}
|
|
24425
|
+
viaGroups = psgcQ.records.map((r2) => {
|
|
24426
|
+
const row = r2;
|
|
24427
|
+
return {
|
|
24428
|
+
id: String(row.PermissionSetGroupId ?? ""),
|
|
24429
|
+
name: String(row.PermissionSetGroup?.DeveloperName ?? row.PermissionSetGroupId ?? "")
|
|
24430
|
+
};
|
|
24431
|
+
});
|
|
24432
|
+
}
|
|
24433
|
+
const scope = target.kind === "permissionSetGroup" ? `PermissionSetGroupId = '${target.id}'` : viaGroups.length > 0 ? `((PermissionSetId = '${target.id}' AND PermissionSetGroupId = null) OR PermissionSetGroupId IN (${viaGroups.map((g2) => `'${g2.id}'`).join(", ")}))` : `(PermissionSetId = '${target.id}' AND PermissionSetGroupId = null)`;
|
|
24434
|
+
const activeClause = input2.includeInactiveAssignees ? "" : " AND Assignee.IsActive = true";
|
|
24435
|
+
const notExpiredClause = ` AND (ExpirationDate = null OR ExpirationDate >= ${nowLiteral})`;
|
|
24436
|
+
const where = `${scope}${activeClause}${notExpiredClause}`;
|
|
24437
|
+
const totalQ = await liveQuery(org, `SELECT COUNT_DISTINCT(AssigneeId) total FROM PermissionSetAssignment WHERE ${where}`, exec);
|
|
24438
|
+
if (!totalQ.available)
|
|
24439
|
+
return err(UNAVAILABLE_ERROR("PermissionSetAssignment", org, totalQ.reason));
|
|
24440
|
+
const totalAssignees = aggregateNumber(totalQ.records, "total");
|
|
24441
|
+
let activeAssignees = totalAssignees;
|
|
24442
|
+
if (input2.includeInactiveAssignees) {
|
|
24443
|
+
const activeQ = await liveQuery(org, `SELECT COUNT_DISTINCT(AssigneeId) total FROM PermissionSetAssignment WHERE ${scope} AND Assignee.IsActive = true${notExpiredClause}`, exec);
|
|
24444
|
+
if (!activeQ.available)
|
|
24445
|
+
return err(UNAVAILABLE_ERROR("PermissionSetAssignment", org, activeQ.reason));
|
|
24446
|
+
activeAssignees = aggregateNumber(activeQ.records, "total");
|
|
24447
|
+
}
|
|
24448
|
+
const expiredQ = await liveQuery(org, `SELECT COUNT() FROM PermissionSetAssignment WHERE ${scope}${activeClause} AND ExpirationDate < ${nowLiteral}`, exec);
|
|
24449
|
+
const expiredExcluded = expiredQ.available ? expiredQ.total : 0;
|
|
24450
|
+
let buckets;
|
|
24451
|
+
if (groupBy === "profile") {
|
|
24452
|
+
const bucketQ = await liveQuery(org, `SELECT Assignee.ProfileId pid, MAX(Assignee.Profile.Name) pname, COUNT(Id) holders FROM PermissionSetAssignment WHERE ${where} GROUP BY Assignee.ProfileId ORDER BY COUNT(Id) DESC LIMIT ${MAX_HOLDER_BUCKETS}`, exec);
|
|
24453
|
+
if (bucketQ.available) {
|
|
24454
|
+
buckets = bucketQ.records.map((r2) => {
|
|
24455
|
+
const row = r2;
|
|
24456
|
+
return {
|
|
24457
|
+
profileId: row.pid === void 0 || row.pid === null ? null : String(row.pid),
|
|
24458
|
+
profileName: row.pname === void 0 || row.pname === null ? null : String(row.pname),
|
|
24459
|
+
holders: Number(row.holders ?? row.expr1 ?? 0)
|
|
24460
|
+
};
|
|
24461
|
+
});
|
|
24462
|
+
}
|
|
24463
|
+
}
|
|
24464
|
+
const detailQ = await liveQuery(org, `SELECT Id, AssigneeId, Assignee.Name, Assignee.Username, Assignee.IsActive, Assignee.Profile.Name, ExpirationDate, PermissionSetGroupId, PermissionSetGroup.DeveloperName FROM PermissionSetAssignment WHERE ${where}${afterClause} ORDER BY Id LIMIT ${limit}`, exec);
|
|
24465
|
+
if (!detailQ.available)
|
|
24466
|
+
return err(UNAVAILABLE_ERROR("PermissionSetAssignment", org, detailQ.reason));
|
|
24467
|
+
const rows = detailQ.records.map(toPermsetHolder);
|
|
24468
|
+
return ok({
|
|
24469
|
+
data: fitPermsetHolders({
|
|
24470
|
+
target,
|
|
24471
|
+
totalAssignees,
|
|
24472
|
+
activeAssignees,
|
|
24473
|
+
expiredExcluded,
|
|
24474
|
+
trust: liveTrust(queriedAt)
|
|
24475
|
+
}, rows, viaGroups.map((g2) => g2.name), buckets),
|
|
24476
|
+
vaultState: livePlaneVaultState(ctx)
|
|
24477
|
+
});
|
|
24478
|
+
};
|
|
24479
|
+
splitHolders = (rows, groupNames) => {
|
|
24480
|
+
const direct = rows.filter((h2) => h2.viaGroup === null);
|
|
24481
|
+
const byGroup = /* @__PURE__ */ new Map();
|
|
24482
|
+
for (const h2 of rows) {
|
|
24483
|
+
if (h2.viaGroup === null)
|
|
24484
|
+
continue;
|
|
24485
|
+
const list = byGroup.get(h2.viaGroup) ?? [];
|
|
24486
|
+
list.push(h2);
|
|
24487
|
+
byGroup.set(h2.viaGroup, list);
|
|
24488
|
+
}
|
|
24489
|
+
const ordered = [
|
|
24490
|
+
...groupNames.filter((g2) => byGroup.has(g2)),
|
|
24491
|
+
...[...byGroup.keys()].filter((g2) => !groupNames.includes(g2))
|
|
24492
|
+
];
|
|
24493
|
+
return {
|
|
24494
|
+
direct,
|
|
24495
|
+
via: ordered.map((g2) => ({ groupName: g2, holders: byGroup.get(g2) ?? [] }))
|
|
24496
|
+
};
|
|
24497
|
+
};
|
|
24498
|
+
fitPermsetHolders = (base, allRows, groupNames, buckets) => {
|
|
24499
|
+
let slice = allRows;
|
|
24500
|
+
let byteTrimmed = false;
|
|
24501
|
+
for (; ; ) {
|
|
24502
|
+
const returned = slice.length;
|
|
24503
|
+
const distinctReturned = new Set(slice.map((h2) => h2.userId)).size;
|
|
24504
|
+
const capped = byteTrimmed || base.totalAssignees > distinctReturned;
|
|
24505
|
+
const last = slice[slice.length - 1];
|
|
24506
|
+
const nextAfterId = capped && last !== void 0 ? last.assignmentId ?? last.userId : void 0;
|
|
24507
|
+
const { direct, via } = splitHolders(slice, groupNames);
|
|
24508
|
+
const shape = {
|
|
24509
|
+
...base,
|
|
24510
|
+
returned,
|
|
24511
|
+
capped,
|
|
24512
|
+
...nextAfterId !== void 0 ? { nextAfterId } : {},
|
|
24513
|
+
...buckets !== void 0 ? { buckets } : {},
|
|
24514
|
+
directHolders: direct,
|
|
24515
|
+
...groupNames.length > 0 || via.length > 0 ? { viaGroupHolders: via } : {},
|
|
24516
|
+
...byteTrimmed ? {
|
|
24517
|
+
note: `Detail rows trimmed to ${returned} to stay within the response size limit; totalAssignees (${base.totalAssignees}) is the true count \u2014 page on with afterId.`
|
|
24518
|
+
} : {}
|
|
24519
|
+
};
|
|
24520
|
+
const rendered = renderPermsetHoldersMarkdown(shape);
|
|
24521
|
+
const bytes = Buffer.byteLength(JSON.stringify({ ...shape, rendered }), "utf8");
|
|
24522
|
+
if (bytes <= HOLDERS_BYTE_BUDGET || slice.length <= 1) {
|
|
24523
|
+
return { ...shape, rendered };
|
|
24524
|
+
}
|
|
24525
|
+
slice = slice.slice(0, Math.max(1, Math.floor(slice.length * 0.85)));
|
|
24526
|
+
byteTrimmed = true;
|
|
24527
|
+
}
|
|
24528
|
+
};
|
|
24529
|
+
MAX_ZOMBIE_ROWS = 500;
|
|
24530
|
+
DEFAULT_ZOMBIE_ROWS = 100;
|
|
24531
|
+
ZOMBIE_BYTE_BUDGET = 36e3;
|
|
24532
|
+
ZOMBIE_CLIENT_DIFF_CAP = 2e3;
|
|
24533
|
+
ZOMBIE_ACCOUNTS_DISCLOSURE = 'A "zombie" here still holds every permission its PROFILE grants \u2014 this tool reports "no permission-set/PSG assignments", NOT "no access". Read-only; point-in-time as of queriedAt.';
|
|
24534
|
+
liveZombieAccountsInputSchema = liveEnabledSchema.extend({
|
|
24535
|
+
/** Additionally require last login older than N days (default 0 = ignore
|
|
24536
|
+
* login age; dormancy alone is sfi.live_inactive_users' job). */
|
|
24537
|
+
minDaysInactive: z2.number().int().min(0).max(3650).optional(),
|
|
24538
|
+
/** Include non-Standard user types (default false — Standard/human only). */
|
|
24539
|
+
includeAllUserTypes: z2.boolean().optional(),
|
|
24540
|
+
/** Max detail rows (default 100, hard cap 500); byte budget may trim further.
|
|
24541
|
+
* `totalZombies` is always the true count. */
|
|
24542
|
+
limit: z2.number().int().min(1).max(MAX_ZOMBIE_ROWS).optional(),
|
|
24543
|
+
orgAlias: z2.string().min(1).optional()
|
|
24544
|
+
});
|
|
24545
|
+
toZombieUser = (r2) => ({
|
|
24546
|
+
id: String(r2.Id ?? ""),
|
|
24547
|
+
name: String(r2.Name ?? ""),
|
|
24548
|
+
username: String(r2.Username ?? ""),
|
|
24549
|
+
profileName: r2.Profile?.Name ?? null,
|
|
24550
|
+
lastLoginDate: r2.LastLoginDate ?? null,
|
|
24551
|
+
neverLoggedIn: (r2.LastLoginDate ?? null) === null
|
|
24552
|
+
});
|
|
24553
|
+
renderZombieAccountsMarkdown = (data) => {
|
|
24554
|
+
const head = `**${data.totalZombies.toLocaleString("en-US")}** active ${data.criteria.userTypeFilter === "Standard" ? "Standard " : ""}user(s) have login access but ZERO permission-set/PSG assignments` + (data.criteria.cutoff !== null ? ` and no login within ${data.criteria.minDaysInactive} days` : "") + ".";
|
|
24555
|
+
const table = data.users.length === 0 ? "" : `
|
|
24556
|
+
|
|
24557
|
+
${mdTable(["User", "Username", "Profile", "Last login"], data.users.map((u2) => [
|
|
24558
|
+
u2.name,
|
|
24559
|
+
u2.username,
|
|
24560
|
+
u2.profileName ?? "\u2014",
|
|
24561
|
+
u2.neverLoggedIn ? "never" : u2.lastLoginDate ?? "\u2014"
|
|
24562
|
+
]))}`;
|
|
24563
|
+
const cappedLine = data.capped ? `
|
|
24564
|
+
|
|
24565
|
+
Showing ${data.returned} of ${data.totalZombies}.` : "";
|
|
24566
|
+
const methodLine = data.method === "client-diff" ? `
|
|
24567
|
+
|
|
24568
|
+
Method: client-diff \u2014 the org rejected the single anti-join SOQL, so this was computed by diffing two bounded queries (scan cap ${ZOMBIE_CLIENT_DIFF_CAP} rows each).` : "";
|
|
24569
|
+
return `${head}${table}${cappedLine}${methodLine}
|
|
24570
|
+
|
|
24571
|
+
${data.disclosure}
|
|
24572
|
+
|
|
24573
|
+
${renderTrustFooter(data.trust)}`;
|
|
24574
|
+
};
|
|
24575
|
+
fitZombieUsers = (base, allUsers, extraNote) => {
|
|
24576
|
+
let slice = allUsers;
|
|
24577
|
+
let byteTrimmed = false;
|
|
24578
|
+
for (; ; ) {
|
|
24579
|
+
const returned = slice.length;
|
|
24580
|
+
const capped = byteTrimmed || base.totalZombies > returned;
|
|
24581
|
+
const noteText = [
|
|
24582
|
+
...extraNote !== void 0 ? [extraNote] : [],
|
|
24583
|
+
...byteTrimmed ? [
|
|
24584
|
+
`Detail rows trimmed to ${returned} to stay within the response size limit; totalZombies (${base.totalZombies}) is the true count.`
|
|
24585
|
+
] : []
|
|
24586
|
+
].join(" ");
|
|
24587
|
+
const shape = {
|
|
24588
|
+
...base,
|
|
24589
|
+
returned,
|
|
24590
|
+
capped,
|
|
24591
|
+
users: slice,
|
|
24592
|
+
...noteText.length > 0 ? { note: noteText } : {}
|
|
24593
|
+
};
|
|
24594
|
+
const rendered = renderZombieAccountsMarkdown(shape);
|
|
24595
|
+
const bytes = Buffer.byteLength(JSON.stringify({ ...shape, rendered }), "utf8");
|
|
24596
|
+
if (bytes <= ZOMBIE_BYTE_BUDGET || slice.length <= 1) {
|
|
24597
|
+
return { ...shape, rendered };
|
|
24598
|
+
}
|
|
24599
|
+
slice = slice.slice(0, Math.max(1, Math.floor(slice.length * 0.85)));
|
|
24600
|
+
byteTrimmed = true;
|
|
24601
|
+
}
|
|
24602
|
+
};
|
|
24603
|
+
liveZombieAccountsHandler = async (ctx, input2, exec = nodeExecFile2) => {
|
|
24604
|
+
const gate = await gateLive(ctx, input2);
|
|
24605
|
+
if (!gate.ok)
|
|
24606
|
+
return gate;
|
|
24607
|
+
const org = gate.value;
|
|
24608
|
+
const queriedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
24609
|
+
const minDaysInactive = input2.minDaysInactive ?? 0;
|
|
24610
|
+
const limit = input2.limit ?? DEFAULT_ZOMBIE_ROWS;
|
|
24611
|
+
const cutoff = minDaysInactive > 0 ? daysAgoSoql(minDaysInactive) : null;
|
|
24612
|
+
const userTypeClause = input2.includeAllUserTypes ? "" : ` AND UserType = 'Standard'`;
|
|
24613
|
+
const loginClause = cutoff !== null ? ` AND (LastLoginDate < ${cutoff} OR LastLoginDate = null)` : "";
|
|
24614
|
+
const baseWhere = `IsActive = true${userTypeClause}${loginClause}`;
|
|
24615
|
+
const antiJoin = ` AND Id NOT IN (SELECT AssigneeId FROM PermissionSetAssignment WHERE PermissionSet.IsOwnedByProfile = false)`;
|
|
24616
|
+
const criteria = {
|
|
24617
|
+
minDaysInactive,
|
|
24618
|
+
cutoff,
|
|
24619
|
+
userTypeFilter: input2.includeAllUserTypes ? "all" : "Standard"
|
|
24620
|
+
};
|
|
24621
|
+
const detailSelect = `SELECT Id, Name, Username, Profile.Name, LastLoginDate FROM User`;
|
|
24622
|
+
const countQ = await liveQuery(org, `SELECT COUNT() FROM User WHERE ${baseWhere}${antiJoin}`, exec);
|
|
24623
|
+
if (countQ.available) {
|
|
24624
|
+
const detailQ = await liveQuery(org, `${detailSelect} WHERE ${baseWhere}${antiJoin} ORDER BY LastLoginDate ASC NULLS FIRST LIMIT ${limit}`, exec);
|
|
24625
|
+
if (!detailQ.available)
|
|
24626
|
+
return err(UNAVAILABLE_ERROR("User", org, detailQ.reason));
|
|
24627
|
+
const users = detailQ.records.map(toZombieUser);
|
|
24628
|
+
return ok({
|
|
24629
|
+
data: fitZombieUsers({
|
|
24630
|
+
criteria,
|
|
24631
|
+
totalZombies: countQ.total,
|
|
24632
|
+
method: "anti-join",
|
|
24633
|
+
disclosure: ZOMBIE_ACCOUNTS_DISCLOSURE,
|
|
24634
|
+
trust: liveTrust(queriedAt)
|
|
24635
|
+
}, users, void 0),
|
|
24636
|
+
vaultState: livePlaneVaultState(ctx)
|
|
24637
|
+
});
|
|
24638
|
+
}
|
|
24639
|
+
if (isBudgetExhaustedReason(countQ.reason)) {
|
|
24640
|
+
return err({ kind: "invalid-query", message: redactSecrets(countQ.reason ?? "live-query budget exhausted") });
|
|
24641
|
+
}
|
|
24642
|
+
const usersQ = await liveQuery(org, `${detailSelect} WHERE ${baseWhere} ORDER BY LastLoginDate ASC NULLS FIRST LIMIT ${ZOMBIE_CLIENT_DIFF_CAP}`, exec);
|
|
24643
|
+
if (!usersQ.available)
|
|
24644
|
+
return err(UNAVAILABLE_ERROR("User", org, usersQ.reason));
|
|
24645
|
+
const assigneesQ = await liveQuery(org, `SELECT AssigneeId FROM PermissionSetAssignment WHERE PermissionSet.IsOwnedByProfile = false GROUP BY AssigneeId LIMIT ${ZOMBIE_CLIENT_DIFF_CAP}`, exec);
|
|
24646
|
+
if (!assigneesQ.available) {
|
|
24647
|
+
return err(UNAVAILABLE_ERROR("PermissionSetAssignment", org, assigneesQ.reason));
|
|
24648
|
+
}
|
|
24649
|
+
const assigneeIds = new Set(assigneesQ.records.map((r2) => String(r2.AssigneeId ?? "")));
|
|
24650
|
+
const zombies = usersQ.records.map(toZombieUser).filter((u2) => !assigneeIds.has(u2.id) && !assigneeIds.has(u2.id.slice(0, 15)));
|
|
24651
|
+
const scanBounded = usersQ.records.length >= ZOMBIE_CLIENT_DIFF_CAP || assigneesQ.records.length >= ZOMBIE_CLIENT_DIFF_CAP;
|
|
24652
|
+
const extraNote = `Anti-join SOQL was rejected by the org (${redactSecrets(countQ.reason ?? "unknown").slice(0, 120)}); fell back to a client-side diff of two bounded queries.` + (scanBounded ? ` One scan hit its ${ZOMBIE_CLIENT_DIFF_CAP}-row cap, so totalZombies may UNDERCOUNT \u2014 treat it as a lower bound.` : "");
|
|
24653
|
+
return ok({
|
|
24654
|
+
data: fitZombieUsers({
|
|
24655
|
+
criteria,
|
|
24656
|
+
totalZombies: zombies.length,
|
|
24657
|
+
method: "client-diff",
|
|
24658
|
+
disclosure: ZOMBIE_ACCOUNTS_DISCLOSURE,
|
|
24659
|
+
trust: liveTrust(queriedAt)
|
|
24660
|
+
}, zombies.slice(0, limit), extraNote),
|
|
24661
|
+
vaultState: livePlaneVaultState(ctx)
|
|
24662
|
+
});
|
|
24663
|
+
};
|
|
24664
|
+
MAX_MEMBER_ROWS = 500;
|
|
24665
|
+
DEFAULT_MEMBER_ROWS = 100;
|
|
24666
|
+
MEMBERS_BYTE_BUDGET = 36e3;
|
|
24667
|
+
MEMBER_ID_CHUNK = 200;
|
|
24668
|
+
GROUP_MEMBERS_DISCLOSURE = 'Direct membership only by default \u2014 nested public groups and role entries are listed but NOT expanded to users. expandNested: true expands exactly ONE level of nested public groups (stamped expansion: "partial-one-level"); deeper nesting and role-hierarchy subordinates are not enumerated \u2014 never treat this as the full effective membership. Read-only; point-in-time as of queriedAt.';
|
|
24669
|
+
liveGroupMembersInputSchema = liveEnabledSchema.extend({
|
|
24670
|
+
/** Group.DeveloperName or Group.Name (label) — exact match, never a guess. */
|
|
24671
|
+
name: z2.string().min(1),
|
|
24672
|
+
/** What kind of Group `name` names. Default 'auto' matches both Queue and
|
|
24673
|
+
* Regular (public group); an ambiguous name across both is an honest error. */
|
|
24674
|
+
groupType: z2.enum(["Queue", "Regular", "auto"]).optional(),
|
|
24675
|
+
/** Expand exactly ONE level of nested public groups (default false). Role
|
|
24676
|
+
* entries are never expanded. */
|
|
24677
|
+
expandNested: z2.boolean().optional(),
|
|
24678
|
+
/** Max direct GroupMember rows fetched (default 100, hard cap 500); a ~36 KB
|
|
24679
|
+
* byte budget may trim the user page further. `totalDirectMembers` is
|
|
24680
|
+
* always the TRUE count. */
|
|
24681
|
+
limit: z2.number().int().min(1).max(MAX_MEMBER_ROWS).optional(),
|
|
24682
|
+
orgAlias: z2.string().min(1).optional()
|
|
24683
|
+
});
|
|
24684
|
+
idChunks = (ids) => {
|
|
24685
|
+
const chunks = [];
|
|
24686
|
+
for (let i2 = 0; i2 < ids.length; i2 += MEMBER_ID_CHUNK) {
|
|
24687
|
+
chunks.push(ids.slice(i2, i2 + MEMBER_ID_CHUNK));
|
|
24688
|
+
}
|
|
24689
|
+
return chunks;
|
|
24690
|
+
};
|
|
24691
|
+
resolveMemberUsers = async (org, ids, exec) => {
|
|
24692
|
+
const byId = /* @__PURE__ */ new Map();
|
|
24693
|
+
for (const chunk of idChunks(ids)) {
|
|
24694
|
+
const inList = chunk.map((id) => soqlLiteral(id)).join(", ");
|
|
24695
|
+
const q2 = await liveQuery(org, `SELECT Id, Name, Username, IsActive FROM User WHERE Id IN (${inList})`, exec);
|
|
24696
|
+
if (!q2.available)
|
|
24697
|
+
return err(UNAVAILABLE_ERROR("User", org, q2.reason));
|
|
24698
|
+
for (const r2 of q2.records) {
|
|
24699
|
+
const row = r2;
|
|
24700
|
+
const id = String(row.Id ?? "");
|
|
24701
|
+
byId.set(id, {
|
|
24702
|
+
id,
|
|
24703
|
+
name: String(row.Name ?? ""),
|
|
24704
|
+
username: String(row.Username ?? ""),
|
|
24705
|
+
isActive: row.IsActive === true
|
|
24706
|
+
});
|
|
24707
|
+
}
|
|
24708
|
+
}
|
|
24709
|
+
return ok(byId);
|
|
24710
|
+
};
|
|
24711
|
+
renderGroupMembersMarkdown = (data) => {
|
|
24712
|
+
const kind = data.group.type === "Queue" ? "queue" : "public group";
|
|
24713
|
+
const lines = [
|
|
24714
|
+
`### Members of ${kind} \`${data.group.developerName}\``,
|
|
24715
|
+
"",
|
|
24716
|
+
`**${data.totalDirectMembers.toLocaleString("en-US")}** direct member entr${data.totalDirectMembers === 1 ? "y" : "ies"} (${data.users.length} user(s), ${data.nestedGroups.length} nested group(s), ${data.roles.length} role(s) on this page).`
|
|
24717
|
+
];
|
|
24718
|
+
if (data.supportedObjects !== void 0) {
|
|
24719
|
+
lines.push("", data.supportedObjects.length > 0 ? `**Can own:** ${data.supportedObjects.join(", ")}` : "**Can own:** (no QueueSobject rows \u2014 this queue supports no objects)");
|
|
24720
|
+
}
|
|
24721
|
+
if (data.users.length > 0) {
|
|
24722
|
+
lines.push("", "**Users**", "", mdTable(["User", "Username", "Active"], data.users.map((u2) => [u2.name, u2.username, u2.isActive ? "yes" : "no"])));
|
|
24723
|
+
}
|
|
24724
|
+
for (const g2 of data.nestedGroups) {
|
|
24725
|
+
lines.push("", `**Nested group \`${g2.name}\`** (${g2.type}${g2.members === void 0 ? " \u2014 not expanded" : ""})`);
|
|
24726
|
+
if (g2.members !== void 0) {
|
|
24727
|
+
lines.push("", g2.members.length > 0 ? mdTable(["User", "Username", "Active"], g2.members.map((u2) => [u2.name, u2.username, u2.isActive ? "yes" : "no"])) : "(no direct users)");
|
|
24728
|
+
if ((g2.unexpandedNestedCount ?? 0) > 0) {
|
|
24729
|
+
lines.push("", `${g2.unexpandedNestedCount} nested group/role member(s) inside \`${g2.name}\` were NOT expanded (one-level limit).`);
|
|
24730
|
+
}
|
|
24731
|
+
}
|
|
24732
|
+
}
|
|
24733
|
+
if (data.roles.length > 0) {
|
|
24734
|
+
lines.push("", "**Roles** (never expanded to users)", "", mdTable(["Role", "Includes subordinates"], data.roles.map((r2) => [r2.roleName ?? r2.id, r2.includesSubordinates ? "yes" : "no"])));
|
|
24735
|
+
}
|
|
24736
|
+
lines.push("", data.vaultDeclaredMemberCount === null ? "Vault cross-check: no declared member count in the vault for this group (membership is runtime data)." : `Vault cross-check: vault declared **${data.vaultDeclaredMemberCount}** member(s) at last refresh vs **${data.liveDirectMemberCount}** live direct member(s) \u2014 drift: ${data.drift ? "YES" : "no"}.`);
|
|
24737
|
+
if (data.capped) {
|
|
24738
|
+
lines.push("", `Showing ${data.returned} of ${data.totalDirectMembers} direct member(s).`);
|
|
24739
|
+
}
|
|
24740
|
+
lines.push("", data.disclosure, "", renderTrustFooter(data.trust));
|
|
24741
|
+
return lines.join("\n");
|
|
24742
|
+
};
|
|
24743
|
+
fitGroupMembers = (base, allUsers, pageEntryCount, extraNote) => {
|
|
24744
|
+
let slice = allUsers;
|
|
24745
|
+
let byteTrimmed = false;
|
|
24746
|
+
for (; ; ) {
|
|
24747
|
+
const returned = pageEntryCount - (allUsers.length - slice.length);
|
|
24748
|
+
const capped = byteTrimmed || base.totalDirectMembers > pageEntryCount;
|
|
24749
|
+
const noteText = [
|
|
24750
|
+
...extraNote !== void 0 ? [extraNote] : [],
|
|
24751
|
+
...byteTrimmed ? [
|
|
24752
|
+
`User rows trimmed to ${slice.length} to stay within the response size limit; totalDirectMembers (${base.totalDirectMembers}) is the true count.`
|
|
24753
|
+
] : []
|
|
24754
|
+
].join(" ");
|
|
24755
|
+
const shape = {
|
|
24756
|
+
...base,
|
|
24757
|
+
returned,
|
|
24758
|
+
capped,
|
|
24759
|
+
users: slice,
|
|
24760
|
+
...noteText.length > 0 ? { note: noteText } : {}
|
|
24761
|
+
};
|
|
24762
|
+
const rendered = renderGroupMembersMarkdown(shape);
|
|
24763
|
+
const bytes = Buffer.byteLength(JSON.stringify({ ...shape, rendered }), "utf8");
|
|
24764
|
+
if (bytes <= MEMBERS_BYTE_BUDGET || slice.length <= 1) {
|
|
24765
|
+
return { ...shape, rendered };
|
|
24766
|
+
}
|
|
24767
|
+
slice = slice.slice(0, Math.max(1, Math.floor(slice.length * 0.85)));
|
|
24768
|
+
byteTrimmed = true;
|
|
24769
|
+
}
|
|
24770
|
+
};
|
|
24771
|
+
liveGroupMembersHandler = async (ctx, input2, exec = nodeExecFile2) => {
|
|
24772
|
+
const gate = await gateLive(ctx, input2);
|
|
24773
|
+
if (!gate.ok)
|
|
24774
|
+
return gate;
|
|
24775
|
+
const org = gate.value;
|
|
24776
|
+
const queriedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
24777
|
+
const limit = input2.limit ?? DEFAULT_MEMBER_ROWS;
|
|
24778
|
+
const lit = soqlLiteral(input2.name);
|
|
24779
|
+
const typeClause = input2.groupType === void 0 || input2.groupType === "auto" ? `Type IN ('Queue', 'Regular')` : `Type = '${input2.groupType}'`;
|
|
24780
|
+
const resolveQ = await liveQuery(org, `SELECT Id, Name, DeveloperName, Type FROM Group WHERE (DeveloperName = ${lit} OR Name = ${lit}) AND ${typeClause} LIMIT 3`, exec);
|
|
24781
|
+
if (!resolveQ.available)
|
|
24782
|
+
return err(UNAVAILABLE_ERROR("Group", org, resolveQ.reason));
|
|
24783
|
+
if (resolveQ.records.length === 0) {
|
|
24784
|
+
return err({
|
|
24785
|
+
kind: "component-not-found",
|
|
24786
|
+
message: `No queue or public group named '${input2.name}' in the live org (probed Group by exact DeveloperName/Name${input2.groupType && input2.groupType !== "auto" ? `, Type ${input2.groupType}` : ""}). Check the API name \u2014 sfi.resolve can fix typos against the vault.`
|
|
24787
|
+
});
|
|
24788
|
+
}
|
|
24789
|
+
if (resolveQ.records.length > 1) {
|
|
24790
|
+
const kinds = resolveQ.records.map((r2) => String(r2.Type ?? "?")).join(" + ");
|
|
24791
|
+
return err({
|
|
24792
|
+
kind: "invalid-query",
|
|
24793
|
+
message: `'${input2.name}' matches ${resolveQ.records.length} Group records in the live org (${kinds}) \u2014 refusing to guess. Pass groupType ('Queue' | 'Regular') or the exact DeveloperName.`
|
|
24794
|
+
});
|
|
24795
|
+
}
|
|
24796
|
+
const groupRow = resolveQ.records[0];
|
|
24797
|
+
const group = {
|
|
24798
|
+
id: String(groupRow.Id ?? ""),
|
|
24799
|
+
name: String(groupRow.Name ?? groupRow.DeveloperName ?? input2.name),
|
|
24800
|
+
developerName: String(groupRow.DeveloperName ?? input2.name),
|
|
24801
|
+
type: groupRow.Type === "Queue" ? "Queue" : "Regular"
|
|
24802
|
+
};
|
|
24803
|
+
const countQ = await liveQuery(org, `SELECT COUNT() FROM GroupMember WHERE GroupId = '${group.id}'`, exec);
|
|
24804
|
+
if (!countQ.available)
|
|
24805
|
+
return err(UNAVAILABLE_ERROR("GroupMember", org, countQ.reason));
|
|
24806
|
+
const totalDirectMembers = countQ.total;
|
|
24807
|
+
const membersQ = await liveQuery(org, `SELECT Id, UserOrGroupId FROM GroupMember WHERE GroupId = '${group.id}' ORDER BY Id LIMIT ${limit}`, exec);
|
|
24808
|
+
if (!membersQ.available)
|
|
24809
|
+
return err(UNAVAILABLE_ERROR("GroupMember", org, membersQ.reason));
|
|
24810
|
+
const memberIds = membersQ.records.map((r2) => String(r2.UserOrGroupId ?? ""));
|
|
24811
|
+
const directUserIds = memberIds.filter((id) => id.startsWith("005"));
|
|
24812
|
+
const nestedGroupIds = memberIds.filter((id) => id.startsWith("00G"));
|
|
24813
|
+
const unclassified = memberIds.filter((id) => !id.startsWith("005") && !id.startsWith("00G"));
|
|
24814
|
+
let nestedRows = [];
|
|
24815
|
+
if (nestedGroupIds.length > 0) {
|
|
24816
|
+
for (const chunk of idChunks(nestedGroupIds)) {
|
|
24817
|
+
const inList = chunk.map((id) => soqlLiteral(id)).join(", ");
|
|
24818
|
+
const q2 = await liveQuery(org, `SELECT Id, Name, DeveloperName, Type, RelatedId FROM Group WHERE Id IN (${inList})`, exec);
|
|
24819
|
+
if (!q2.available)
|
|
24820
|
+
return err(UNAVAILABLE_ERROR("Group", org, q2.reason));
|
|
24821
|
+
nestedRows = nestedRows.concat(q2.records);
|
|
24822
|
+
}
|
|
24823
|
+
}
|
|
24824
|
+
const roleRows = nestedRows.filter((r2) => String(r2.Type ?? "").startsWith("Role"));
|
|
24825
|
+
const publicNestedRows = nestedRows.filter((r2) => !String(r2.Type ?? "").startsWith("Role"));
|
|
24826
|
+
const roleNameById = /* @__PURE__ */ new Map();
|
|
24827
|
+
const relatedIds = roleRows.map((r2) => String(r2.RelatedId ?? "")).filter((id) => id.length > 0);
|
|
24828
|
+
if (relatedIds.length > 0) {
|
|
24829
|
+
const inList = relatedIds.map((id) => soqlLiteral(id)).join(", ");
|
|
24830
|
+
const q2 = await liveQuery(org, `SELECT Id, Name FROM UserRole WHERE Id IN (${inList})`, exec);
|
|
24831
|
+
if (!q2.available && isBudgetExhaustedReason(q2.reason)) {
|
|
24832
|
+
return err({ kind: "invalid-query", message: redactSecrets(q2.reason ?? "live-query budget exhausted") });
|
|
24833
|
+
}
|
|
24834
|
+
if (q2.available) {
|
|
24835
|
+
for (const r2 of q2.records) {
|
|
24836
|
+
const row = r2;
|
|
24837
|
+
roleNameById.set(String(row.Id ?? ""), String(row.Name ?? ""));
|
|
24838
|
+
}
|
|
24839
|
+
}
|
|
24840
|
+
}
|
|
24841
|
+
const roles = roleRows.map((r2) => {
|
|
24842
|
+
const relatedId = String(r2.RelatedId ?? "");
|
|
24843
|
+
return {
|
|
24844
|
+
id: String(r2.Id ?? ""),
|
|
24845
|
+
roleName: roleNameById.get(relatedId) ?? null,
|
|
24846
|
+
type: String(r2.Type ?? "Role"),
|
|
24847
|
+
includesSubordinates: String(r2.Type ?? "").startsWith("RoleAndSubordinates")
|
|
24848
|
+
};
|
|
24849
|
+
});
|
|
24850
|
+
const expandNested = input2.expandNested === true;
|
|
24851
|
+
const nestedMembersByGroup = /* @__PURE__ */ new Map();
|
|
24852
|
+
if (expandNested && publicNestedRows.length > 0) {
|
|
24853
|
+
const inList = publicNestedRows.map((r2) => soqlLiteral(String(r2.Id ?? ""))).join(", ");
|
|
24854
|
+
const q2 = await liveQuery(org, `SELECT GroupId, UserOrGroupId FROM GroupMember WHERE GroupId IN (${inList}) ORDER BY Id LIMIT ${MAX_MEMBER_ROWS}`, exec);
|
|
24855
|
+
if (!q2.available)
|
|
24856
|
+
return err(UNAVAILABLE_ERROR("GroupMember", org, q2.reason));
|
|
24857
|
+
for (const r2 of q2.records) {
|
|
24858
|
+
const row = r2;
|
|
24859
|
+
const gid = String(row.GroupId ?? "");
|
|
24860
|
+
const list = nestedMembersByGroup.get(gid) ?? [];
|
|
24861
|
+
list.push(String(row.UserOrGroupId ?? ""));
|
|
24862
|
+
nestedMembersByGroup.set(gid, list);
|
|
24863
|
+
}
|
|
24864
|
+
}
|
|
24865
|
+
const nestedUserIds = [...nestedMembersByGroup.values()].flat().filter((id) => id.startsWith("005"));
|
|
24866
|
+
const allUserIds = [.../* @__PURE__ */ new Set([...directUserIds, ...nestedUserIds])];
|
|
24867
|
+
const usersR = await resolveMemberUsers(org, allUserIds, exec);
|
|
24868
|
+
if (!usersR.ok)
|
|
24869
|
+
return usersR;
|
|
24870
|
+
const userById = usersR.value;
|
|
24871
|
+
const toUser = (id) => userById.get(id) ?? userById.get(id.slice(0, 15)) ?? { id, name: "", username: "", isActive: false };
|
|
24872
|
+
const nestedGroups = publicNestedRows.map((r2) => {
|
|
24873
|
+
const id = String(r2.Id ?? "");
|
|
24874
|
+
const base = {
|
|
24875
|
+
id,
|
|
24876
|
+
name: String(r2.DeveloperName ?? r2.Name ?? id),
|
|
24877
|
+
type: String(r2.Type ?? "Regular")
|
|
24878
|
+
};
|
|
24879
|
+
if (!expandNested)
|
|
24880
|
+
return base;
|
|
24881
|
+
const memberIdsOfGroup = nestedMembersByGroup.get(id) ?? [];
|
|
24882
|
+
const userMembers = memberIdsOfGroup.filter((m2) => m2.startsWith("005")).map(toUser);
|
|
24883
|
+
const unexpanded = memberIdsOfGroup.length - userMembers.length;
|
|
24884
|
+
return {
|
|
24885
|
+
...base,
|
|
24886
|
+
members: userMembers,
|
|
24887
|
+
...unexpanded > 0 ? { unexpandedNestedCount: unexpanded } : {}
|
|
24888
|
+
};
|
|
24889
|
+
});
|
|
24890
|
+
let supportedObjects;
|
|
24891
|
+
if (group.type === "Queue") {
|
|
24892
|
+
const q2 = await liveQuery(org, `SELECT SobjectType FROM QueueSobject WHERE QueueId = '${group.id}'`, exec);
|
|
24893
|
+
if (!q2.available && isBudgetExhaustedReason(q2.reason)) {
|
|
24894
|
+
return err({ kind: "invalid-query", message: redactSecrets(q2.reason ?? "live-query budget exhausted") });
|
|
24895
|
+
}
|
|
24896
|
+
if (q2.available) {
|
|
24897
|
+
supportedObjects = q2.records.map((r2) => String(r2.SobjectType ?? "")).filter((s2) => s2.length > 0).sort();
|
|
24898
|
+
}
|
|
24899
|
+
}
|
|
24900
|
+
let vaultDeclaredMemberCount = null;
|
|
24901
|
+
if (ctx.graph !== void 0 && ctx.graph !== null) {
|
|
24902
|
+
const vaultId = `${group.type === "Queue" ? "Queue" : "Group"}:${group.developerName}`;
|
|
24903
|
+
const nodeR = await getNodeById(ctx.graph, vaultId);
|
|
24904
|
+
if (nodeR.ok && nodeR.value !== null) {
|
|
24905
|
+
const declared = nodeR.value.properties["memberCount"];
|
|
24906
|
+
if (typeof declared === "number")
|
|
24907
|
+
vaultDeclaredMemberCount = declared;
|
|
24908
|
+
}
|
|
24909
|
+
}
|
|
24910
|
+
const drift = vaultDeclaredMemberCount !== null && vaultDeclaredMemberCount !== totalDirectMembers;
|
|
24911
|
+
const extraNote = unclassified.length > 0 ? `${unclassified.length} GroupMember row(s) carry an id prefix that is neither User (005) nor Group (00G) \u2014 listed in no bucket, but counted in totalDirectMembers.` : void 0;
|
|
24912
|
+
return ok({
|
|
24913
|
+
data: fitGroupMembers({
|
|
24914
|
+
group,
|
|
24915
|
+
...supportedObjects !== void 0 ? { supportedObjects } : {},
|
|
24916
|
+
totalDirectMembers,
|
|
24917
|
+
nestedGroups,
|
|
24918
|
+
roles,
|
|
24919
|
+
expansion: expandNested ? "partial-one-level" : "none",
|
|
24920
|
+
vaultDeclaredMemberCount,
|
|
24921
|
+
liveDirectMemberCount: totalDirectMembers,
|
|
24922
|
+
drift,
|
|
24923
|
+
disclosure: GROUP_MEMBERS_DISCLOSURE,
|
|
24924
|
+
trust: liveTrust(queriedAt)
|
|
24925
|
+
}, directUserIds.map(toUser), memberIds.length, extraNote),
|
|
24926
|
+
vaultState: livePlaneVaultState(ctx)
|
|
24927
|
+
});
|
|
24928
|
+
};
|
|
24929
|
+
MAX_USER_PERMSET_ROWS = 500;
|
|
24930
|
+
DEFAULT_USER_PERMSET_ROWS = 200;
|
|
24931
|
+
USER_PERMSETS_BYTE_BUDGET = 36e3;
|
|
24932
|
+
USER_PERMSETS_DISCLOSURE = "Live = WHICH grantors this user holds right now (point-in-time as of queriedAt); vault sfi.effective_permissions = WHAT those grantors grant \u2014 pair them for a dual-provenance answer. The PROFILE grants permissions too: it is named here but its contents are not enumerated. Read-only.";
|
|
24933
|
+
liveUserPermsetsInputSchema = liveEnabledSchema.extend({
|
|
24934
|
+
/** Exact Username (preferred — unique) or exact User.Name. An ambiguous name
|
|
24935
|
+
* returns an honest candidate list, never a guess. */
|
|
24936
|
+
user: z2.string().min(1),
|
|
24937
|
+
/** Max assignment rows (default 200, hard cap 500); a ~36 KB byte budget may
|
|
24938
|
+
* trim further. `totalAssignments` is always the TRUE count. */
|
|
24939
|
+
limit: z2.number().int().min(1).max(MAX_USER_PERMSET_ROWS).optional(),
|
|
24940
|
+
orgAlias: z2.string().min(1).optional()
|
|
24941
|
+
});
|
|
24942
|
+
renderUserPermsetsMarkdown = (data) => {
|
|
24943
|
+
const direct = data.directPermsets.length;
|
|
24944
|
+
const via = data.viaGroups.reduce((n2, g2) => n2 + g2.permsets.length, 0);
|
|
24945
|
+
const lines = [
|
|
24946
|
+
`### Live permission-set holdings for ${data.user.name} (\`${data.user.username}\`)`,
|
|
24947
|
+
"",
|
|
24948
|
+
`Profile: **${data.user.profileName ?? "(none)"}**${data.user.isActive ? "" : " \u2014 user is INACTIVE"}.`,
|
|
24949
|
+
"",
|
|
24950
|
+
`**${data.totalAssignments.toLocaleString("en-US")}** assignment(s) (${direct} direct, ${via} via group(s) on this page)` + (data.expiredExcluded > 0 ? `; ${data.expiredExcluded} expired assignment(s) excluded` : "") + "."
|
|
24951
|
+
];
|
|
24952
|
+
const grantRows = (grants) => mdTable(["Permission set", "Label", "Expires"], grants.map((g2) => [g2.permissionSetName, g2.permissionSetLabel ?? "\u2014", g2.expirationDate ?? "\u2014"]));
|
|
24953
|
+
if (data.directPermsets.length > 0) {
|
|
24954
|
+
lines.push("", "**Direct permission sets**", "", grantRows(data.directPermsets));
|
|
24955
|
+
}
|
|
24956
|
+
for (const g2 of data.viaGroups) {
|
|
24957
|
+
lines.push("", `**Via permission set group \`${g2.groupName}\`**`, "", grantRows(g2.permsets));
|
|
24958
|
+
}
|
|
24959
|
+
if (data.capped) {
|
|
24960
|
+
lines.push("", `Showing ${data.returned} of ${data.totalAssignments} assignment(s).`);
|
|
24961
|
+
}
|
|
24962
|
+
lines.push("", data.disclosure, "", renderTrustFooter(data.trust));
|
|
24963
|
+
return lines.join("\n");
|
|
24964
|
+
};
|
|
24965
|
+
fitUserPermsets = (base, allRows) => {
|
|
24966
|
+
let slice = allRows;
|
|
24967
|
+
let byteTrimmed = false;
|
|
24968
|
+
for (; ; ) {
|
|
24969
|
+
const returned = slice.length;
|
|
24970
|
+
const capped = byteTrimmed || base.totalAssignments > returned;
|
|
24971
|
+
const direct = [];
|
|
24972
|
+
const byGroup = /* @__PURE__ */ new Map();
|
|
24973
|
+
for (const row of slice) {
|
|
24974
|
+
const { viaGroup, ...grant } = row;
|
|
24975
|
+
if (viaGroup === null) {
|
|
24976
|
+
direct.push(grant);
|
|
24977
|
+
} else {
|
|
24978
|
+
const list = byGroup.get(viaGroup) ?? [];
|
|
24979
|
+
list.push(grant);
|
|
24980
|
+
byGroup.set(viaGroup, list);
|
|
24981
|
+
}
|
|
24982
|
+
}
|
|
24983
|
+
const shape = {
|
|
24984
|
+
...base,
|
|
24985
|
+
returned,
|
|
24986
|
+
capped,
|
|
24987
|
+
directPermsets: direct,
|
|
24988
|
+
viaGroups: [...byGroup.entries()].map(([groupName, permsets]) => ({ groupName, permsets })),
|
|
24989
|
+
...byteTrimmed ? {
|
|
24990
|
+
note: `Assignment rows trimmed to ${returned} to stay within the response size limit; totalAssignments (${base.totalAssignments}) is the true count.`
|
|
24991
|
+
} : {}
|
|
24992
|
+
};
|
|
24993
|
+
const rendered = renderUserPermsetsMarkdown(shape);
|
|
24994
|
+
const bytes = Buffer.byteLength(JSON.stringify({ ...shape, rendered }), "utf8");
|
|
24995
|
+
if (bytes <= USER_PERMSETS_BYTE_BUDGET || slice.length <= 1) {
|
|
24996
|
+
return { ...shape, rendered };
|
|
24997
|
+
}
|
|
24998
|
+
slice = slice.slice(0, Math.max(1, Math.floor(slice.length * 0.85)));
|
|
24999
|
+
byteTrimmed = true;
|
|
25000
|
+
}
|
|
25001
|
+
};
|
|
25002
|
+
liveUserPermsetsHandler = async (ctx, input2, exec = nodeExecFile2) => {
|
|
25003
|
+
const gate = await gateLive(ctx, input2);
|
|
25004
|
+
if (!gate.ok)
|
|
25005
|
+
return gate;
|
|
25006
|
+
const org = gate.value;
|
|
25007
|
+
const queriedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
25008
|
+
const limit = input2.limit ?? DEFAULT_USER_PERMSET_ROWS;
|
|
25009
|
+
const lit = soqlLiteral(input2.user);
|
|
25010
|
+
const nowLiteral = soqlDateTime(/* @__PURE__ */ new Date());
|
|
25011
|
+
const userSelect = `SELECT Id, Name, Username, IsActive, Profile.Name FROM User`;
|
|
25012
|
+
let userRow;
|
|
25013
|
+
const byUsername = await liveQuery(org, `${userSelect} WHERE Username = ${lit} LIMIT 2`, exec);
|
|
25014
|
+
if (!byUsername.available)
|
|
25015
|
+
return err(UNAVAILABLE_ERROR("User", org, byUsername.reason));
|
|
25016
|
+
if (byUsername.records.length === 1) {
|
|
25017
|
+
userRow = byUsername.records[0];
|
|
25018
|
+
} else {
|
|
25019
|
+
const byName = await liveQuery(org, `${userSelect} WHERE Name = ${lit} LIMIT 5`, exec);
|
|
25020
|
+
if (!byName.available)
|
|
25021
|
+
return err(UNAVAILABLE_ERROR("User", org, byName.reason));
|
|
25022
|
+
if (byName.records.length === 0) {
|
|
25023
|
+
return err({
|
|
25024
|
+
kind: "component-not-found",
|
|
25025
|
+
message: `No user with exact Username or Name '${input2.user}' in the live org. Usernames are unique \u2014 prefer the full Username.`
|
|
25026
|
+
});
|
|
25027
|
+
}
|
|
25028
|
+
if (byName.records.length > 1) {
|
|
25029
|
+
const candidates = byName.records.map((r2) => String(r2.Username ?? "")).filter((u2) => u2.length > 0).join(", ");
|
|
25030
|
+
return err({
|
|
25031
|
+
kind: "invalid-query",
|
|
25032
|
+
message: `'${input2.user}' matches ${byName.records.length} users in the live org \u2014 refusing to guess. Pass the exact Username. Candidates: ${candidates}.`
|
|
25033
|
+
});
|
|
25034
|
+
}
|
|
25035
|
+
userRow = byName.records[0];
|
|
25036
|
+
}
|
|
25037
|
+
const user = {
|
|
25038
|
+
id: String(userRow.Id ?? ""),
|
|
25039
|
+
name: String(userRow.Name ?? ""),
|
|
25040
|
+
username: String(userRow.Username ?? ""),
|
|
25041
|
+
isActive: userRow.IsActive === true,
|
|
25042
|
+
profileName: userRow.Profile?.Name ?? null
|
|
25043
|
+
};
|
|
25044
|
+
const scope = `AssigneeId = '${user.id}' AND PermissionSet.IsOwnedByProfile = false`;
|
|
25045
|
+
const notExpiredClause = ` AND (ExpirationDate = null OR ExpirationDate >= ${nowLiteral})`;
|
|
25046
|
+
const countQ = await liveQuery(org, `SELECT COUNT() FROM PermissionSetAssignment WHERE ${scope}${notExpiredClause}`, exec);
|
|
25047
|
+
if (!countQ.available) {
|
|
25048
|
+
return err(UNAVAILABLE_ERROR("PermissionSetAssignment", org, countQ.reason));
|
|
25049
|
+
}
|
|
25050
|
+
const totalAssignments = countQ.total;
|
|
25051
|
+
const expiredQ = await liveQuery(org, `SELECT COUNT() FROM PermissionSetAssignment WHERE ${scope} AND ExpirationDate < ${nowLiteral}`, exec);
|
|
25052
|
+
const expiredExcluded = expiredQ.available ? expiredQ.total : 0;
|
|
25053
|
+
const detailQ = await liveQuery(org, `SELECT Id, PermissionSet.Name, PermissionSet.Label, PermissionSetGroupId, PermissionSetGroup.DeveloperName, ExpirationDate FROM PermissionSetAssignment WHERE ${scope}${notExpiredClause} ORDER BY Id LIMIT ${limit}`, exec);
|
|
25054
|
+
if (!detailQ.available) {
|
|
25055
|
+
return err(UNAVAILABLE_ERROR("PermissionSetAssignment", org, detailQ.reason));
|
|
25056
|
+
}
|
|
25057
|
+
const rows = detailQ.records.map((r2) => ({
|
|
25058
|
+
assignmentId: String(r2.Id ?? ""),
|
|
25059
|
+
permissionSetName: String(r2.PermissionSet?.Name ?? ""),
|
|
25060
|
+
permissionSetLabel: r2.PermissionSet?.Label ?? null,
|
|
25061
|
+
expirationDate: r2.ExpirationDate ?? null,
|
|
25062
|
+
viaGroup: r2.PermissionSetGroupId ? r2.PermissionSetGroup?.DeveloperName ?? String(r2.PermissionSetGroupId) : null
|
|
25063
|
+
})).sort((a2, b2) => a2.permissionSetName.localeCompare(b2.permissionSetName));
|
|
25064
|
+
return ok({
|
|
25065
|
+
data: fitUserPermsets({
|
|
25066
|
+
user,
|
|
25067
|
+
totalAssignments,
|
|
25068
|
+
expiredExcluded,
|
|
25069
|
+
disclosure: USER_PERMSETS_DISCLOSURE,
|
|
25070
|
+
trust: liveTrust(queriedAt)
|
|
25071
|
+
}, rows),
|
|
25072
|
+
vaultState: livePlaneVaultState(ctx)
|
|
25073
|
+
});
|
|
25074
|
+
};
|
|
24187
25075
|
}
|
|
24188
25076
|
});
|
|
24189
25077
|
|
|
@@ -25151,25 +26039,50 @@ var init_intent_router = __esm({
|
|
|
25151
26039
|
/\bhow\s+many\s+users?\b.*\bon\b.*\bprofiles?\b/
|
|
25152
26040
|
]
|
|
25153
26041
|
},
|
|
26042
|
+
{
|
|
26043
|
+
// ENGINE-ARC §2c (NEW arm): "zombie accounts" — active users with login
|
|
26044
|
+
// access but ZERO permission-set/PSG assignments. This is the User × PSA
|
|
26045
|
+
// anti-join (sfi.live_zombie_accounts), an explicit DELTA over
|
|
26046
|
+
// live_inactive_users: dormancy-only phrasings ("who hasn't logged in")
|
|
26047
|
+
// carry no permission-set-absence noun and fall through to inactive-users.
|
|
26048
|
+
// Every pattern requires either the literal "zombie" or a
|
|
26049
|
+
// no/zero/without + permission-set frame, so genuine holder-roster asks
|
|
26050
|
+
// ("which users have permission set X" — no negative) never land here.
|
|
26051
|
+
// Sits BEFORE profile-user-roster so "users with nothing assigned beyond
|
|
26052
|
+
// their profile" is not stolen by the profile-roster grammar.
|
|
26053
|
+
intent: "zombie-accounts",
|
|
26054
|
+
plane: "live",
|
|
26055
|
+
tools: ["sfi.live_zombie_accounts"],
|
|
26056
|
+
liveRequired: true,
|
|
26057
|
+
needsResolve: false,
|
|
26058
|
+
reason: 'Active users with NO permission-set/PSG assignments is a live User \xD7 PermissionSetAssignment anti-join (live_zombie_accounts). Note: a "zombie" still holds everything its PROFILE grants \u2014 the tool discloses that.',
|
|
26059
|
+
patterns: [
|
|
26060
|
+
/\bzombie\s+(?:accounts?|users?)\b/,
|
|
26061
|
+
// "which active users have no/zero permission sets (assigned)?" /
|
|
26062
|
+
// "users with zero permission set or group assignments" / "can log in
|
|
26063
|
+
// but have no permission set assignments"
|
|
26064
|
+
/\b(?:users?|accounts?)\b[^.?!]{0,50}\b(?:no|zero|without|not\s+any)\b[^.?!]{0,15}\bpermission\s+sets?\b/,
|
|
26065
|
+
// "which users have nothing assigned beyond their profile?"
|
|
26066
|
+
/\bnothing\s+assigned\s+beyond\b[^.?!]{0,25}\bprofiles?\b/,
|
|
26067
|
+
// "login access but no perm sets" (perm-set abbreviation variant)
|
|
26068
|
+
/\b(?:no|zero|without)\b[^.?!]{0,15}\bperm\s+sets?\b/
|
|
26069
|
+
]
|
|
26070
|
+
},
|
|
25154
26071
|
{
|
|
25155
26072
|
// "list everyone with the X profile" is a USER ROSTER ask: user-to-profile
|
|
25156
26073
|
// assignment is runtime User-record state, not vault metadata — the schema
|
|
25157
26074
|
// list rule used to claim it via "list ... profiles" and answer with the
|
|
25158
|
-
// Profile METADATA catalog (eval family D).
|
|
25159
|
-
//
|
|
25160
|
-
//
|
|
25161
|
-
//
|
|
26075
|
+
// Profile METADATA catalog (eval family D). ENGINE-ARC §4: the name-by-name
|
|
26076
|
+
// roster IS now built — sfi.live_permset_holders (kind:'profile') lists the
|
|
26077
|
+
// users live; live_group_count keeps the count side and live_inactive_users
|
|
26078
|
+
// the login-activity side. The old partial-answer gap block is DELETED.
|
|
25162
26079
|
intent: "profile-user-roster",
|
|
25163
26080
|
plane: "live",
|
|
25164
|
-
tools: ["sfi.live_group_count", "sfi.live_inactive_users"],
|
|
26081
|
+
tools: ["sfi.live_permset_holders", "sfi.live_group_count", "sfi.live_inactive_users"],
|
|
25165
26082
|
liveRequired: true,
|
|
25166
26083
|
needsResolve: false,
|
|
25167
|
-
reason: "Which users hold a profile is runtime User-record state:
|
|
25168
|
-
|
|
25169
|
-
category: "profile-user-roster",
|
|
25170
|
-
note: "Partial answer: live_group_count returns user COUNTS per profile and live_inactive_users lists dormant users, but a full name-by-name user roster per profile is not a built capability yet."
|
|
25171
|
-
},
|
|
25172
|
-
suggestArgs: () => ({ objectApiName: "User", groupByField: "ProfileId" }),
|
|
26084
|
+
reason: "Which users hold a profile is runtime User-record state: live_permset_holders (kind: profile) lists the roster name-by-name from the live org; live_group_count covers per-profile counts and live_inactive_users the login-activity side.",
|
|
26085
|
+
suggestArgs: () => ({ kind: "profile" }),
|
|
25173
26086
|
patterns: [
|
|
25174
26087
|
/\b(list|show|who\s+are)\b[^.?!]{0,20}\b(everyone|everybody|all\s+(the\s+)?users?|the\s+users?|people)\b[^.?!]{0,40}\b(with|on|assigned|holding|having)\b[^.?!]{0,40}\bprofile\b/,
|
|
25175
26088
|
/\b(which|what)\s+users?\b[^.?!]{0,40}\b(have|hold|are\s+on|with|assigned)\b[^.?!]{0,40}\bprofile\b/,
|
|
@@ -25393,6 +26306,41 @@ var init_intent_router = __esm({
|
|
|
25393
26306
|
/\bstandard\s+objects?\b.*\b(at\s+least\s+one\s+)?(apex\s+)?triggers?\b/
|
|
25394
26307
|
]
|
|
25395
26308
|
},
|
|
26309
|
+
{
|
|
26310
|
+
// ENGINE-ARC §4 (NEW arm): runtime queue / public-group MEMBERSHIP asks
|
|
26311
|
+
// ("who's in the Support queue", "members of the X public group", "can the
|
|
26312
|
+
// X queue own Case"). GroupMember rows are runtime state the vault does not
|
|
26313
|
+
// model — sfi.live_group_members reads them live (polymorphic user/group/
|
|
26314
|
+
// role split, QueueSobject supported objects, vault-drift cross-check).
|
|
26315
|
+
// Sits BEFORE group-count, which used to claim "who's in the X queue" with
|
|
26316
|
+
// a generic GROUP BY count; the roster tool now owns that vocabulary.
|
|
26317
|
+
// Boundary notes: "which queues are EMPTY" (declared-metadata hygiene
|
|
26318
|
+
// sweep) stays on empty-queues-groups; the neutral "which queues does
|
|
26319
|
+
// X_Queue route to and who are the members" stays on queue-membership
|
|
26320
|
+
// (vault get_component — declared members); a PSG "member of" ask never
|
|
26321
|
+
// lands here (patterns require the literal queue / public group noun).
|
|
26322
|
+
intent: "queue-group-member-roster",
|
|
26323
|
+
plane: "live",
|
|
26324
|
+
tools: ["sfi.live_group_members"],
|
|
26325
|
+
liveRequired: true,
|
|
26326
|
+
needsResolve: false,
|
|
26327
|
+
reason: "Who is actually IN a queue or public group is runtime GroupMember data \u2014 live_group_members lists the current roster (users, nested groups, roles) from the live org and cross-checks the vault-declared member count.",
|
|
26328
|
+
patterns: [
|
|
26329
|
+
// "who's / who is / who are ... in|on|member of ... the X queue|public group"
|
|
26330
|
+
/\bwho(?:'?s|\s+is|\s+are)?\b[^.?!]{0,30}\b(?:in|on|member\s+of)\b[^.?!]{0,40}\b(?:queues?|public\s+groups?)\b/,
|
|
26331
|
+
// "which users are in the Support queue?"
|
|
26332
|
+
/\b(?:which|what)\s+users?\b[^.?!]{0,30}\b(?:in|on|belong\s+to)\b[^.?!]{0,40}\b(?:queues?|public\s+groups?)\b/,
|
|
26333
|
+
// "members of the X queue" / "list the members of the Y public group"
|
|
26334
|
+
/\bmembers?\s+of\b[^.?!]{0,40}\b(?:queues?|public\s+groups?)\b/,
|
|
26335
|
+
// "the current membership of the X queue"
|
|
26336
|
+
/\bmembership\s+of\b[^.?!]{0,40}\b(?:queues?|public\s+groups?)\b/,
|
|
26337
|
+
// q267: "can the ADA_Team_Queue own Case records?" / "what objects does
|
|
26338
|
+
// the X queue support?" — QueueSobject supported objects.
|
|
26339
|
+
/\bcan\s+(?:the\s+)?\S+\s*queue\s+own\b/,
|
|
26340
|
+
/\bqueues?\b[^.?!]{0,30}\b(?:own|support)\b[^.?!]{0,30}\b(?:case|lead|objects?|records?)\b/,
|
|
26341
|
+
/\bobjects?\s+(?:does|can)\b[^.?!]{0,40}\bqueue\s+(?:support|own)\b/
|
|
26342
|
+
]
|
|
26343
|
+
},
|
|
25396
26344
|
{
|
|
25397
26345
|
intent: "group-count",
|
|
25398
26346
|
plane: "live",
|
|
@@ -25412,10 +26360,10 @@ var init_intent_router = __esm({
|
|
|
25412
26360
|
// "how many".
|
|
25413
26361
|
/\bcount\b[^.?!]{0,60}\bgrouped?\s+by\b/,
|
|
25414
26362
|
/\bcount\s+of\b[^.?!]{0,60}\bby\b/,
|
|
25415
|
-
/\bcount\b[^.?!]{0,60}\brecords?\s+by\b
|
|
25416
|
-
// "who's in the ADA Team Queue"
|
|
25417
|
-
//
|
|
25418
|
-
|
|
26363
|
+
/\bcount\b[^.?!]{0,60}\brecords?\s+by\b/
|
|
26364
|
+
// NOTE (ENGINE-ARC §4): "who's in the ADA Team Queue" used to land here
|
|
26365
|
+
// as a generic GROUP BY count; the queue-group-member-roster arm ABOVE
|
|
26366
|
+
// now owns that vocabulary with the dedicated live_group_members roster.
|
|
25419
26367
|
]
|
|
25420
26368
|
},
|
|
25421
26369
|
{
|
|
@@ -25892,6 +26840,58 @@ var init_intent_router = __esm({
|
|
|
25892
26840
|
/\brecord\s?type\s?id\b[^.?!]{0,60}\b(?:pick|select|choose|create|use)\b/
|
|
25893
26841
|
]
|
|
25894
26842
|
},
|
|
26843
|
+
{
|
|
26844
|
+
// ENGINE-ARC §4 (NEW arm): the REVERSE user→grantors direction — "what
|
|
26845
|
+
// permission sets does USER Jane hold". A different contract from the
|
|
26846
|
+
// holder-roster direction (permset-user-roster below) and from the vault
|
|
26847
|
+
// GRANTS direction: PermissionSetAssignment rows for one assignee are
|
|
26848
|
+
// runtime state, so sfi.live_user_permsets reads them live (direct sets vs
|
|
26849
|
+
// via-PSG, expirations, profile named). The ordered pair with vault
|
|
26850
|
+
// sfi.effective_permissions is deliberate DUAL PROVENANCE: live = WHICH
|
|
26851
|
+
// grantors she holds; vault = WHAT those grantors grant. Sits BEFORE
|
|
26852
|
+
// effective-permissions, which used to first-match these phrasings and
|
|
26853
|
+
// could only describe grants, never the user's actual assignments.
|
|
26854
|
+
intent: "user-permset-holdings",
|
|
26855
|
+
plane: "live",
|
|
26856
|
+
tools: ["sfi.live_user_permsets", "sfi.effective_permissions"],
|
|
26857
|
+
liveRequired: true,
|
|
26858
|
+
needsResolve: false,
|
|
26859
|
+
reason: "What a named USER holds (their PermissionSetAssignment rows) is runtime state: live_user_permsets lists the direct sets and via-PSG assignments from the live org; effective_permissions then expands what those grantors grant (dual provenance).",
|
|
26860
|
+
patterns: [
|
|
26861
|
+
// Permission sets assigned to a named user (moved here from
|
|
26862
|
+
// effective-permissions — the router used to first-match these to the
|
|
26863
|
+
// vault GRANTS tool, which cannot know a user's assignments).
|
|
26864
|
+
/\bpermission\s+sets?\b.*\bassigned\b.*\buser\b/,
|
|
26865
|
+
/\bwhat\s+permission\s+sets?\b.*\bassigned\b/,
|
|
26866
|
+
// "what permission sets does Jane Doe have / hold" — requires the
|
|
26867
|
+
// PERMISSION SET(S) noun phrase, so "what permissions does the X profile
|
|
26868
|
+
// have" (grants direction) stays on effective-permissions. The
|
|
26869
|
+
// `(?!\s+group)` lookahead keeps "which permission set GROUPS are
|
|
26870
|
+
// assigned to nobody" on the unassigned-permset-groups hygiene arm.
|
|
26871
|
+
/\b(?:what|which)\s+(?:permission\s+sets?|perm\s+sets?)(?!\s+group)\b[^.?!]{0,20}\b(?:does|do|is|are)\b[^.?!]{0,50}\b(?:have|hold|holds|assigned|carry)\b/,
|
|
26872
|
+
// "which permission set groups is <UserName> a member of?" — anchored on
|
|
26873
|
+
// the literal "member of" so "which PSGs are assigned to NOBODY" (the
|
|
26874
|
+
// unassigned-permset-groups hygiene ask, a later rule) is never stolen.
|
|
26875
|
+
// [^?!] (not [^.?!]): usernames are email-shaped and contain dots.
|
|
26876
|
+
/\bwhich\s+permission\s+set\s+groups?\s+(?:is|are)\b[^?!]{0,60}\bmember\s+of\b/,
|
|
26877
|
+
// "which permission sets is jane.doe@example.com assigned right now?" —
|
|
26878
|
+
// the grantee token sits between is/are and "assigned"; \S+ spans the
|
|
26879
|
+
// email-shaped username that clause-bounded classes cannot.
|
|
26880
|
+
/\bpermission\s+sets?\s+(?:is|are)\s+\S+(?:\s+\S+)?\s+assigned\b/,
|
|
26881
|
+
// "what does the user Jane hold — direct sets and via groups?"
|
|
26882
|
+
/\bwhat\s+does\s+(?:the\s+)?user\b[^.?!]{0,40}\bhold\b/,
|
|
26883
|
+
// "show me every permission set assigned to <UserName>" — the grantee is
|
|
26884
|
+
// named after "assigned to" (holder-direction phrasings name the SET
|
|
26885
|
+
// after the users noun and never match this). The lookahead excludes
|
|
26886
|
+
// "assigned to NOBODY / no one / anyone" — that is the vault
|
|
26887
|
+
// unassigned-permsets hygiene sweep, not a user's holdings.
|
|
26888
|
+
/\b(?:every|all|each)\s+permission\s+sets?\s+assigned\s+to\s+(?!nobody\b|no\s+one\b|noone\b|anyone\b|anybody\b)/,
|
|
26889
|
+
// "list <UserName>'s permission set and PSG assignments"
|
|
26890
|
+
/\S+'s\s+permission\s+sets?\b/,
|
|
26891
|
+
// "does <UserName> have any expiring permission set assignments?"
|
|
26892
|
+
/\bexpir\w+\s+permission\s+set\s+assignments?\b/
|
|
26893
|
+
]
|
|
26894
|
+
},
|
|
25895
26895
|
{
|
|
25896
26896
|
intent: "effective-permissions",
|
|
25897
26897
|
plane: "vault",
|
|
@@ -25905,9 +26905,10 @@ var init_intent_router = __esm({
|
|
|
25905
26905
|
// "what permissions does the Sales User profile have" — a top
|
|
25906
26906
|
// baseline-300 unrouted cluster (P14-ROUTER-goldset-expand).
|
|
25907
26907
|
/\bwhat\s+permissions?\s+(does|do)\b.*\b(profiles?|permission\s+sets?|users?)\b/,
|
|
25908
|
-
//
|
|
25909
|
-
|
|
25910
|
-
|
|
26908
|
+
// NOTE (ENGINE-ARC §4): the "permission sets assigned to a user"
|
|
26909
|
+
// phrasings moved to the user-permset-holdings arm ABOVE — a user's
|
|
26910
|
+
// actual assignments are runtime state (live_user_permsets); this vault
|
|
26911
|
+
// arm keeps the GRANTS direction only.
|
|
25911
26912
|
// REACH (permissions/access cluster): "does/can the <Named> profile /
|
|
25912
26913
|
// <Named> perm set have|give|grant|read|edit|create|delete|access|see|
|
|
25913
26914
|
// change <object>". These name a SPECIFIC granter (a profile/permission
|
|
@@ -26410,70 +27411,81 @@ var init_intent_router = __esm({
|
|
|
26410
27411
|
]
|
|
26411
27412
|
},
|
|
26412
27413
|
{
|
|
26413
|
-
// HONEST GAP
|
|
26414
|
-
//
|
|
26415
|
-
//
|
|
26416
|
-
//
|
|
26417
|
-
//
|
|
26418
|
-
//
|
|
27414
|
+
// PARTIAL FLIP (ENGINE-ARC §4, was HONEST GAP P0b): PermissionSetGroup
|
|
27415
|
+
// ASSIGNMENT is now answerable live per group — sfi.live_permset_holders
|
|
27416
|
+
// (kind:'permissionSetGroup') returns the true holder count and roster for
|
|
27417
|
+
// a NAMED PSG, so "is PSG X assigned to nobody" is a live zero-holder
|
|
27418
|
+
// check. What is still unbuilt is an enumerate-ALL mode (vault PSG list
|
|
27419
|
+
// minus a live GROUP BY PermissionSetGroupId sweep in one call) — the gap
|
|
27420
|
+
// note is DOWNGRADED to that partial until Deferred-2 ships. Must sit
|
|
27421
|
+
// AFTER unassigned-permsets: the lookahead above already refuses
|
|
27422
|
+
// "permission set groups", so this rule catches it on the fall-through.
|
|
26419
27423
|
intent: "unassigned-permset-groups",
|
|
26420
|
-
plane: "
|
|
26421
|
-
tools: [],
|
|
26422
|
-
liveRequired:
|
|
27424
|
+
plane: "live",
|
|
27425
|
+
tools: ["sfi.live_permset_holders"],
|
|
27426
|
+
liveRequired: true,
|
|
26423
27427
|
needsResolve: false,
|
|
26424
|
-
reason: "
|
|
27428
|
+
reason: "Whether a permission set group is assigned to anyone is runtime PermissionSetAssignment state: live_permset_holders (kind: permissionSetGroup) returns the true holder count per named PSG.",
|
|
26425
27429
|
gap: {
|
|
26426
27430
|
category: "unassigned-permset-groups",
|
|
26427
|
-
note: "
|
|
27431
|
+
note: "Partial: live_permset_holders checks ONE named permission set group at a time (a zero effectiveTotal = unassigned). A single-call sweep of ALL PSGs is not built yet \u2014 enumerate the vault PSG list first, then check candidates live. Do not substitute unassigned_permission_sets, which covers PermissionSet only."
|
|
26428
27432
|
},
|
|
27433
|
+
suggestArgs: () => ({ kind: "permissionSetGroup" }),
|
|
26429
27434
|
patterns: [
|
|
26430
27435
|
/\b(unassigned|unused|empty)\b[^.?!]{0,40}\bpermission\s+set\s+groups?\b/,
|
|
26431
27436
|
/\bpermission\s+set\s+groups?\b[^.?!]{0,40}\b(no\s+one|nobody|unassigned|unused|assigned\s+to\s+nobody)\b/
|
|
26432
27437
|
]
|
|
26433
27438
|
},
|
|
26434
27439
|
{
|
|
26435
|
-
// HONEST GAP
|
|
26436
|
-
// holder ROSTER — PermissionSetAssignment is
|
|
26437
|
-
// vault does not model, and
|
|
26438
|
-
//
|
|
26439
|
-
//
|
|
26440
|
-
//
|
|
26441
|
-
//
|
|
26442
|
-
//
|
|
27440
|
+
// FULL FLIP (ENGINE-ARC §4, was HONEST GAP eval family D): "which USERS
|
|
27441
|
+
// have permission set X" is a holder ROSTER — PermissionSetAssignment is
|
|
27442
|
+
// runtime assignment data the vault does not model, and it is now answered
|
|
27443
|
+
// LIVE by sfi.live_permset_holders (kinds permissionSet | permissionSetGroup
|
|
27444
|
+
// | profile | auto; PSG-trap-aware: direct holders vs via-group holders,
|
|
27445
|
+
// deduped effectiveTotal). The gap block is DELETED. The
|
|
27446
|
+
// do-not-substitute warning stays in `reason`: effective_permissions /
|
|
27447
|
+
// object_access_audit describe what a permission set GRANTS, not who HOLDS
|
|
27448
|
+
// it. The reverse direction ("what permission sets does user X have") is
|
|
27449
|
+
// the user-permset-holdings arm (live_user_permsets).
|
|
26443
27450
|
intent: "permset-user-roster",
|
|
26444
|
-
plane: "
|
|
26445
|
-
tools: [],
|
|
26446
|
-
liveRequired:
|
|
27451
|
+
plane: "live",
|
|
27452
|
+
tools: ["sfi.live_permset_holders"],
|
|
27453
|
+
liveRequired: true,
|
|
26447
27454
|
needsResolve: false,
|
|
26448
|
-
reason: "Which users hold a permission set
|
|
26449
|
-
gap: {
|
|
26450
|
-
category: "permset-user-roster",
|
|
26451
|
-
note: 'PermissionSetAssignment modeling is not built yet, so "which users have permission set X" cannot be answered. Do not substitute effective_permissions or object_access_audit \u2014 they describe what a permission set GRANTS, not who holds it.'
|
|
26452
|
-
},
|
|
27455
|
+
reason: "Which users hold a permission set is runtime PermissionSetAssignment state: live_permset_holders lists the roster live, including holders via permission set groups. Do not substitute effective_permissions or object_access_audit \u2014 they describe what a permission set GRANTS, not who holds it.",
|
|
26453
27456
|
patterns: [
|
|
26454
|
-
|
|
26455
|
-
|
|
26456
|
-
|
|
26457
|
-
|
|
27457
|
+
// "perm set(s)" / "permset(s)" ride along with the full "permission
|
|
27458
|
+
// set(s)" spelling — q1213 ("who has the View_Fraud_Score_Component
|
|
27459
|
+
// perm set", the design doc's grounding miss) uses the abbreviation,
|
|
27460
|
+
// and the user-permset-holdings arm above already accepts it.
|
|
27461
|
+
/\b(which|what|list)\s+users?\b[^.?!]{0,40}\b(have|hold|with|assigned)\b[^.?!]{0,40}\b(?:permission\s+sets?|perm\s+sets?|permsets?)\b/,
|
|
27462
|
+
/\bwho\s+(has|holds|is\s+assigned)\b[^.?!]{0,50}\b(?:permission\s+sets?|perm\s+sets?|permsets?)\b/,
|
|
27463
|
+
/\b(everyone|everybody|all\s+users?)\b[^.?!]{0,30}\bwith\b[^.?!]{0,40}\b(?:permission\s+sets?|perm\s+sets?|permsets?)\b/,
|
|
27464
|
+
/\busers?\b[^.?!]{0,30}\bassigned\b[^.?!]{0,30}\b(?:permission\s+sets?|perm\s+sets?|permsets?)\b/
|
|
26458
27465
|
]
|
|
26459
27466
|
},
|
|
26460
27467
|
{
|
|
26461
|
-
// HONEST GAP
|
|
26462
|
-
// permission / the Y role" is a
|
|
26463
|
-
//
|
|
26464
|
-
//
|
|
26465
|
-
//
|
|
26466
|
-
//
|
|
26467
|
-
//
|
|
27468
|
+
// PARTIAL FLIP (ENGINE-ARC §4, was HONEST GAP round-2 q1948/q1559):
|
|
27469
|
+
// "which PSG grants the X custom permission / the Y role" is a
|
|
27470
|
+
// PermissionSetGroup COMPOSITION lookup. live_permset_holders now surfaces
|
|
27471
|
+
// PSG composition live via PermissionSetGroupComponent (probing a
|
|
27472
|
+
// permission SET reports which PSGs contain it), so the roster/containment
|
|
27473
|
+
// half is answerable. The 2-hop chain "which PSG grants custom permission
|
|
27474
|
+
// X" (custom permission → permission set → PSG, q1916) remains an honest
|
|
27475
|
+
// gap — the slimmed note below keeps it — and roles are not granted by
|
|
27476
|
+
// PSGs at all, so substituting object_access_audit / effective_permissions
|
|
27477
|
+
// would still be confidently wrong. Requires the GROUP noun: "which
|
|
27478
|
+
// permission SETS grant edit on X" (no `group`) stays on the real
|
|
27479
|
+
// field/effective-permissions routes.
|
|
26468
27480
|
intent: "permset-group-grants",
|
|
26469
|
-
plane: "
|
|
26470
|
-
tools: [],
|
|
26471
|
-
liveRequired:
|
|
27481
|
+
plane: "live",
|
|
27482
|
+
tools: ["sfi.live_permset_holders"],
|
|
27483
|
+
liveRequired: true,
|
|
26472
27484
|
needsResolve: false,
|
|
26473
|
-
reason: "
|
|
27485
|
+
reason: "PSG composition is runtime state: live_permset_holders reads PermissionSetGroupComponent live (probe the permission SET to see which PSGs contain it). Roles are never granted by PSGs.",
|
|
26474
27486
|
gap: {
|
|
26475
27487
|
category: "permset-group-grants",
|
|
26476
|
-
note: '
|
|
27488
|
+
note: 'Partial: live_permset_holders surfaces which PSGs CONTAIN a given permission set (PermissionSetGroupComponent). The 2-hop chain "which PSG grants custom permission X" (custom permission \u2192 permission set \u2192 PSG) is still unbuilt \u2014 resolve the custom permission to its granting permission sets in the vault first, then probe each set live. Do not substitute effective_permissions or object_access_audit for the PSG-composition step.'
|
|
26477
27489
|
},
|
|
26478
27490
|
patterns: [
|
|
26479
27491
|
// Tempered verb→grants gap: "which permission set groups REFERENCE a
|
|
@@ -26488,11 +27500,16 @@ var init_intent_router = __esm({
|
|
|
26488
27500
|
},
|
|
26489
27501
|
{
|
|
26490
27502
|
intent: "empty-queues-groups",
|
|
26491
|
-
|
|
26492
|
-
|
|
27503
|
+
// ENGINE-ARC §4: vault scan stays PRIMARY; sfi.live_group_members is
|
|
27504
|
+
// appended as the optional runtime-verification secondary (declared
|
|
27505
|
+
// metadata can drift from runtime GroupMember rows — the live tool
|
|
27506
|
+
// measures that drift per queue/group). Hybrid like unused-fields:
|
|
27507
|
+
// liveRequired stays false, the vault half answers alone.
|
|
27508
|
+
plane: "hybrid",
|
|
27509
|
+
tools: ["sfi.empty_queues_and_groups", "sfi.live_group_members"],
|
|
26493
27510
|
liveRequired: false,
|
|
26494
27511
|
needsResolve: false,
|
|
26495
|
-
reason: "Queues / public groups with no members \u2014 vault membership scan.",
|
|
27512
|
+
reason: "Queues / public groups with no members \u2014 vault membership scan first; live_group_members optionally verifies a candidate against runtime GroupMember rows (declared metadata can drift).",
|
|
26496
27513
|
patterns: [
|
|
26497
27514
|
/\b(empty|unused)\b.*\b(queues?|groups?)\b/,
|
|
26498
27515
|
/\b(queues?|public\s+groups?)\b.*\b(empty|no\s+members?|unused)\b/,
|
|
@@ -32205,6 +33222,10 @@ var init_capabilities = __esm({
|
|
|
32205
33222
|
"sfi.live_org_limits",
|
|
32206
33223
|
"sfi.live_inactive_users",
|
|
32207
33224
|
"sfi.live_license_usage",
|
|
33225
|
+
"sfi.live_permset_holders",
|
|
33226
|
+
"sfi.live_zombie_accounts",
|
|
33227
|
+
"sfi.live_group_members",
|
|
33228
|
+
"sfi.live_user_permsets",
|
|
32208
33229
|
"sfi.live_report_usage",
|
|
32209
33230
|
"sfi.live_folder_access",
|
|
32210
33231
|
"sfi.live_email_template_usage",
|
|
@@ -32305,6 +33326,10 @@ var init_capabilities = __esm({
|
|
|
32305
33326
|
"sfi.live_org_limits",
|
|
32306
33327
|
"sfi.live_inactive_users",
|
|
32307
33328
|
"sfi.live_license_usage",
|
|
33329
|
+
"sfi.live_permset_holders",
|
|
33330
|
+
"sfi.live_zombie_accounts",
|
|
33331
|
+
"sfi.live_group_members",
|
|
33332
|
+
"sfi.live_user_permsets",
|
|
32308
33333
|
"sfi.live_report_usage",
|
|
32309
33334
|
"sfi.live_folder_access",
|
|
32310
33335
|
"sfi.live_email_template_usage",
|
|
@@ -34344,19 +35369,72 @@ var init_component_history = __esm({
|
|
|
34344
35369
|
}
|
|
34345
35370
|
});
|
|
34346
35371
|
|
|
35372
|
+
// ../mcp/dist/src/tools/vault-assignment-disclosure.js
|
|
35373
|
+
var USER_ASSIGNMENT_NOT_IN_VAULT, PERMSET_INTERSECTION_NOT_AVAILABLE, SHARING_USER_ENUMERATION_NOT_AVAILABLE, ASSIGNMENT_DATA_LIVE_TOOLS, userAssignmentUnavailable;
|
|
35374
|
+
var init_vault_assignment_disclosure = __esm({
|
|
35375
|
+
"../mcp/dist/src/tools/vault-assignment-disclosure.js"() {
|
|
35376
|
+
"use strict";
|
|
35377
|
+
init_src3();
|
|
35378
|
+
USER_ASSIGNMENT_NOT_IN_VAULT = "User and PermissionSetAssignment data are not retrieved into this vault \u2014 which users hold a profile, permission set, or group membership cannot be answered offline. It is answerable via the live plane: run `sfi.live_permset_holders` (who holds a permission set / PSG / profile), `sfi.live_group_members` (who is in a queue / public group), or `sfi.live_user_permsets` (what a named user holds) \u2014 read-only, consent-gated \u2014 or refresh with a PermissionSetAssignment facts capture for offline holder counts.";
|
|
35379
|
+
PERMSET_INTERSECTION_NOT_AVAILABLE = "Which users have multiple permission sets assigned requires PermissionSetAssignment rows (user assignment data not in vault). This tool can list each permission set structurally but cannot name users with both assignments offline. It is answerable via the live plane: `sfi.live_permset_holders` (read-only, consent-gated) lists the holders of each set for the intersection.";
|
|
35380
|
+
SHARING_USER_ENUMERATION_NOT_AVAILABLE = "Sharing rules name group/role targets, but User records and group membership to individual users are not fully available offline \u2014 cannot enumerate every user who effectively receives Edit from a group-based sharing rule without live User / GroupMember / PermissionSetAssignment data. It is answerable via the live plane: `sfi.live_group_members` (read-only, consent-gated) enumerates the current members of a named queue / public group.";
|
|
35381
|
+
ASSIGNMENT_DATA_LIVE_TOOLS = [
|
|
35382
|
+
"sfi.live_permset_holders",
|
|
35383
|
+
"sfi.live_user_permsets",
|
|
35384
|
+
"sfi.live_group_members",
|
|
35385
|
+
"sfi.live_zombie_accounts"
|
|
35386
|
+
];
|
|
35387
|
+
userAssignmentUnavailable = (ctx) => {
|
|
35388
|
+
const userCov = summarizeCoverage(ctx.manifest, ["User"]);
|
|
35389
|
+
const psaCov = summarizeCoverage(ctx.manifest, ["PermissionSetAssignment"]);
|
|
35390
|
+
return userCov.status !== "complete" || psaCov.status !== "complete";
|
|
35391
|
+
};
|
|
35392
|
+
}
|
|
35393
|
+
});
|
|
35394
|
+
|
|
34347
35395
|
// ../mcp/dist/src/tools/coverage-report.js
|
|
34348
35396
|
import { z as z25 } from "zod";
|
|
34349
|
-
var TOP_UNCOVERED_FAMILIES_CAP, COVERAGE_DISCLOSURE, coverageReportInputSchema, partitionCoverage, coverageReportHandler;
|
|
35397
|
+
var TOP_UNCOVERED_FAMILIES_CAP, COVERAGE_DISCLOSURE, coverageReportInputSchema, ASSIGNMENT_DATA_NOT_A_GAP_REASON, buildAssignmentDataCoverage, partitionCoverage, coverageReportHandler;
|
|
34350
35398
|
var init_coverage_report = __esm({
|
|
34351
35399
|
"../mcp/dist/src/tools/coverage-report.js"() {
|
|
34352
35400
|
"use strict";
|
|
34353
35401
|
init_src();
|
|
35402
|
+
init_src2();
|
|
34354
35403
|
init_src3();
|
|
35404
|
+
init_live_plane();
|
|
35405
|
+
init_vault_assignment_disclosure();
|
|
34355
35406
|
TOP_UNCOVERED_FAMILIES_CAP = 10;
|
|
34356
35407
|
COVERAGE_DISCLOSURE = "Coverage describes what the last `sf project retrieve` requested and returned \u2014 not what exists in the org. A type listed under `notModeled` is not analyzed by this product at all; its absence from any result means 'not checked', never 'none'. Re-run `/sfi-refresh` after widening your retrieve manifest to close a gap. `topUncoveredFamilies` ranks (by skipped-file volume) directories that WERE retrieved but not modeled by an extractor \u2014 a listed family is retrieved-but-not-modeled, never 'absent'.";
|
|
34357
35408
|
coverageReportInputSchema = z25.object({
|
|
34358
35409
|
type: z25.string().min(1).optional()
|
|
34359
35410
|
});
|
|
35411
|
+
ASSIGNMENT_DATA_NOT_A_GAP_REASON = "runtime data object \u2014 by design, not a retrieve gap";
|
|
35412
|
+
buildAssignmentDataCoverage = async (ctx) => {
|
|
35413
|
+
let present = false;
|
|
35414
|
+
let capturedAt = null;
|
|
35415
|
+
try {
|
|
35416
|
+
const rows = await readFacts(ctx.graph, {
|
|
35417
|
+
subjectId: ACTIVE_HOLDERS_COMPLETE_SUBJECT,
|
|
35418
|
+
metric: "activeHolders",
|
|
35419
|
+
limit: 1
|
|
35420
|
+
});
|
|
35421
|
+
const marker = rows.ok ? rows.value[0] : void 0;
|
|
35422
|
+
if (marker !== void 0 && typeof marker.value === "object" && marker.value !== null && marker.value.complete === true) {
|
|
35423
|
+
present = true;
|
|
35424
|
+
capturedAt = marker.capturedAt;
|
|
35425
|
+
}
|
|
35426
|
+
} catch {
|
|
35427
|
+
}
|
|
35428
|
+
const liveConsent = (await resolveLiveAccess(ctx.manifest.sourceOrg)).allowed;
|
|
35429
|
+
return {
|
|
35430
|
+
vaultModeled: false,
|
|
35431
|
+
reason: ASSIGNMENT_DATA_NOT_A_GAP_REASON,
|
|
35432
|
+
liveTools: ASSIGNMENT_DATA_LIVE_TOOLS,
|
|
35433
|
+
liveConsent,
|
|
35434
|
+
factsCounts: { present, capturedAt },
|
|
35435
|
+
rendered: `Runtime assignment data: not in vault (by design). Live: ${liveConsent ? "available" : "consent-needed"} (${ASSIGNMENT_DATA_LIVE_TOOLS.join(", ")}). Offline counts snapshot: ${present && capturedAt !== null ? `as of ${capturedAt}` : "none"}.`
|
|
35436
|
+
};
|
|
35437
|
+
};
|
|
34360
35438
|
partitionCoverage = (entries) => ({
|
|
34361
35439
|
covered: entries.filter((entry) => entry.requested && (entry.retrieved > 0 || entry.retrieveConfirmed === true) && !entry.errored && !entry.neverModeled && entry.pending !== true),
|
|
34362
35440
|
partial: entries.filter((entry) => entry.requested && (entry.retrieved === 0 && entry.retrieveConfirmed !== true || entry.errored) && !entry.neverModeled && entry.pending !== true),
|
|
@@ -34370,6 +35448,7 @@ var init_coverage_report = __esm({
|
|
|
34370
35448
|
const missingCoverage = summary.missingCoverage;
|
|
34371
35449
|
const staged = ctx.manifest.staged;
|
|
34372
35450
|
const topUncoveredFamilies = rankUncoveredFamilies(ctx.manifest).slice(0, TOP_UNCOVERED_FAMILIES_CAP);
|
|
35451
|
+
const assignmentData = await buildAssignmentDataCoverage(ctx);
|
|
34373
35452
|
return ok({
|
|
34374
35453
|
data: {
|
|
34375
35454
|
coverageKnown: summary.coverageKnown,
|
|
@@ -34378,6 +35457,7 @@ var init_coverage_report = __esm({
|
|
|
34378
35457
|
...staged !== void 0 ? { stagedBuild: { tier: staged.tier, totalTiers: staged.totalTiers } } : {},
|
|
34379
35458
|
summary,
|
|
34380
35459
|
topUncoveredFamilies,
|
|
35460
|
+
assignmentData,
|
|
34381
35461
|
trust: {
|
|
34382
35462
|
provenance: "offline_snapshot",
|
|
34383
35463
|
confidence: summary.coverageKnown ? "declared" : "unknown",
|
|
@@ -49364,7 +50444,7 @@ import { existsSync as existsSync5 } from "node:fs";
|
|
|
49364
50444
|
import { stat as stat6 } from "node:fs/promises";
|
|
49365
50445
|
import { join as join20 } from "node:path";
|
|
49366
50446
|
import { z as z81 } from "zod";
|
|
49367
|
-
var STALE_AGE_DAYS, MS_PER_DAY2, SKIPPED_FILES_DEGRADED_THRESHOLD, healthCheckInputSchema, buildFreshness, probeGraph, probeSourceHash, probeRenderComplete, healthCheckHandler;
|
|
50447
|
+
var STALE_AGE_DAYS, MS_PER_DAY2, SKIPPED_FILES_DEGRADED_THRESHOLD, STALE_FACTS_ADVISORY_DAYS, healthCheckInputSchema, buildFreshness, probeGraph, probeSourceHash, probeRenderComplete, healthCheckHandler;
|
|
49368
50448
|
var init_health_check = __esm({
|
|
49369
50449
|
"../mcp/dist/src/tools/health-check.js"() {
|
|
49370
50450
|
"use strict";
|
|
@@ -49372,9 +50452,11 @@ var init_health_check = __esm({
|
|
|
49372
50452
|
init_src2();
|
|
49373
50453
|
init_src3();
|
|
49374
50454
|
init_history_store();
|
|
50455
|
+
init_coverage_report();
|
|
49375
50456
|
STALE_AGE_DAYS = 7;
|
|
49376
50457
|
MS_PER_DAY2 = 24 * 60 * 60 * 1e3;
|
|
49377
50458
|
SKIPPED_FILES_DEGRADED_THRESHOLD = 100;
|
|
50459
|
+
STALE_FACTS_ADVISORY_DAYS = 30;
|
|
49378
50460
|
healthCheckInputSchema = z81.object({});
|
|
49379
50461
|
buildFreshness = (ctx, now, sourceHashMatches, recent, update) => {
|
|
49380
50462
|
const refreshedAt = ctx.manifest.refreshedAt;
|
|
@@ -49523,6 +50605,16 @@ var init_health_check = __esm({
|
|
|
49523
50605
|
}
|
|
49524
50606
|
const freshness = buildFreshness(ctx, now, hashProbe.match, recent, update);
|
|
49525
50607
|
const vaultHistoryEnabled = existsSync5(join20(ctx.vaultRoot, ".git"));
|
|
50608
|
+
const assignmentBase = await buildAssignmentDataCoverage(ctx);
|
|
50609
|
+
let assignmentAdvisory = null;
|
|
50610
|
+
if (assignmentBase.factsCounts.present && assignmentBase.factsCounts.capturedAt !== null) {
|
|
50611
|
+
const capturedMs = Date.parse(assignmentBase.factsCounts.capturedAt);
|
|
50612
|
+
const factsAgeDays = Number.isNaN(capturedMs) ? null : Math.max(0, Math.floor((now - capturedMs) / MS_PER_DAY2));
|
|
50613
|
+
if (factsAgeDays !== null && factsAgeDays >= STALE_FACTS_ADVISORY_DAYS) {
|
|
50614
|
+
assignmentAdvisory = `Assignment-data counts snapshot is ${factsAgeDays} days old (captured ${assignmentBase.factsCounts.capturedAt}); holder counts quoted from it may be stale \u2014 re-run \`sfi refresh --with-data-shape\`, or use the live tools for current rosters.`;
|
|
50615
|
+
}
|
|
50616
|
+
}
|
|
50617
|
+
const assignmentData = { ...assignmentBase, advisory: assignmentAdvisory };
|
|
49526
50618
|
return ok({
|
|
49527
50619
|
data: {
|
|
49528
50620
|
status,
|
|
@@ -49530,6 +50622,7 @@ var init_health_check = __esm({
|
|
|
49530
50622
|
checks,
|
|
49531
50623
|
coverage,
|
|
49532
50624
|
freshness,
|
|
50625
|
+
assignmentData,
|
|
49533
50626
|
vaultHistory: {
|
|
49534
50627
|
enabled: vaultHistoryEnabled,
|
|
49535
50628
|
enableHint: vaultHistoryEnabled ? null : "Run `sfi vault git enable` once for `sfi.component_history` / `sfi.component_as_of`."
|
|
@@ -51599,7 +52692,19 @@ var init_list_components = __esm({
|
|
|
51599
52692
|
/** Flow metadata: exact match on `properties.status` (e.g. Active). */
|
|
51600
52693
|
status: z90.string().min(1).optional(),
|
|
51601
52694
|
/** Flow metadata: keep only record-triggered flows (`triggerType` starts with Record). */
|
|
51602
|
-
recordTriggered: coercedOptionalBoolean
|
|
52695
|
+
recordTriggered: coercedOptionalBoolean,
|
|
52696
|
+
/**
|
|
52697
|
+
* Keep only components that LACK a description — `properties.description` is
|
|
52698
|
+
* absent, null, or empty. Answers "which reports/objects/permission-sets have
|
|
52699
|
+
* no description". Only trustworthy for a type whose extractor captures
|
|
52700
|
+
* description; a type that carries no `<description>` in Salesforce source
|
|
52701
|
+
* (ListView, CustomPermission, MutingPermissionSet, CustomMetadata records)
|
|
52702
|
+
* will match ALL of its nodes — that means "this type has no description in
|
|
52703
|
+
* source", not "the org failed to fill them in".
|
|
52704
|
+
*/
|
|
52705
|
+
missingDescription: coercedOptionalBoolean,
|
|
52706
|
+
/** Keep only components that HAVE a non-empty `properties.description`. */
|
|
52707
|
+
hasDescription: coercedOptionalBoolean
|
|
51603
52708
|
});
|
|
51604
52709
|
listComponentsHandler = async (ctx, input2) => {
|
|
51605
52710
|
if (input2.type === void 0) {
|
|
@@ -51608,6 +52713,13 @@ var init_list_components = __esm({
|
|
|
51608
52713
|
message: "type is required for v0.1"
|
|
51609
52714
|
});
|
|
51610
52715
|
}
|
|
52716
|
+
if (input2.missingDescription === true && input2.hasDescription === true) {
|
|
52717
|
+
return err({
|
|
52718
|
+
kind: "invalid-query",
|
|
52719
|
+
message: "missingDescription and hasDescription are mutually exclusive \u2014 pass at most one."
|
|
52720
|
+
});
|
|
52721
|
+
}
|
|
52722
|
+
const descriptionPresence = input2.missingDescription === true ? "absent" : input2.hasDescription === true ? "present" : void 0;
|
|
51611
52723
|
const limit = input2.limit ?? LIST_DEFAULT_LIMIT2;
|
|
51612
52724
|
const recordTriggered = input2.recordTriggered === true;
|
|
51613
52725
|
const fingerprint = argsFingerprint({
|
|
@@ -51616,6 +52728,7 @@ var init_list_components = __esm({
|
|
|
51616
52728
|
...input2.triggerObject !== void 0 ? { triggerObject: input2.triggerObject } : {},
|
|
51617
52729
|
...input2.status !== void 0 ? { status: input2.status } : {},
|
|
51618
52730
|
...recordTriggered ? { recordTriggered: true } : {},
|
|
52731
|
+
...descriptionPresence !== void 0 ? { descriptionPresence } : {},
|
|
51619
52732
|
...Object.fromEntries(APEX_BOOLEAN_FILTERS.flatMap((k2) => input2[k2] !== void 0 ? [[k2, input2[k2]]] : []))
|
|
51620
52733
|
});
|
|
51621
52734
|
let offset = input2.offset ?? 0;
|
|
@@ -51646,7 +52759,8 @@ var init_list_components = __esm({
|
|
|
51646
52759
|
...input2.parentId !== void 0 ? { parentId: input2.parentId } : {},
|
|
51647
52760
|
...hasPropertyFilter ? { propertyEquals } : {},
|
|
51648
52761
|
...hasStringPropertyFilter ? { propertyStringEquals } : {},
|
|
51649
|
-
...recordTriggered ? { recordTriggered: true } : {}
|
|
52762
|
+
...recordTriggered ? { recordTriggered: true } : {},
|
|
52763
|
+
...descriptionPresence !== void 0 ? { descriptionPresence } : {}
|
|
51650
52764
|
};
|
|
51651
52765
|
const queryResult = await listNodesByType(ctx.graph, input2.type, {
|
|
51652
52766
|
limit,
|
|
@@ -51679,7 +52793,7 @@ var init_list_components = __esm({
|
|
|
51679
52793
|
const propertiesSlimmed = kept.some((n2) => n2.properties?.["propertiesTruncated"] === true);
|
|
51680
52794
|
const hasMore = pageNodes.length === limit || trimmed;
|
|
51681
52795
|
let retrievalHint;
|
|
51682
|
-
if (offset === 0 && pageNodes.length === 0 && !hasPropertyFilter && !hasStringPropertyFilter && !recordTriggered) {
|
|
52796
|
+
if (offset === 0 && pageNodes.length === 0 && !hasPropertyFilter && !hasStringPropertyFilter && !recordTriggered && descriptionPresence === void 0) {
|
|
51683
52797
|
const cov = summarizeCoverage(ctx.manifest, [input2.type]);
|
|
51684
52798
|
if (cov.notModeledTypes.includes(input2.type)) {
|
|
51685
52799
|
retrievalHint = `No \`${input2.type}\` in the vault \u2014 this type is NOT modeled by the current build, so its absence means "not analyzed", never "none in the org".`;
|
|
@@ -52847,23 +53961,6 @@ var init_naming_convention_report = __esm({
|
|
|
52847
53961
|
}
|
|
52848
53962
|
});
|
|
52849
53963
|
|
|
52850
|
-
// ../mcp/dist/src/tools/vault-assignment-disclosure.js
|
|
52851
|
-
var USER_ASSIGNMENT_NOT_IN_VAULT, PERMSET_INTERSECTION_NOT_AVAILABLE, SHARING_USER_ENUMERATION_NOT_AVAILABLE, userAssignmentUnavailable;
|
|
52852
|
-
var init_vault_assignment_disclosure = __esm({
|
|
52853
|
-
"../mcp/dist/src/tools/vault-assignment-disclosure.js"() {
|
|
52854
|
-
"use strict";
|
|
52855
|
-
init_src3();
|
|
52856
|
-
USER_ASSIGNMENT_NOT_IN_VAULT = "User and PermissionSetAssignment data are not retrieved into this vault \u2014 which users hold a profile, permission set, or group membership cannot be answered offline. Run the live org plane (read-only) or refresh with a PermissionSetAssignment facts capture for user-assignment questions.";
|
|
52857
|
-
PERMSET_INTERSECTION_NOT_AVAILABLE = "Which users have multiple permission sets assigned requires PermissionSetAssignment rows (user assignment data not in vault). This tool can list each permission set structurally but cannot name users with both assignments without querying live assignment data.";
|
|
52858
|
-
SHARING_USER_ENUMERATION_NOT_AVAILABLE = "Sharing rules name group/role targets, but User records and group membership to individual users are not fully available offline \u2014 cannot enumerate every user who effectively receives Edit from a group-based sharing rule without live User / GroupMember / PermissionSetAssignment data.";
|
|
52859
|
-
userAssignmentUnavailable = (ctx) => {
|
|
52860
|
-
const userCov = summarizeCoverage(ctx.manifest, ["User"]);
|
|
52861
|
-
const psaCov = summarizeCoverage(ctx.manifest, ["PermissionSetAssignment"]);
|
|
52862
|
-
return userCov.status !== "complete" || psaCov.status !== "complete";
|
|
52863
|
-
};
|
|
52864
|
-
}
|
|
52865
|
-
});
|
|
52866
|
-
|
|
52867
53964
|
// ../mcp/dist/src/tools/object-access-audit.js
|
|
52868
53965
|
import { z as z100 } from "zod";
|
|
52869
53966
|
var CUSTOM_OBJECT_PREFIX7, PERMISSION_SET_PREFIX2, GRANTOR_TYPES2, objectAccessAuditInputSchema, flag, compareGrants2, objectAccessAuditHandler;
|
|
@@ -55386,7 +56483,7 @@ var init_retrieve_blindspot_report = __esm({
|
|
|
55386
56483
|
});
|
|
55387
56484
|
|
|
55388
56485
|
// ../mcp/dist/src/context-resolution.js
|
|
55389
|
-
var PRONOUN_ANCHORS, REPARAM_ANCHORS, detectPronounAnchor, detectReparamAnchor, ANAPHOR_ONLY, isAnaphorOnly, ORDINAL_INDEX, SELECTION_SHAPE, NON_DESCRIPTOR_TOKENS, detectClarificationSelection, GAP_FOLLOW_UP_SHAPES, detectGapShapedFollowUp, COMPONENT_ID_SHAPE, validatePreviousContext, CONTINUATION_TOOL_TYPES, continuationToolCompatible, REPARAM_FILLER, extractReparamTarget;
|
|
56486
|
+
var PRONOUN_ANCHORS, REPARAM_ANCHORS, detectPronounAnchor, detectReparamAnchor, ANAPHOR_ONLY, isAnaphorOnly, ORDINAL_INDEX, SELECTION_SHAPE, NON_DESCRIPTOR_TOKENS, detectClarificationSelection, RUNTIME_FOLLOW_UP_SHAPES, GAP_FOLLOW_UP_SHAPES, detectGapShapedFollowUp, COMPONENT_ID_SHAPE, validatePreviousContext, CONTINUATION_TOOL_TYPES, continuationToolCompatible, REPARAM_FILLER, extractReparamTarget;
|
|
55390
56487
|
var init_context_resolution = __esm({
|
|
55391
56488
|
"../mcp/dist/src/context-resolution.js"() {
|
|
55392
56489
|
"use strict";
|
|
@@ -55477,6 +56574,27 @@ var init_context_resolution = __esm({
|
|
|
55477
56574
|
const containing = options.filter((option) => option.toLowerCase().includes(token));
|
|
55478
56575
|
return containing.length === 1 ? { kind: "selected", anaphor, selection: containing[0] } : { kind: "re-ask", anaphor };
|
|
55479
56576
|
};
|
|
56577
|
+
RUNTIME_FOLLOW_UP_SHAPES = [
|
|
56578
|
+
// login history + IPs (per-user session telemetry, never modeled).
|
|
56579
|
+
/\b(?:login|log[-\s]?in|sign[-\s]?in)\s+history\b|\bwho\s+(?:actually\s+)?logged\s+in\b|\bfrom\s+what\s+ip\b|\bip\s+address(?:es)?\b/i,
|
|
56580
|
+
// API call logs / latency / error rate (runtime, not metadata).
|
|
56581
|
+
/\bapi\s+call\s+(?:logs?|counts?|volume)\b|\b(?:error|failure)\s+rate\b|\bhow\s+(?:many\s+times|often)\s+(?:was\s+it|has\s+it\s+been)\s+called\b|\bcall\s+volume\b/i,
|
|
56582
|
+
// approval-instance history — "who actually approved" (a runtime instance,
|
|
56583
|
+
// not the approval-process metadata). "who CAN approve" stays a config read.
|
|
56584
|
+
/\bwho\s+(?:actually\s+)?approved\b|\bapproval\s+history\b|\bwas\s+(?:it|that|this)\s+approved\b/i,
|
|
56585
|
+
// record field-history — every value change ON A RECORD (runtime audit
|
|
56586
|
+
// trail). "why did the FIELD change" (metadata provenance) is NOT this: the
|
|
56587
|
+
// anchor is a record/instance ("on that record", "for that account").
|
|
56588
|
+
/\b(?:every|all\s+the|each)\s+(?:value\s+)?changes?\s+(?:on|to|for)\s+(?:that|this|the)\s+(?:record|row|account|contact|case|lead|opportunity)\b|\bfield\s+history\s+(?:on|for)\b/i,
|
|
56589
|
+
// report/dashboard subscription recipients (runtime subscriptions).
|
|
56590
|
+
/\bwho\s+(?:is\s+)?subscribed\b|\bsubscription\s+recipients?\b|\bwho\s+gets?\s+(?:the|that)\s+(?:report|dashboard)\s+(?:emailed|sent)\b/i,
|
|
56591
|
+
// chatter / feed posts (runtime social data).
|
|
56592
|
+
/\bchatter\s+(?:posts?|activity|feed)\b|\bfeed\s+(?:posts?|items?|comments?)\b/i,
|
|
56593
|
+
// sandbox refresh date / deployment logs (runtime org lifecycle telemetry).
|
|
56594
|
+
/\bsandbox\s+(?:last\s+)?refresh(?:ed)?\b|\bwhen\s+was\s+(?:the\s+)?sandbox\b|\bdeployment\s+(?:logs?|history)\b|\blast\s+deploy(?:ment|ed)\b/i,
|
|
56595
|
+
// flow-interview history / event publish counts (runtime execution traces).
|
|
56596
|
+
/\bflow\s+interview(?:s|\s+history)?\b|\b(?:how\s+many\s+)?(?:events?\s+)?(?:were\s+)?published\b|\bpublish\s+counts?\b/i
|
|
56597
|
+
];
|
|
55480
56598
|
GAP_FOLLOW_UP_SHAPES = [
|
|
55481
56599
|
// Normative/judgment asks — the product reads metadata, it has no opinion
|
|
55482
56600
|
// (q891/q30 "should they be able to?", q95 "is it normal?", q1075 "was any
|
|
@@ -55490,13 +56608,29 @@ var init_context_resolution = __esm({
|
|
|
55490
56608
|
[/\bhow\s+hard\s+(?:is|would)\s+it\b/i, "judgment"],
|
|
55491
56609
|
[/\bdoes\s+that\s+mean\s+something(?:'s|\s+is)\s+broken\b/i, "judgment"],
|
|
55492
56610
|
[/\bis\s+it\s+doing\s+its\s+job\b/i, "judgment"],
|
|
56611
|
+
// R4 additions — more normative/opinion shapes the 2K context run showed
|
|
56612
|
+
// inheriting the prior tool: "is that a problem?", "is that too many/too
|
|
56613
|
+
// much?", "is that a good idea?", "should I be worried?", "is that
|
|
56614
|
+
// secure/safe?" (a verdict, not a metadata read — "is it SAFE TO DELETE"
|
|
56615
|
+
// is excluded: it carries its own real route and never reaches this
|
|
56616
|
+
// detector, which only runs while STILL unrouted).
|
|
56617
|
+
[/\bis\s+(?:that|this|it)\s+(?:a\s+)?problem\b|\bis\s+(?:that|this|it)\s+(?:too\s+(?:many|much|few)|a\s+lot)\b/i, "judgment"],
|
|
56618
|
+
[/\bis\s+(?:that|this|it)\s+(?:a\s+)?good\s+idea\b|\bshould\s+i\s+be\s+(?:worried|concerned)\b/i, "judgment"],
|
|
56619
|
+
[/\bis\s+(?:that|this|it)\s+(?:considered\s+)?(?:secure|safe|risky|dangerous)\b(?!\s+to\s+delete\b)/i, "judgment"],
|
|
55493
56620
|
// Delivery/export asks (q914 "as a file rather than on screen", q882 "can
|
|
55494
56621
|
// it be exported…", q1939 "flag those as an audit finding").
|
|
55495
56622
|
[/\bas\s+a\s+file\b|\bcan\s+(?:it|this|that)\s+be\s+exported\b/i, "delivery"],
|
|
55496
56623
|
[/^\s*flag\s+(?:those|these|them|it)\b/i, "delivery"],
|
|
56624
|
+
// R4 additions — more delivery/notification shapes: "email me that", "send
|
|
56625
|
+
// it to …", "put it in a spreadsheet / csv / pdf", "download it".
|
|
56626
|
+
[/^\s*(?:email|send|forward)\s+(?:me\s+)?(?:that|this|it|those|these|them)\b/i, "delivery"],
|
|
56627
|
+
[/\b(?:as|in|to)\s+(?:a\s+)?(?:spreadsheet|csv|pdf|excel|report\s+file)\b|\b(?:can\s+(?:you|i)\s+)?download\s+(?:it|that|this|them)\b/i, "delivery"],
|
|
55497
56628
|
// Self-capability probes about the TOOL, not the org (q849 "does the tool
|
|
55498
56629
|
// trace transitive access like that, or is that beyond it?").
|
|
55499
56630
|
[/\b(?:does|can)\s+(?:the|this)\s+tool\b/i, "tool-self-capability"],
|
|
56631
|
+
// R4 additions — "is that beyond you / can you even do that / do you have
|
|
56632
|
+
// that data" self-capability probes.
|
|
56633
|
+
[/\bis\s+(?:that|this)\s+beyond\s+(?:you|it|the\s+tool)\b|\bcan\s+you\s+even\b|\bdo\s+you\s+(?:have|hold)\s+(?:that|this)\s+(?:data|info(?:rmation)?)\b/i, "tool-self-capability"],
|
|
55500
56634
|
// Deployment-status telemetry (q1402 "was the change deployed or is it
|
|
55501
56635
|
// still pending?").
|
|
55502
56636
|
[/\bstill\s+pending\b|\bwas\s+(?:it|that|the\s+change)\s+deployed\b/i, "deployment-status"]
|
|
@@ -55506,6 +56640,10 @@ var init_context_resolution = __esm({
|
|
|
55506
56640
|
if (shape.test(question))
|
|
55507
56641
|
return family;
|
|
55508
56642
|
}
|
|
56643
|
+
for (const shape of RUNTIME_FOLLOW_UP_SHAPES) {
|
|
56644
|
+
if (shape.test(question))
|
|
56645
|
+
return "runtime-analytics";
|
|
56646
|
+
}
|
|
55509
56647
|
return null;
|
|
55510
56648
|
};
|
|
55511
56649
|
COMPONENT_ID_SHAPE = /^[A-Za-z]+:.+$/;
|
|
@@ -55593,16 +56731,49 @@ var init_context_resolution = __esm({
|
|
|
55593
56731
|
});
|
|
55594
56732
|
|
|
55595
56733
|
// ../mcp/dist/src/refusal-gates.js
|
|
55596
|
-
var INJECTION_PATTERNS, EXFIL_VERB, EXFIL_QUANTIFIER, EXFIL_SENSITIVE, EXFIL_VALUES, EXFIL_SINGLE_VALUE, INJECTION_DISCLOSURE, WRITE_VERB, WRITE_VERB_FILLER, WRITE_EXCLUDER, SIMULATION_TAIL, WRITE_SENTENCE_INITIAL, WRITE_LEAD_IN, WRITE_TRAILING_FRAME, WRITE_MAKE_SCHEMA, WRITE_GIVE_GRANT, WRITE_GIVE_INITIAL, WRITE_CHAINED_DUPE_DELETE, RUN_VERB, RUN_TARGET, RUN_GAP, RUN_IMPERATIVE_INITIAL, RUN_IMPERATIVE_LEAD_IN, RUN_ANAPHOR, runReadOnlyAlternativeFor, readOnlyAlternativeFor, IDENTITY_IDIOM, IDENTITY_ALLOWED_MUTATE, IDENTITY_LOOK_EXCLUDER, IDENTITY_DISCLOSURE, RUNTIME_TRIGGERS, RUNTIME_WINDOW, RUNTIME_INCIDENT, runtimeDisclosure, EXTERNAL_SYSTEM, POLICY_ASK, RETENTION_POLICY_ASK, CONSENT_PROCESS_ASK, OPINION_ASK, SHOULD_WE, METADATA_NOUN, DELIVERY_ASK, WRITE_CODE_ASK, DRAFT_DOCUMENT_ASK, CAREER_ASK, outOfScopeDisclosure, detectRefusalShape;
|
|
56734
|
+
var INJECTION_PATTERNS, EXFIL_VERB, EXFIL_QUANTIFIER, EXFIL_SENSITIVE, EXFIL_VALUES, EXFIL_SINGLE_VALUE, INJECTION_DISCLOSURE, WRITE_VERB, WRITE_VERB_FILLER, WRITE_EXCLUDER, WRITE_DOC_NOUN, WRITE_READ_FRAME, SIMULATION_TAIL, CLAUSE_SEP, WRITE_SENTENCE_INITIAL, WRITE_LEAD_IN, WRITE_TRAILING_FRAME, WRITE_MAKE_SCHEMA, WRITE_GIVE_GRANT, WRITE_GIVE_INITIAL, WRITE_CHECK_IN, WRITE_CHAINED_DEPLOY, WRITE_CHAINED_DUPE_DELETE, RUN_VERB, RUN_TARGET, RUN_GAP, RUN_IMPERATIVE_INITIAL, RUN_IMPERATIVE_LEAD_IN, RUN_ANAPHOR, runReadOnlyAlternativeFor, readOnlyAlternativeFor, IDENTITY_IDIOM, IDENTITY_ALLOWED_MUTATE, IDENTITY_LOOK_EXCLUDER, IDENTITY_DISCLOSURE, FORECAST_VERB, FORECAST_TREND, FORECAST_HORIZON, FORECAST_OUTCOME, FORECAST_STRONG_OUTCOME, FORECAST_DISCLOSURE, PROVENANCE_CREATE, PROVENANCE_CREATE_DATE, PROVENANCE_LASTMOD_ONLY, PROVENANCE_DISCLOSURE, RUNTIME_TRIGGERS, RUNTIME_WINDOW, RUNTIME_INCIDENT, runtimeDisclosure, EXTERNAL_SYSTEM, POLICY_ASK, RETENTION_POLICY_ASK, CONSENT_PROCESS_ASK, OPINION_ASK, SHOULD_WE, METADATA_NOUN, DELIVERY_ASK, WRITE_CODE_ASK, DRAFT_DOCUMENT_ASK, CAREER_ASK, outOfScopeDisclosure, detectRefusalShape;
|
|
55597
56735
|
var init_refusal_gates = __esm({
|
|
55598
56736
|
"../mcp/dist/src/refusal-gates.js"() {
|
|
55599
56737
|
"use strict";
|
|
55600
56738
|
INJECTION_PATTERNS = [
|
|
55601
|
-
|
|
56739
|
+
// "ignore your previous/prior/read-only instructions|rules|restrictions".
|
|
56740
|
+
// R4: the (?:and\s+\w+\s+)? optional carries "ignore your read-only AND
|
|
56741
|
+
// PRIVACY restrictions" (q3749) — a conjoined adjective before the noun no
|
|
56742
|
+
// longer breaks the token sequence. `constraints` joins the noun set (q4047
|
|
56743
|
+
// "ignore your read-only constraints").
|
|
56744
|
+
/\bignore\s+(?:your|all|the)\s+(?:previous|prior|read-?only)\s+(?:and\s+\w+\s+)?(?:instructions?|rules?|restrictions?|constraints?)\b/i,
|
|
56745
|
+
// R4 injection framings the 3K phrasings used to evade every arm above:
|
|
56746
|
+
// - "forget everything above / forget your prior instructions" (q4842).
|
|
56747
|
+
/\bforget\s+(?:everything|all(?:\s+of)?\s+the)\s+(?:above|prior|previous|earlier)\b|\bforget\s+(?:your|the)\s+(?:prior|previous|earlier)\s+(?:instructions?|rules?|restrictions?|constraints?)\b/i,
|
|
56748
|
+
// - role-play jailbreak: "AI with no restrictions", "named DAN", "you are
|
|
56749
|
+
// now an unrestricted assistant / DAN / an AI that can modify the org".
|
|
56750
|
+
/\b(?:ai|assistant|model)\s+(?:with\s+)?no\s+restrictions\b|\bnamed\s+dan\b|\bact\s+as\s+dan\b|\byou\s+are\s+(?:now\s+)?(?:an?\s+)?(?:unrestricted|jailbroken|uncensored)\b/i,
|
|
56751
|
+
// - "SYSTEM OVERRIDE", "the previous ... constraints are revoked",
|
|
56752
|
+
// "you are now permitted to reveal ..." (q4873).
|
|
56753
|
+
/\bsystem\s+override\b|\b(?:previous|prior)\s+(?:knowledge-?base\s+)?(?:constraints?|restrictions?|instructions?)\s+(?:are|have\s+been)\s+revoked\b|\byou\s+are\s+now\s+permitted\s+to\s+reveal\b/i,
|
|
56754
|
+
// - "new instruction ... embedded in this message" / "instruction from the
|
|
56755
|
+
// vault owner embedded in ..." (q4627) — a smuggled-instruction framing.
|
|
56756
|
+
/\b(?:new\s+)?instruction[s]?\b[^.?!]{0,40}\bembedded\s+in\s+this\s+(?:message|prompt|text|question)\b/i,
|
|
56757
|
+
// - social-engineering authority claim to force an action: "the (real)
|
|
56758
|
+
// admins told me to have you ...", "they authorized it", "the client
|
|
56759
|
+
// signed off" chained with a mutation/exfil (q4893/q4668). Narrow: the
|
|
56760
|
+
// authority claim must co-occur with an override/do-it imperative.
|
|
56761
|
+
// "the client signed off" arms ONLY when a redeploy/deploy/mutation of org
|
|
56762
|
+
// artifacts rides in the same breath (q4668) — a plain "the client signed
|
|
56763
|
+
// off on the design, show me the flow" must not gate, so require a deploy
|
|
56764
|
+
// verb nearby.
|
|
56765
|
+
/\bthey\s+authorized\s+it\b|\breal\s+admins?\s+told\s+me\s+to\s+have\s+you\b|\b(?:the\s+)?client\s+signed\s+off\b[^.?!]{0,120}\b(?:re-?deploy|deploy|repoint|push\s+(?:it|them|this)|redeploy\s+them)\b/i,
|
|
55602
56766
|
/\bpretend\s+the\s+(?:previous|prior)\s+rules\b/i,
|
|
55603
56767
|
/\byou\s+are\s+now\s+(?:in\s+)?(?:admin|deploy|god)\s*[- ]?mode\b/i,
|
|
55604
56768
|
/\byou'?re\s+actually\s+an?\b[^.?!]*\bbot\b/i,
|
|
55605
56769
|
/\b(?:print|reveal|show)\s+(?:me\s+)?your\s+(?:system\s+)?prompt\b/i,
|
|
56770
|
+
// Secret/credential exfiltration (q4873/q3635 secret asks): reveal/output/
|
|
56771
|
+
// give the raw connection string, OAuth/client secret, session/refresh
|
|
56772
|
+
// token, API key, or password policy for delivery. The metadata READ
|
|
56773
|
+
// "which named credentials exist" / "what auth provider does X use" carries
|
|
56774
|
+
// no reveal-the-SECRET-VALUE verb and stays routed. `endpoint` alone is NOT
|
|
56775
|
+
// a secret (q4956 "I need the endpoint on the named credential" routes).
|
|
56776
|
+
/\b(?:reveal|output|show\s+me|give\s+me|dump|print|expose)\b[^.?!]{0,60}\b(?:client\s+secret|oauth\s+secret|connection\s+string|session\s+token|refresh\s+token|api\s+key|secret\s+key|private\s+key)\b/i,
|
|
55606
56777
|
// Privilege-escalation asks (q1548 "sudo give me full access"): `sudo` as a
|
|
55607
56778
|
// command prefix, or a grant-to-SELF ("give me full/admin access"). The
|
|
55608
56779
|
// READ delivery ask "give me the FLS grant list" carries no privilege noun
|
|
@@ -55617,17 +56788,27 @@ var init_refusal_gates = __esm({
|
|
|
55617
56788
|
EXFIL_VALUES = /\bvalues?\b|\bstored\b/i;
|
|
55618
56789
|
EXFIL_SINGLE_VALUE = /\b(?:what(?:'s|s|\s+is)|show\s+me|give\s+me|pull)\s+the\s+(?:ssn|social\s+security(?:\s+number)?)\b(?!\s+fields?\b)[^.?!]{0,60}\b(?:lead|contact|record|student|person|user|account|applicant)s?\b/i;
|
|
55619
56790
|
INJECTION_DISCLOSURE = "REFUSED: this request asks me to override instructions or exfiltrate record values. sf-intelligence reads metadata only; it never returns stored record values and its instructions are not overridable.";
|
|
55620
|
-
WRITE_VERB = "(?:create|add|delete|remove|update|change|(?<!permission\\s)set|deploy|assign|reassign|revoke|deactivate|activate|disable|enable|merge|reset|rename|fix|grant|provision|turn\\s+(?:on|off)|clean\\s+up|migrate|convert|insert|upsert|push|publish|install|uninstall|schedule|upgrade|downgrade|throttle|purge)";
|
|
55621
|
-
WRITE_VERB_FILLER = "(?:(?:bulk|mass|batch|just|please|go\\s+ahead\\s+and)
|
|
56791
|
+
WRITE_VERB = "(?:create|add|delete|remove|update|change|(?<!permission\\s)set|(?:quick[-\\s]?|re)?deploy|assign|reassign|revoke|deactivate|activate|disable|enable|merge|reset|rename|fix|grant|provision|turn\\s+(?:on|off)|clean\\s+up|migrate|convert|insert|upsert|push|publish|install|uninstall|schedule|upgrade|downgrade|throttle|purge|build|split|rotate|bump|pin|operationalize|repoint|stand\\s+up|spin\\s+up|comment\\s+out|wire\\s+(?:it|them|this|that)?\\s*(?:in|into)|roll\\s+(?:it|them|this|that)?\\s*into)";
|
|
56792
|
+
WRITE_VERB_FILLER = "(?:(?:bulk|mass|batch|just|please|go\\s+ahead\\s+and)[-\\s]+)*";
|
|
55622
56793
|
WRITE_EXCLUDER = /\b(?:am\s+i|can\s+i|could\s+i|do\s+i|who\s+can|who\s+is\s+able|allowed\s+to|able\s+to|what\s+if|what\s+would|would\s+happen|is\s+it\s+safe|safe\s+to|before\s+i|if\s+(?:i|we)|when\s+(?:i|we)|suppose|should\s+i|how\s+do\s+i|how\s+would\s+i|what\s+happens\s+when)\b|\bsafely\s*\?|\bwhat\s+(?:depends\s+on|breaks|would\s+break|will\s+break|stops\s+working)\b/i;
|
|
56794
|
+
WRITE_DOC_NOUN = "(?:onboarding|admin|developer|dev|architecture|data)?[-\\s]?(?:doc(?:ument(?:ation)?|s)?|handbook|tour|walkthrough|overview|dictionary|summary|guide|primer|runbook|onboarding|write[-\\s]?up|report\\s+of)";
|
|
56795
|
+
WRITE_READ_FRAME = new RegExp(
|
|
56796
|
+
// 1 — doc-generation: a build/create/generate/write/draft/make verb whose
|
|
56797
|
+
// object (within a short window) is a documentation noun.
|
|
56798
|
+
`\\b(?:build|create|generate|write|draft|put\\s+together|make|need|want|give\\s+me)\\b[^.?!;]{0,40}\\b${WRITE_DOC_NOUN}\\b|\\b${WRITE_DOC_NOUN}\\b[^.?!;]{0,30}\\.?\\s*(?:build|create|generate|make|put\\s+together)\\s+(?:it|this|that|one)\\b|\\b(?:which|what|who|whose|list\\s+(?:the\\s+)?)\\b[^.?!;]{0,40}\\b(?:enables?|disables?|grants?)\\b|\\b(?:before|after|ahead\\s+of|prior\\s+to|until|once)\\s+(?:a\\s+|the\\s+|we\\s+|you\\s+|i\\s+)?(?:re-?)?deploy(?:ment|ing)?\\b`,
|
|
56799
|
+
"i"
|
|
56800
|
+
);
|
|
55623
56801
|
SIMULATION_TAIL = /\b(?:and\s+)?(?:tell|show|give)\s+me\s+(?:the\s+)?(?:impact|blast\s+radius|consequences?|every\s+conflict|conflicts|what\s+(?:breaks|happens|stops|changes))\b|[—–-]\s*(?:impact|consequences?|blast\s+radius)\s*\??\s*$|\b(?:impact|consequences)\s*\?\s*$|\bwhat(?:'s|\s+is)\s+the\s+(?:impact|blast\s+radius|fallout)\b|\bwhat\s+(?:happens|stops|breaks)\s+(?:to|if|when|after|downstream)\b/i;
|
|
55624
|
-
|
|
56802
|
+
CLAUSE_SEP = "(?:^|[.!?;]\\s+|\\s+[\u2014\u2013]\\s*)";
|
|
56803
|
+
WRITE_SENTENCE_INITIAL = new RegExp(`${CLAUSE_SEP}(?:please\\s+|just\\s+)?${WRITE_VERB_FILLER}(${WRITE_VERB}\\b[^.?!;]{0,80})`, "i");
|
|
55625
56804
|
WRITE_LEAD_IN = new RegExp(`\\b(?:please|(?<!\\b(?:or|vs|than)\\s)just|go\\s+ahead\\s+and|can\\s+(?:you|u)|could\\s+(?:you|u)|would\\s+(?:you|u)|you\\s+should|i\\s+(?:need|want)\\s+you\\s+to)\\s+(?:please\\s+|just\\s+)?${WRITE_VERB_FILLER}(${WRITE_VERB}\\b[^.?!;]{0,80})`, "i");
|
|
55626
56805
|
WRITE_TRAILING_FRAME = new RegExp(`\\b(${WRITE_VERB}\\b[^.?!;]{0,80}?\\b(?:for\\s+me|right\\s+now|and\\s+confirm)\\b[^.?!;]{0,20})`, "i");
|
|
55627
56806
|
WRITE_MAKE_SCHEMA = new RegExp(`\\b(?:can|could|would)\\s+(?:you|u)\\b[^.?!;]{0,10}\\s(make\\b[^.?!;]{0,60}\\b(?:required|mandatory|optional|unique|read[-\\s]?only|editable|visible)\\b)`, "i");
|
|
55628
56807
|
WRITE_GIVE_GRANT = new RegExp(`\\b(?:can|could|would)\\s+(?:you|u)\\b[^.?!;]{0,30}\\b(give\\s+(?!me\\b)[^.?!;]{0,60}\\b(?:profile|permission|perm\\s+set|access|user)\\b[^.?!;]{0,40})`, "i");
|
|
55629
56808
|
WRITE_GIVE_INITIAL = /(?:^|[.!?;]\s+)(give\s+(?!me\b)[^.?!;]{0,60}\b(?:admin|access|permission|profile)\b[^.?!;]{0,30})/i;
|
|
55630
|
-
|
|
56809
|
+
WRITE_CHECK_IN = /\b(check\s+(?:it|them|this|that)\s+in|commit\s+(?:it|them|this|the\s+\w+)|push\s+(?:it|them|this)\s+to\s+(?:prod|production|the\s+repo)|promote\s+(?:it|them|this)\s+to\s+(?:prod|production))\b/i;
|
|
56810
|
+
WRITE_CHAINED_DEPLOY = /(?:^|[.!?;]\s+|,\s*(?:and\s+|then\s+)?|\band\s+|\bthen\s+)((?:re-?deploy|quick[-\s]?deploy|repoint)\b[^.?!;]{0,60})/i;
|
|
56811
|
+
WRITE_CHAINED_DUPE_DELETE = /(?:\b(?:and|then)\s+(?:just\s+)?|(?:^|[.!?;]\s+)(?:just\s+)?)((?:delete|remove|purge|merge)\s+(?:all\s+|the\s+|every\s+){0,2}dup(?:e|licate)s?\b[^.?!;]{0,30})/i;
|
|
55631
56812
|
RUN_VERB = "(?:run|re-?run|execute|invoke|kick\\s+off|trigger|launch|fire\\s+off)";
|
|
55632
56813
|
RUN_TARGET = "(?:flows?|triggers?|batch(?:\\s+(?:jobs?|class(?:es)?|apex))?|jobs?|apex\\s+class(?:es)?|automations?|scripts?)";
|
|
55633
56814
|
RUN_GAP = "(?:(?!\\b(?:on|of|for|across|against|in|over|about)\\b)[^.?!;]){0,60}?";
|
|
@@ -55685,6 +56866,16 @@ var init_refusal_gates = __esm({
|
|
|
55685
56866
|
IDENTITY_ALLOWED_MUTATE = /\bam\s+i\s+(?:allowed|permitted|able)\s+to\s+(?:change|delete|remove|merge|deactivate|disable|create|rename|reset|convert|reassign|modify|update)\b|\bis\s+it\s+(?:okay?|ok)\s+for\s+me\s+to\s+(?:change|delete|remove|merge|deactivate|disable|create|rename|reset|convert|reassign|modify|update)\b/i;
|
|
55686
56867
|
IDENTITY_LOOK_EXCLUDER = /\bam\s+i\s+(?:allowed|permitted|able)\s+to\s+(?:look|see|view|read|ask|know|check|understand|open)\b|\bis\s+it\s+(?:okay?|ok)\s+for\s+me\s+to\s+(?:look|see|view|read|ask|know|check)\b/i;
|
|
55687
56868
|
IDENTITY_DISCLOSURE = "HONEST GAP (identity): sf-intelligence has no session-user identity \u2014 it cannot know what YOU specifically are allowed to do. Name the user, profile, or permission set to check and I can answer from the permission metadata (sfi.effective_permissions / sfi.user_ability). Which user or profile should I check?";
|
|
56869
|
+
FORECAST_VERB = /\b(?:forecast|predict|projection|projecting|extrapolate|estimate\s+how\s+many|best\s+estimate\s+of\s+how\s+many)\b|\bproject\s+(?:our|the|how|where|when|my)\b|\bwhen\s+do\s+you\s+project\b|\bgive\s+me\s+your\s+(?:projection|best\s+estimate|churn\s+forecast)\b/i;
|
|
56870
|
+
FORECAST_TREND = /\b(?:current\s+(?:\w+\s+)?(?:growth|trend|trajectory)|given\s+(?:current|projected)\s+(?:\w+\s+)?(?:growth|trends?)|based\s+on\s+(?:current\s+)?trends?|at\s+the\s+current\s+(?:growth\s+)?rate|projected\s+growth)\b/i;
|
|
56871
|
+
FORECAST_HORIZON = /\b(?:next\s+(?:quarter|term|month|fiscal\s+year|year)|by\s+(?:the\s+)?end\s+of\s+next\b|in\s+(?:12\s+months|two\s+quarters|\d+\s+(?:months|quarters|years))|next\s+term)\b/i;
|
|
56872
|
+
FORECAST_OUTCOME = /\bwill\s+(?:we|it|this|our|migrating|rolling|the)\b|\b(?:hit|exceed|run\s+out|breach|reach)\b|\bhow\s+many\b|\bhow\s+likely\b|\bwe'?ll\b|\bwhere\s+will\b/i;
|
|
56873
|
+
FORECAST_STRONG_OUTCOME = /\bwill\b[^.?!]{0,60}\b(?:hit|exceed|run\s+out|breach|reach)\b/i;
|
|
56874
|
+
FORECAST_DISCLOSURE = "HONEST GAP (forecast): sf-intelligence has no time-series or forecasting model \u2014 it reads the CURRENT metadata/limits snapshot, not future projections or growth trends. I can show you the present state (e.g. sfi.live_org_limits, sfi.org_pulse, sfi.tech_debt_score) so you can extrapolate, but the prediction itself is out of scope.";
|
|
56875
|
+
PROVENANCE_CREATE = /\bwho\s+(?:originally\s+)?(?:created|built|authored|made|wrote|set\s+up|first\s+(?:created|built|granted|authored))\b|\b(?:original|initial)\s+author\b|\bwho\s+first\s+(?:created|built|authored)\b|\bcreator\s+of\b|\bwho\s+(?:created|built|authored|set\s+up)\s+(?:it|this|that|the)\b/i;
|
|
56876
|
+
PROVENANCE_CREATE_DATE = /\bcreation\s+date\b|\binstall\s+date\b/i;
|
|
56877
|
+
PROVENANCE_LASTMOD_ONLY = /\bwho\s+(?:last\s+(?:modified|changed|updated|edited)|modified|changed|updated|edited)\b/i;
|
|
56878
|
+
PROVENANCE_DISCLOSURE = "HONEST GAP (authorship): the vault records LastModified (who/when) where the refresh captured it, but never CreatedBy / original author / creation date \u2014 Salesforce does not expose creator provenance in metadata source. For the answerable side, sfi.last_modified reports who LAST changed a component and when.";
|
|
55688
56879
|
RUNTIME_TRIGGERS = [
|
|
55689
56880
|
[/\blogin\s+history\b|\baudit\s+trail\s+of\s+logins?\b/i, "login history"],
|
|
55690
56881
|
// Per-user LOGIN EVENTS (hon-031/hon-036/hon-060): who logged in when,
|
|
@@ -55770,6 +56961,15 @@ var init_refusal_gates = __esm({
|
|
|
55770
56961
|
/\bactual\s+content\s+of\s+the\s+(?:emails?|messages?|texts?)\b/i,
|
|
55771
56962
|
"the content of individually sent messages (the TEMPLATE body is metadata \u2014 sfi.get_component reads it)"
|
|
55772
56963
|
],
|
|
56964
|
+
// Chatter/feed RUNTIME activity (q4450 "who posted last in the … chatter
|
|
56965
|
+
// group"): who posted, most recent post, feed content — CollaborationGroup
|
|
56966
|
+
// feed data that lives in the runtime org, never the vault. Metadata reads
|
|
56967
|
+
// stay routed: "which public groups exist" / "who is IN the group"
|
|
56968
|
+
// (live_group_members) carry no post/feed verb.
|
|
56969
|
+
[
|
|
56970
|
+
/\bwho\s+(?:posted|commented|last\s+posted)\b|\b(?:chatter|feed)\b[^.?!]{0,40}\b(?:posts?|activity|comments?)\b|\b(?:last|latest|recent)\s+(?:chatter|feed)\s+posts?\b/i,
|
|
56971
|
+
"Chatter/feed activity (who posted, most recent posts)"
|
|
56972
|
+
],
|
|
55773
56973
|
// Site/community WEB ANALYTICS (hon-050): click paths, page views.
|
|
55774
56974
|
[
|
|
55775
56975
|
/\b(?:click|page|site|web)\s*[- ]?(?:analytics|traffic)\b|\bpage\s+views?\b|\bwhich\s+pages\b[^.?!]{0,60}\bvisit/i,
|
|
@@ -55834,8 +57034,9 @@ var init_refusal_gates = __esm({
|
|
|
55834
57034
|
return { kind: "injection-exfiltration", disclosure: INJECTION_DISCLOSURE };
|
|
55835
57035
|
}
|
|
55836
57036
|
if (!WRITE_EXCLUDER.test(q2)) {
|
|
55837
|
-
const
|
|
55838
|
-
const
|
|
57037
|
+
const readFrame = WRITE_READ_FRAME.test(q2);
|
|
57038
|
+
const simulationFrame = SIMULATION_TAIL.test(q2) || readFrame;
|
|
57039
|
+
const verbPhrase = simulationFrame ? void 0 : (WRITE_SENTENCE_INITIAL.exec(q2) ?? WRITE_LEAD_IN.exec(q2) ?? WRITE_TRAILING_FRAME.exec(q2) ?? WRITE_MAKE_SCHEMA.exec(q2) ?? WRITE_GIVE_GRANT.exec(q2) ?? WRITE_GIVE_INITIAL.exec(q2) ?? WRITE_CHAINED_DUPE_DELETE.exec(q2) ?? WRITE_CHAINED_DEPLOY.exec(q2) ?? WRITE_CHECK_IN.exec(q2))?.[1]?.trim().replace(/[,;:]$/, "");
|
|
55839
57040
|
if (verbPhrase !== void 0 && verbPhrase.length > 0) {
|
|
55840
57041
|
const alternative = readOnlyAlternativeFor(verbPhrase, q2);
|
|
55841
57042
|
return {
|
|
@@ -55857,6 +57058,16 @@ var init_refusal_gates = __esm({
|
|
|
55857
57058
|
if ((IDENTITY_IDIOM.test(q2) || IDENTITY_ALLOWED_MUTATE.test(q2)) && !IDENTITY_LOOK_EXCLUDER.test(q2)) {
|
|
55858
57059
|
return { kind: "identity-gap", disclosure: IDENTITY_DISCLOSURE };
|
|
55859
57060
|
}
|
|
57061
|
+
if (!SIMULATION_TAIL.test(q2)) {
|
|
57062
|
+
const isForecast = FORECAST_VERB.test(q2) || FORECAST_TREND.test(q2) && FORECAST_HORIZON.test(q2) && FORECAST_OUTCOME.test(q2) || FORECAST_TREND.test(q2) && FORECAST_STRONG_OUTCOME.test(q2);
|
|
57063
|
+
if (isForecast) {
|
|
57064
|
+
return { kind: "forecast-gap", disclosure: FORECAST_DISCLOSURE };
|
|
57065
|
+
}
|
|
57066
|
+
}
|
|
57067
|
+
const provenanceCreate = PROVENANCE_CREATE.test(q2) || PROVENANCE_CREATE_DATE.test(q2) && /\bcreated?\b/i.test(q2);
|
|
57068
|
+
if (provenanceCreate && !(PROVENANCE_LASTMOD_ONLY.test(q2) && !PROVENANCE_CREATE.test(q2))) {
|
|
57069
|
+
return { kind: "provenance-gap", disclosure: PROVENANCE_DISCLOSURE };
|
|
57070
|
+
}
|
|
55860
57071
|
const runtimeTopic = RUNTIME_TRIGGERS.find(([pattern]) => pattern.test(q2))?.[1] ?? (RUNTIME_WINDOW.test(q2) && RUNTIME_INCIDENT.test(q2) ? "runtime incident forensics" : void 0);
|
|
55861
57072
|
if (runtimeTopic !== void 0) {
|
|
55862
57073
|
return { kind: "runtime-analytics", disclosure: runtimeDisclosure(runtimeTopic) };
|
|
@@ -55936,7 +57147,13 @@ var init_funnel_utterances = __esm({
|
|
|
55936
57147
|
"is <ComponentName> even the right name? prove it actually exists",
|
|
55937
57148
|
"wait, is <Name__c> a field or an object?",
|
|
55938
57149
|
"pull up <ComponentName> \u2014 I might have the name spelled wrong",
|
|
55939
|
-
"does the <ComponentName> component actually exist in this org, yes or no?"
|
|
57150
|
+
"does the <ComponentName> component actually exist in this org, yes or no?",
|
|
57151
|
+
// R4 show-me corpus (DIAGNOSIS-R4 §2 lever 2): "show me / pull up / open X"
|
|
57152
|
+
// where X is a bare informal name that must be RESOLVED first — the display
|
|
57153
|
+
// verb over an unqualified nickname reaches resolve.
|
|
57154
|
+
"show me the <nickname> thing \u2014 I'm not sure of its exact name",
|
|
57155
|
+
"pull up whatever we call <nickname> around here",
|
|
57156
|
+
"open the one that sounds like <fuzzy-name>"
|
|
55940
57157
|
],
|
|
55941
57158
|
"sfi.capabilities": [
|
|
55942
57159
|
"what can you actually do?",
|
|
@@ -56036,7 +57253,19 @@ var init_funnel_utterances = __esm({
|
|
|
56036
57253
|
"what is the <PermSet> permission set for? the name confuses me",
|
|
56037
57254
|
"is the <FlowName> flow active or dormant?",
|
|
56038
57255
|
"can you give me the raw metadata XML for the <CustomPermission> custom permission?",
|
|
56039
|
-
"pull up <ComponentName> so I can look at it"
|
|
57256
|
+
"pull up <ComponentName> so I can look at it",
|
|
57257
|
+
// R4 show-me corpus (DIAGNOSIS-R4 §2 lever 2): the "show me X / pull up X /
|
|
57258
|
+
// open X / bring up X / display X" families — display verbs over a single
|
|
57259
|
+
// NAMED component reach get_component. Generic vocabulary, no org tokens.
|
|
57260
|
+
"open the <FlowName> flow so I can see its definition",
|
|
57261
|
+
"bring up the <ApexClass> class for me",
|
|
57262
|
+
"display the metadata for the <PermSet> permission set",
|
|
57263
|
+
"let me see the <ComponentName> component in full",
|
|
57264
|
+
"open up <ComponentName> and show me everything about it",
|
|
57265
|
+
"can you show me what the <FlowName> flow contains?",
|
|
57266
|
+
"pull the definition of the <ComponentName> component",
|
|
57267
|
+
"I want to see the full <ApexClass> apex class source and metadata",
|
|
57268
|
+
"render the raw metadata for the <ComponentName> component"
|
|
56040
57269
|
],
|
|
56041
57270
|
"sfi.list_components": [
|
|
56042
57271
|
"whats on the Account object \u2014 give me the whole field list",
|
|
@@ -56059,7 +57288,17 @@ var init_funnel_utterances = __esm({
|
|
|
56059
57288
|
"pull the full custom permission inventory for the audit workpaper",
|
|
56060
57289
|
"can i see the validation rules on the <Object> object",
|
|
56061
57290
|
"is there validation on the <Object> object?",
|
|
56062
|
-
"show me all the invocable apex methods"
|
|
57291
|
+
"show me all the invocable apex methods",
|
|
57292
|
+
// R4 show-me corpus (DIAGNOSIS-R4 §2 lever 2): "show me / pull up / open /
|
|
57293
|
+
// bring up ALL the X" enumeration phrasings over a component TYPE reach
|
|
57294
|
+
// list_components. Generic vocabulary, no org tokens.
|
|
57295
|
+
"open the list of every flow in this org",
|
|
57296
|
+
"bring up all the apex classes for me \u2014 the whole inventory",
|
|
57297
|
+
"display all the custom objects we have",
|
|
57298
|
+
"let me see everything of type permission set",
|
|
57299
|
+
"pull up the full list of validation rules",
|
|
57300
|
+
"show me the whole inventory of layouts",
|
|
57301
|
+
"bring up the entire list of triggers in the org"
|
|
56063
57302
|
],
|
|
56064
57303
|
"sfi.get_edges": [
|
|
56065
57304
|
"what are the edges between <ApexClass> and the objects it touches?",
|
|
@@ -56286,6 +57525,58 @@ var init_funnel_utterances = __esm({
|
|
|
56286
57525
|
"which license types do we have and how many are used?",
|
|
56287
57526
|
"how many Salesforce licenses are actually being used vs assigned?"
|
|
56288
57527
|
],
|
|
57528
|
+
// ENGINE-ARC §2a — assignment rosters (holders of a grantor). Vocabulary is
|
|
57529
|
+
// the HOLDER direction (who has / assigned to / roster / everyone with);
|
|
57530
|
+
// the GRANTS direction ("which permission sets grant edit on X") stays owned
|
|
57531
|
+
// by sfi.effective_permissions — do not add grants-phrasings here.
|
|
57532
|
+
"sfi.live_permset_holders": [
|
|
57533
|
+
"who has the <PermSet> permission set?",
|
|
57534
|
+
"list everyone assigned the <PermSet> permission set",
|
|
57535
|
+
"which users hold the <PermSetGroup> permission set group?",
|
|
57536
|
+
"show me the roster of users with the <PermSet> permission set",
|
|
57537
|
+
"how many users are assigned <PermSet> right now \u2014 directly or through a group?",
|
|
57538
|
+
"name-by-name list of users with the <Profile> profile",
|
|
57539
|
+
"which users are in the <PermSetGroup> permission set group?",
|
|
57540
|
+
"who holds <PermSet>, including via permission set groups?",
|
|
57541
|
+
"all users assigned the System Administrator profile",
|
|
57542
|
+
"holder count for the <PermSet> permission set \u2014 I need it for an audit"
|
|
57543
|
+
],
|
|
57544
|
+
// ENGINE-ARC §2c — the perm-set-less sweep (anti-join). Dormancy-only
|
|
57545
|
+
// phrasings ("who hasn't logged in") stay owned by sfi.live_inactive_users.
|
|
57546
|
+
"sfi.live_zombie_accounts": [
|
|
57547
|
+
"which active users have no permission sets assigned?",
|
|
57548
|
+
"find zombie accounts \u2014 login access but zero permission sets",
|
|
57549
|
+
"users who can log in but have no permission set assignments",
|
|
57550
|
+
"which users have nothing assigned beyond their profile?",
|
|
57551
|
+
"active accounts with zero permission set or group assignments",
|
|
57552
|
+
"audit accounts that can still log in but hold no perm sets"
|
|
57553
|
+
],
|
|
57554
|
+
// ENGINE-ARC §2b — runtime queue/public-group membership. "Which queues are
|
|
57555
|
+
// EMPTY" (hygiene sweep over declared metadata) stays owned by
|
|
57556
|
+
// sfi.empty_queues_and_groups; this family is the who's-in-it roster.
|
|
57557
|
+
"sfi.live_group_members": [
|
|
57558
|
+
"who's in the <QueueName> queue right now?",
|
|
57559
|
+
"list the members of the <GroupName> public group",
|
|
57560
|
+
"which users are in the Support queue?",
|
|
57561
|
+
"show me the current membership of the <QueueName> queue",
|
|
57562
|
+
"can the <QueueName> queue own Case records?",
|
|
57563
|
+
"what objects does the <QueueName> queue support?",
|
|
57564
|
+
"members of the <GroupName> group \u2014 including any nested groups or roles",
|
|
57565
|
+
"who is actually in this queue in the live org, not the vault snapshot?"
|
|
57566
|
+
],
|
|
57567
|
+
// ENGINE-ARC §3 — reverse direction: what a named USER holds. The GRANTS
|
|
57568
|
+
// direction ("what can <Profile> do", "expand the <PermSetGroup>") stays
|
|
57569
|
+
// owned by sfi.effective_permissions; this family is user → grantors.
|
|
57570
|
+
"sfi.live_user_permsets": [
|
|
57571
|
+
"what permission sets does <UserName> have?",
|
|
57572
|
+
"which permission sets is <UserName> assigned right now?",
|
|
57573
|
+
"list <UserName>'s permission set and permission set group assignments",
|
|
57574
|
+
"what does the user <UserName> hold \u2014 direct sets and via groups?",
|
|
57575
|
+
"which permission set groups is <UserName> a member of?",
|
|
57576
|
+
"show me every permission set assigned to <UserName>, with expirations",
|
|
57577
|
+
"does <UserName> have any expiring permission set assignments?",
|
|
57578
|
+
"what perm sets is jane.doe@example.com assigned in the live org?"
|
|
57579
|
+
],
|
|
56289
57580
|
"sfi.live_consent": [
|
|
56290
57581
|
"which Contacts have opted out of email?",
|
|
56291
57582
|
"show me consent status for marketing contacts",
|
|
@@ -56446,9 +57737,13 @@ var init_funnel_utterances = __esm({
|
|
|
56446
57737
|
"effective read/write/delete access for <Profile> on Order",
|
|
56447
57738
|
// R3 PSG-expansion band (DIAGNOSIS-R3 permissions family): what a member
|
|
56448
57739
|
// of a permission set GROUP ends up with — effective_permissions expands
|
|
56449
|
-
// PSGs (verified).
|
|
56450
|
-
//
|
|
56451
|
-
//
|
|
57740
|
+
// PSGs (verified). "Which USERS are in the group / hold the set"
|
|
57741
|
+
// (permset-user-roster) is now answered LIVE by sfi.live_permset_holders
|
|
57742
|
+
// (ENGINE-ARC §2a) — that tool owns the holder-roster vocabulary.
|
|
57743
|
+
// permset-group-grants is a PARTIAL live flip (§4): live_permset_holders
|
|
57744
|
+
// surfaces PSG containment via PermissionSetGroupComponent, while the
|
|
57745
|
+
// 2-hop "which PSG grants custom permission X" chain stays an honest gap
|
|
57746
|
+
// note on that intent.
|
|
56452
57747
|
"what does a user get through the <PermSetGroup> permission set group?",
|
|
56453
57748
|
"expand the <PermSetGroup> permission set group \u2014 every permission a member ends up with",
|
|
56454
57749
|
"if someone is assigned the <PermSetGroup> permission set group, do they automatically get <CustomPermission>?",
|
|
@@ -57494,7 +58789,17 @@ var init_funnel_utterances = __esm({
|
|
|
57494
58789
|
"i need the granting perm set for the <CustomPermission> custom permission",
|
|
57495
58790
|
"how could a user have gotten the <CustomPermission> custom permission?",
|
|
57496
58791
|
"what perm set gives access to the <ComponentName> component?",
|
|
57497
|
-
"who is allowed to bypass the validation \u2014 which custom permission guards it and who holds it?"
|
|
58792
|
+
"who is allowed to bypass the validation \u2014 which custom permission guards it and who holds it?",
|
|
58793
|
+
// R4 custom-permission VERIFY-FIRST band (DIAGNOSIS-R4 lever 3, the 5 R3
|
|
58794
|
+
// residuals): a NAMED custom permission where the ask combines "what does
|
|
58795
|
+
// it control" WITH "who has / who grants it", or verifies a specific
|
|
58796
|
+
// granting perm set ("granted by X or a different one"). These name a
|
|
58797
|
+
// specific custom permission and want the reverse-grant answer.
|
|
58798
|
+
"the <CustomPermission> custom permission \u2014 what does it control and who has it?",
|
|
58799
|
+
"does the <CustomPermission> custom permission get granted by the <PermSet> perm set or a different one?",
|
|
58800
|
+
"which permission set enables the <CustomPermission> custom permission, and what does that permission gate?",
|
|
58801
|
+
"for the <CustomPermission> custom permission, tell me both the granting permission set and what it lets people do",
|
|
58802
|
+
"is <CustomPermission> actually granted by <PermSet>, or does the grant come from somewhere else?"
|
|
57498
58803
|
],
|
|
57499
58804
|
"sfi.installed_package_catalog": [
|
|
57500
58805
|
"what managed packages are installed in this org?",
|
|
@@ -57543,7 +58848,7 @@ var init_funnel_utterances = __esm({
|
|
|
57543
58848
|
});
|
|
57544
58849
|
|
|
57545
58850
|
// ../mcp/dist/src/semantic-funnel.js
|
|
57546
|
-
var STOPWORDS, SYNONYMS, PHRASE_SYNONYMS3, applyPhraseSynonyms3, tokenize6, TOOL_KEYWORDS, PLANE_OVERRIDES, planeForTool, EXCLUDED_FROM_CANDIDATES, liveRequiredForPlane, buildPlaneByTool, cachedPlanes, getPlaneByTool, resolveCandidatePlane, EXPANSION_WEIGHT, expandWeighted, buildToolDocs, cached, buildIndex, getFunnelIndex, CONF_HIGH_TOP1, CONF_HIGH_MARGIN, CONF_HIGH_COVERAGE, CONF_FLOOR_TOP1, CONF_MIN_COVERAGE, CONF_LOW_MARGIN, CONF_RESCUE_TOP1, CONF_RESCUE_COVERAGE, CONF_LOW_COVERAGE, funnelConfidence, semanticCandidates;
|
|
58851
|
+
var STOPWORDS, SYNONYMS, PHRASE_SYNONYMS3, applyPhraseSynonyms3, tokenize6, TOOL_KEYWORDS, PLANE_OVERRIDES, planeForTool, EXCLUDED_FROM_CANDIDATES, liveRequiredForPlane, buildPlaneByTool, cachedPlanes, getPlaneByTool, resolveCandidatePlane, EXPANSION_WEIGHT, expandWeighted, CORPUS_EXCLUDED, buildToolDocs, cached, buildIndex, getFunnelIndex, CONF_HIGH_TOP1, CONF_HIGH_MARGIN, CONF_HIGH_COVERAGE, CONF_FLOOR_TOP1, CONF_MIN_COVERAGE, CONF_LOW_MARGIN, CONF_RESCUE_TOP1, CONF_RESCUE_COVERAGE, CONF_LOW_COVERAGE, funnelConfidence, semanticCandidates;
|
|
57547
58852
|
var init_semantic_funnel = __esm({
|
|
57548
58853
|
"../mcp/dist/src/semantic-funnel.js"() {
|
|
57549
58854
|
"use strict";
|
|
@@ -57652,6 +58957,15 @@ var init_semantic_funnel = __esm({
|
|
|
57652
58957
|
many: ["count", "number"],
|
|
57653
58958
|
show: ["sample", "records"],
|
|
57654
58959
|
sample: ["records", "show"],
|
|
58960
|
+
// R4 show-me corpus (DIAGNOSIS-R4 §2 lever 2): display verbs → fetch/list the
|
|
58961
|
+
// component's metadata. EXPANSION_WEIGHT keeps originals dominant, so these
|
|
58962
|
+
// only tip a question that is ALREADY display-shaped ("open X", "pull up X",
|
|
58963
|
+
// "bring up X", "display X") toward get_component / list_components without
|
|
58964
|
+
// hijacking real intents.
|
|
58965
|
+
open: ["fetch", "component", "metadata", "definition"],
|
|
58966
|
+
display: ["fetch", "component", "metadata", "list"],
|
|
58967
|
+
bring: ["fetch", "component", "metadata", "list"],
|
|
58968
|
+
pull: ["fetch", "component", "metadata"],
|
|
57655
58969
|
// change / audit / freshness
|
|
57656
58970
|
changed: ["modified", "change", "history", "audit"],
|
|
57657
58971
|
change: ["modified", "history", "changed"],
|
|
@@ -57953,9 +59267,12 @@ var init_semantic_funnel = __esm({
|
|
|
57953
59267
|
}
|
|
57954
59268
|
return w2;
|
|
57955
59269
|
};
|
|
59270
|
+
CORPUS_EXCLUDED = /* @__PURE__ */ new Set(["sfi.route_question"]);
|
|
57956
59271
|
buildToolDocs = () => {
|
|
57957
59272
|
const docs = /* @__PURE__ */ new Map();
|
|
57958
59273
|
for (const tool of V01_TOOLS) {
|
|
59274
|
+
if (CORPUS_EXCLUDED.has(tool.name))
|
|
59275
|
+
continue;
|
|
57959
59276
|
const nameWords = tool.name.replace(/^sfi\./, "").replace(/_/g, " ");
|
|
57960
59277
|
const keywords = TOOL_KEYWORDS[tool.name] ?? "";
|
|
57961
59278
|
const utterances = (FUNNEL_UTTERANCES[tool.name] ?? []).join(" ");
|
|
@@ -58133,7 +59450,7 @@ import { createHash as createHash7 } from "node:crypto";
|
|
|
58133
59450
|
import { readFile as readFile82 } from "node:fs/promises";
|
|
58134
59451
|
import { join as join23, resolve as resolve3 } from "node:path";
|
|
58135
59452
|
import { z as z115 } from "zod";
|
|
58136
|
-
var RESOLVE_FALLBACK_MAX_TOKENS, SAVE_ORDER_INTENTS, SINGLE_ENTITY_INTENTS, COMPARISON_ASIDE, stripComparisonAside, tryResolveFallback, routeQuestionInputSchema, routeTrust, clarificationIdFor, selectedEntityArgsForRoute, isToolNameMention, extractEntityQuery, NAME_IGNORABLE_WORD, looksLikeComponentName, extractExistenceProbeToken, inferEntityTypes, normLiteral, refineEntityResolution, resolveParentQualifier, applyGlossaryAliases, questionAssertsNameMultiplicity, entityAmbiguityRequiresClarification, hygienicClarificationOptions, splitCompoundQuestion, mixedInventoryAndStoragePlan, PLAN_FAMILY, ASSESSMENT_FAMILY, ROUTE_PREAMBLE_TOOLS, TOOL_COMPATIBLE_TYPES, ACCESS_FLAVORED_TOOLS, resolvedTypeForGuard, applyComponentTypeGuard, REGEX_BONUS, resolveActiveVaultAlias, buildRouteToolArgsMap, mergeRouteHintsIntoCandidates, invokeFromArgsMap, rerankForMode, MARGIN, FUNNEL_PRIMARY_MIN_SCORE, DESTRUCTIVE_TOOL, INFORMATIONAL_IMPACT_TOOL, planesDiverge, LIVE_DATA_SIGNAL, resolvePlaneTie, isCompoundPlaneAsk, risksDiverge, marginClarification, LIVE_DISCLOSURE_LOOKAHEAD, liveConsentDisclosure, guidanceForMode, buildFunnelCandidates, routerMode, REFUSAL_INTENT, REFUSAL_GAP, refusalResponse, CONTEXT_SAVE_ORDER_TOOLS, CONTEXT_TOOL_ARG_KEYS, GENERIC_SCHEMA_INTENTS, contextArgsFor, buildContextContinuation, routeQuestionHandler;
|
|
59453
|
+
var RESOLVE_FALLBACK_MAX_TOKENS, SAVE_ORDER_INTENTS, SINGLE_ENTITY_INTENTS, COMPARISON_ASIDE, stripComparisonAside, tryResolveFallback, routeQuestionInputSchema, routeTrust, clarificationIdFor, selectedEntityArgsForRoute, isToolNameMention, extractEntityQuery, NAME_IGNORABLE_WORD, looksLikeComponentName, extractExistenceProbeToken, inferEntityTypes, normLiteral, refineEntityResolution, resolveParentQualifier, applyGlossaryAliases, questionAssertsNameMultiplicity, entityAmbiguityRequiresClarification, hygienicClarificationOptions, clarificationOptionIds, splitCompoundQuestion, mixedInventoryAndStoragePlan, PLAN_FAMILY, ASSESSMENT_FAMILY, ROUTE_PREAMBLE_TOOLS, SINGLE_COMPONENT_TARGET_TOOLS, routeTargetsSingleComponent, TOOL_COMPATIBLE_TYPES, ACCESS_FLAVORED_TOOLS, resolvedTypeForGuard, applyComponentTypeGuard, REGEX_BONUS, resolveActiveVaultAlias, buildRouteToolArgsMap, mergeRouteHintsIntoCandidates, invokeFromArgsMap, rerankForMode, MARGIN, FUNNEL_PRIMARY_MIN_SCORE, DESTRUCTIVE_TOOL, INFORMATIONAL_IMPACT_TOOL, planesDiverge, LIVE_DATA_SIGNAL, resolvePlaneTie, isCompoundPlaneAsk, risksDiverge, marginClarification, LIVE_DISCLOSURE_LOOKAHEAD, liveConsentDisclosure, guidanceForMode, buildFunnelCandidates, routerMode, REFUSAL_INTENT, REFUSAL_GAP, refusalResponse, CONTEXT_SAVE_ORDER_TOOLS, CONTEXT_TOOL_ARG_KEYS, GENERIC_SCHEMA_INTENTS, contextArgsFor, buildContextContinuation, routeQuestionHandler;
|
|
58137
59454
|
var init_route_question = __esm({
|
|
58138
59455
|
"../mcp/dist/src/tools/route-question.js"() {
|
|
58139
59456
|
"use strict";
|
|
@@ -58478,7 +59795,7 @@ var init_route_question = __esm({
|
|
|
58478
59795
|
};
|
|
58479
59796
|
};
|
|
58480
59797
|
questionAssertsNameMultiplicity = (question) => /\b(?:a\s+few|several|a\s+couple)\s+of\s+(?:these|those|them)\b|\b(?:two|three|four|five|\d+)\s+different\s+(?:things?|ones?|fields?|objects?|components?|versions?)\b|\bmore\s+than\s+one\s+(?:of\s+(?:these|those|them)|with\s+(?:that|this)\s+name)\b|\bwhich\s+one\s+(?:do|should|did)\s+i\b/i.test(question);
|
|
58481
|
-
entityAmbiguityRequiresClarification = (query, resolution, question) => {
|
|
59798
|
+
entityAmbiguityRequiresClarification = (query, resolution, question, routeTargetsSingleComponentTool = false) => {
|
|
58482
59799
|
if (resolution.disposition === "exact" && resolution.candidates.length >= 2 && !/__|\./.test(query)) {
|
|
58483
59800
|
const [top2, second2] = resolution.candidates;
|
|
58484
59801
|
const sameName = top2 !== void 0 && second2 !== void 0 && top2.matchKind === "exact" && second2.matchKind === "exact" && top2.base === second2.base && top2.apiName.toLowerCase() === second2.apiName.toLowerCase() && top2.parentApiName !== second2.parentApiName;
|
|
@@ -58503,7 +59820,16 @@ var init_route_question = __esm({
|
|
|
58503
59820
|
return false;
|
|
58504
59821
|
const [top, second] = resolution.candidates;
|
|
58505
59822
|
const looksLikeNamedComponent = (candidate) => candidate.matchKind !== "fuzzy" && (candidate.nameCoverage ?? 1) >= 0.5;
|
|
58506
|
-
|
|
59823
|
+
if (top.base >= 0.92 && second.base >= 0.92 && second.score >= top.score * 0.97 && looksLikeNamedComponent(top) && looksLikeNamedComponent(second)) {
|
|
59824
|
+
return true;
|
|
59825
|
+
}
|
|
59826
|
+
if (routeTargetsSingleComponentTool) {
|
|
59827
|
+
const sameName = top.id !== second.id && top.apiName.toLowerCase() === second.apiName.toLowerCase();
|
|
59828
|
+
if (sameName && top.base >= 0.85 && second.base >= 0.85 && second.score >= top.score * 0.8) {
|
|
59829
|
+
return true;
|
|
59830
|
+
}
|
|
59831
|
+
}
|
|
59832
|
+
return false;
|
|
58507
59833
|
};
|
|
58508
59834
|
hygienicClarificationOptions = (candidates) => {
|
|
58509
59835
|
const top = candidates[0];
|
|
@@ -58512,6 +59838,21 @@ var init_route_question = __esm({
|
|
|
58512
59838
|
const kept = candidates.filter((candidate) => candidate.matchKind !== "fuzzy" && candidate.base >= top.base * 0.9);
|
|
58513
59839
|
return kept.length > 0 && kept[0] === top ? kept : [top, ...kept.filter((c2) => c2 !== top)];
|
|
58514
59840
|
};
|
|
59841
|
+
clarificationOptionIds = (candidates) => {
|
|
59842
|
+
const hygienic = hygienicClarificationOptions(candidates.map((candidate) => ({
|
|
59843
|
+
componentId: candidate.id,
|
|
59844
|
+
base: candidate.base,
|
|
59845
|
+
matchKind: candidate.matchKind
|
|
59846
|
+
}))).map((candidate) => candidate.componentId);
|
|
59847
|
+
if (hygienic.length >= 2)
|
|
59848
|
+
return hygienic;
|
|
59849
|
+
const top = candidates[0];
|
|
59850
|
+
if (top === void 0)
|
|
59851
|
+
return hygienic;
|
|
59852
|
+
const topName = top.apiName.toLowerCase();
|
|
59853
|
+
const sameName = candidates.filter((candidate) => candidate.apiName.toLowerCase() === topName).map((candidate) => candidate.id);
|
|
59854
|
+
return sameName.length >= 2 ? sameName : hygienic;
|
|
59855
|
+
};
|
|
58515
59856
|
splitCompoundQuestion = (question) => {
|
|
58516
59857
|
const parts = question.split(/(\?|;|\band\s+then\b|\bthen\b|\band\s+(?=who|what|which|how|where|when|why|is|are|can|should|does|do)\b)/i);
|
|
58517
59858
|
const clauses = [];
|
|
@@ -58560,6 +59901,34 @@ var init_route_question = __esm({
|
|
|
58560
59901
|
PLAN_FAMILY = /^sfi\.(what_if_|get_impact|safe_to_delete|downstream_effects|tests_for_change|field_lineage)/;
|
|
58561
59902
|
ASSESSMENT_FAMILY = /(_risk_report$|^sfi\.release_readiness|^sfi\.promotion_readiness|^sfi\.coverage_report|^sfi\.tech_debt_score|^sfi\.governor_limit_risks|^sfi\.crud_fls_audit)/;
|
|
58562
59903
|
ROUTE_PREAMBLE_TOOLS = /* @__PURE__ */ new Set(["sfi.resolve", "sfi.capabilities"]);
|
|
59904
|
+
SINGLE_COMPONENT_TARGET_TOOLS = /* @__PURE__ */ new Set([
|
|
59905
|
+
"sfi.explain_flow",
|
|
59906
|
+
"sfi.explain_apex_method",
|
|
59907
|
+
"sfi.explain_field",
|
|
59908
|
+
"sfi.explain_formula",
|
|
59909
|
+
"sfi.get_component",
|
|
59910
|
+
"sfi.downstream_effects",
|
|
59911
|
+
"sfi.component_history",
|
|
59912
|
+
"sfi.call_graph",
|
|
59913
|
+
"sfi.method_reachability",
|
|
59914
|
+
"sfi.safe_to_delete_field",
|
|
59915
|
+
"sfi.who_can_run",
|
|
59916
|
+
"sfi.field_360",
|
|
59917
|
+
"sfi.field_lineage",
|
|
59918
|
+
"sfi.what_happens_on_save",
|
|
59919
|
+
"sfi.order_of_execution",
|
|
59920
|
+
"sfi.field_access_audit",
|
|
59921
|
+
"sfi.object_access_audit",
|
|
59922
|
+
"sfi.who_can_access_object",
|
|
59923
|
+
"sfi.what_if_deactivate_flow",
|
|
59924
|
+
"sfi.what_if_disable_trigger"
|
|
59925
|
+
]);
|
|
59926
|
+
routeTargetsSingleComponent = (route) => {
|
|
59927
|
+
const primary = route.tools.find((tool) => !ROUTE_PREAMBLE_TOOLS.has(tool));
|
|
59928
|
+
if (primary === void 0)
|
|
59929
|
+
return false;
|
|
59930
|
+
return SINGLE_COMPONENT_TARGET_TOOLS.has(primary) || primary.startsWith("sfi.what_if_");
|
|
59931
|
+
};
|
|
58563
59932
|
TOOL_COMPATIBLE_TYPES = /* @__PURE__ */ new Map([
|
|
58564
59933
|
["sfi.call_graph", /* @__PURE__ */ new Set(["ApexClass", "ApexTrigger"])],
|
|
58565
59934
|
["sfi.method_reachability", /* @__PURE__ */ new Set(["ApexClass", "ApexTrigger"])],
|
|
@@ -58793,6 +60162,8 @@ var init_route_question = __esm({
|
|
|
58793
60162
|
"write-imperative": "refused-write",
|
|
58794
60163
|
"injection-exfiltration": "refused-injection",
|
|
58795
60164
|
"identity-gap": "honest-gap-identity",
|
|
60165
|
+
"forecast-gap": "honest-gap-forecast",
|
|
60166
|
+
"provenance-gap": "honest-gap-provenance",
|
|
58796
60167
|
"runtime-analytics": "honest-gap-runtime",
|
|
58797
60168
|
"out-of-scope": "out-of-scope"
|
|
58798
60169
|
};
|
|
@@ -58809,6 +60180,14 @@ var init_route_question = __esm({
|
|
|
58809
60180
|
category: "identity-gap",
|
|
58810
60181
|
note: "first-person capability ask; no session-user identity \u2014 declined with a which-user clarify pointer"
|
|
58811
60182
|
},
|
|
60183
|
+
"forecast-gap": {
|
|
60184
|
+
category: "forecast-gap",
|
|
60185
|
+
note: "future/forecast ask; no time-series or forecasting model \u2014 honest gap, current snapshot offered"
|
|
60186
|
+
},
|
|
60187
|
+
"provenance-gap": {
|
|
60188
|
+
category: "provenance-gap",
|
|
60189
|
+
note: "creator/authorship ask; vault has LastModified, never CreatedBy \u2014 honest gap disclosed"
|
|
60190
|
+
},
|
|
58812
60191
|
"runtime-analytics": {
|
|
58813
60192
|
category: "runtime-analytics",
|
|
58814
60193
|
note: "runtime/ops telemetry the product does not model; honest gap disclosed"
|
|
@@ -59086,7 +60465,7 @@ ${renderContextApplied(contextApplied2)}`
|
|
|
59086
60465
|
const entityClarificationRequired = entityQuery !== null && refinedEntityResolution !== null && // component-type asks ("is X a flow or a trigger?") are EXEMPT: a
|
|
59087
60466
|
// same-name cross-type collision is the ANSWER to that question, not an
|
|
59088
60467
|
// obstacle — sfi.resolve enumerates the types and the host explains.
|
|
59089
|
-
route.intent !== "component-type" && entityAmbiguityRequiresClarification(entityQuery, refinedEntityResolution, input2.question);
|
|
60468
|
+
route.intent !== "component-type" && entityAmbiguityRequiresClarification(entityQuery, refinedEntityResolution, input2.question, routeTargetsSingleComponent(route));
|
|
59090
60469
|
let entityEvidence = refinedEntityResolution !== null && refinedEntityResolution.disposition !== "none" ? {
|
|
59091
60470
|
query: entityQuery,
|
|
59092
60471
|
typeHints: entityTypes,
|
|
@@ -59112,9 +60491,10 @@ ${renderContextApplied(contextApplied2)}`
|
|
|
59112
60491
|
clarification: {
|
|
59113
60492
|
required: true,
|
|
59114
60493
|
question: "Several components match the named entity. Which component did you mean?",
|
|
59115
|
-
// P4 option hygiene
|
|
59116
|
-
//
|
|
59117
|
-
|
|
60494
|
+
// P4 option hygiene (fuzzy grazes / far-below-top rivals are junk),
|
|
60495
|
+
// with the R4 same-name fallback so a genuine same-name family whose
|
|
60496
|
+
// match reads `fuzzy` (the narrow-clarify shape) still offers ≥2 picks.
|
|
60497
|
+
options: clarificationOptionIds(refinedEntityResolution?.candidates ?? [])
|
|
59118
60498
|
}
|
|
59119
60499
|
};
|
|
59120
60500
|
} else if (entityEvidence?.disposition === "ambiguous" && route.clarification === null) {
|
|
@@ -59333,7 +60713,8 @@ ${renderContextApplied(contextApplied2)}`
|
|
|
59333
60713
|
judgment: "a normative judgment (should/normal/risky) \u2014 sf-intelligence reads org metadata and has no opinion to offer",
|
|
59334
60714
|
delivery: "file export or message delivery, which the product does not perform",
|
|
59335
60715
|
"tool-self-capability": "the product's own capability boundary \u2014 sfi.capabilities answers what is and is not built",
|
|
59336
|
-
"deployment-status": "deployment/pending-change status, runtime telemetry the vault does not model"
|
|
60716
|
+
"deployment-status": "deployment/pending-change status, runtime telemetry the vault does not model",
|
|
60717
|
+
"runtime-analytics": "runtime org data (login history, API-call logs, approval-instance history, record field-history, subscription recipients, chatter posts, sandbox-refresh/deployment logs, or execution traces) \u2014 this is live runtime telemetry, not vault metadata"
|
|
59337
60718
|
};
|
|
59338
60719
|
const carried = previous.componentId !== void 0 ? ` The component carried from the previous turn (${previous.componentId}) was noted but the previous tool was NOT inherited \u2014 inheriting it would execute a read that cannot answer this ask.` : "";
|
|
59339
60720
|
route = {
|
|
@@ -59368,7 +60749,14 @@ ${renderContextApplied(contextApplied2)}`
|
|
|
59368
60749
|
const refinedTarget = refineEntityResolution(phrase, resolvedTarget.value);
|
|
59369
60750
|
if (refinedTarget.disposition === "exact") {
|
|
59370
60751
|
target = refinedTarget.candidates[0];
|
|
59371
|
-
} else if (refinedTarget.disposition === "ambiguous" && entityAmbiguityRequiresClarification(
|
|
60752
|
+
} else if (refinedTarget.disposition === "ambiguous" && entityAmbiguityRequiresClarification(
|
|
60753
|
+
phrase,
|
|
60754
|
+
refinedTarget,
|
|
60755
|
+
void 0,
|
|
60756
|
+
// The re-parameterization inherits previous.tool; the widened
|
|
60757
|
+
// R4 band applies when THAT tool is single-component-target.
|
|
60758
|
+
previous.tool !== void 0 && (SINGLE_COMPONENT_TARGET_TOOLS.has(previous.tool) || previous.tool.startsWith("sfi.what_if_"))
|
|
60759
|
+
)) {
|
|
59372
60760
|
route = {
|
|
59373
60761
|
...route,
|
|
59374
60762
|
confidence: "low",
|
|
@@ -68118,7 +69506,7 @@ __export(tools_exports, {
|
|
|
68118
69506
|
toolProfile: () => toolProfile
|
|
68119
69507
|
});
|
|
68120
69508
|
import { CallToolRequestSchema, ListToolsRequestSchema } from "@modelcontextprotocol/sdk/types.js";
|
|
68121
|
-
var SEARCH_COMPONENTS_INPUT_SCHEMA, CAPABILITIES_INPUT_SCHEMA, SYNTHESIZE_ANSWER_INPUT_SCHEMA, ORG_PULSE_INPUT_SCHEMA, ORG_CARD_INPUT_SCHEMA, LIST_ANALYSES_INPUT_SCHEMA, DESCRIBE_ANALYSIS_INPUT_SCHEMA, RUN_ANALYSIS_INPUT_SCHEMA, FLEET_FIND_INPUT_SCHEMA, FLEET_DRIFT_RANKING_INPUT_SCHEMA, RESOLVE_INPUT_SCHEMA, GET_COMPONENT_INPUT_SCHEMA, GET_EDGES_INPUT_SCHEMA, LIST_COMPONENTS_INPUT_SCHEMA, GET_SUBGRAPH_INPUT_SCHEMA, SEARCH_APEX_SOURCE_INPUT_SCHEMA, SEARCH_FLOW_METADATA_INPUT_SCHEMA, NAMING_CONVENTION_REPORT_INPUT_SCHEMA, GET_MANIFEST_INPUT_SCHEMA, COVERAGE_REPORT_INPUT_SCHEMA, RETRIEVE_BLINDSPOT_REPORT_INPUT_SCHEMA, BASELINE_ACKNOWLEDGE_INPUT_SCHEMA, BASELINE_STATUS_INPUT_SCHEMA, TREND_INPUT_SCHEMA, CHURN_INPUT_SCHEMA, LIVE_ENABLED_PROPERTY, LIVE_DESCRIBE_INPUT_SCHEMA, LIVE_COUNT_INPUT_SCHEMA, LIVE_STALE_CHECK_INPUT_SCHEMA, LIVE_SAMPLE_INPUT_SCHEMA, LIVE_FIELD_POPULATION_INPUT_SCHEMA, LIVE_ORG_LIMITS_INPUT_SCHEMA, LIVE_PICKLIST_USAGE_INPUT_SCHEMA, LIVE_AUTOMATION_FIRED_INPUT_SCHEMA, LIVE_INACTIVE_USERS_INPUT_SCHEMA, LIVE_LICENSE_USAGE_INPUT_SCHEMA, LIVE_GROUP_COUNT_INPUT_SCHEMA, LIVE_STALE_RECORDS_INPUT_SCHEMA, LIVE_RECENT_ACTIVITY_INPUT_SCHEMA, LIVE_AGGREGATE_INPUT_SCHEMA, LIVE_DUPLICATE_CHECK_INPUT_SCHEMA, LIVE_OWNER_BREAKDOWN_INPUT_SCHEMA, LIVE_STORAGE_BY_OBJECT_INPUT_SCHEMA, LIVE_REPORT_USAGE_INPUT_SCHEMA, LIVE_FOLDER_ACCESS_INPUT_SCHEMA, LIVE_EMAIL_TEMPLATE_USAGE_INPUT_SCHEMA, LIVE_ORG_HEALTH_INPUT_SCHEMA, LIVE_CONSENT_INPUT_SCHEMA, ROUTE_QUESTION_INPUT_SCHEMA, SYNTHESIS_INPUT_SCHEMA, PERMISSION_RISK_REPORT_INPUT_SCHEMA, HEALTH_CHECK_INPUT_SCHEMA, GET_IMPACT_INPUT_SCHEMA, BLAST_RADIUS_LIVE_INPUT_SCHEMA, FIND_FORMULA_REFERENCES_INPUT_SCHEMA, FIND_APEX_USAGES_INPUT_SCHEMA, FIND_CODE_USAGES_INPUT_SCHEMA, EFFECTIVE_PERMISSIONS_INPUT_SCHEMA, LIST_VIEW_SHARING_INPUT_SCHEMA, WHO_CAN_RUN_INPUT_SCHEMA, WHO_CAN_ACCESS_OBJECT_INPUT_SCHEMA, WHY_CANT_USER_SEE_RECORD_INPUT_SCHEMA, LAYOUT_FOR_USER_INPUT_SCHEMA, LAYOUT_ASSIGNMENTS_INPUT_SCHEMA, USER_ABILITY_INPUT_SCHEMA, PROFILE_SECURITY_INPUT_SCHEMA, LIGHTNING_PAGES_INPUT_SCHEMA, APP_ACCESS_INPUT_SCHEMA, TAB_AVAILABILITY_INPUT_SCHEMA, LIFECYCLE_PROCESS_INPUT_SCHEMA, INTEGRATION_MAP_INPUT_SCHEMA, EVENT_SUBSCRIBERS_INPUT_SCHEMA, LOOKUP_RECORD_INPUT_SCHEMA, GUIDANCE_INPUT_SCHEMA, EXPLAIN_FIELD_INPUT_SCHEMA, SAFE_TO_DELETE_FIELD_INPUT_SCHEMA, UNUSED_COMPONENTS_INPUT_SCHEMA, DIFF_SNAPSHOTS_INPUT_SCHEMA, COMPARE_COMPONENTS_INPUT_SCHEMA, PII_INVENTORY_INPUT_SCHEMA, FIELD_ACCESS_AUDIT_INPUT_SCHEMA, OBJECT_ACCESS_AUDIT_INPUT_SCHEMA, RECORDTYPE_AVAILABILITY_INPUT_SCHEMA, FIELD_360_INPUT_SCHEMA, FIELD_LINEAGE_INPUT_SCHEMA, ORG_OVERVIEW_INPUT_SCHEMA, DOMAIN_CLUSTERS_INPUT_SCHEMA, CHANGED_SINCE_INPUT_SCHEMA, LAST_MODIFIED_INPUT_SCHEMA, WHAT_HAPPENS_ON_SAVE_INPUT_SCHEMA, WHY_FIELD_CHANGED_INPUT_SCHEMA, ORDER_OF_EXECUTION_INPUT_SCHEMA, EXPLAIN_FLOW_INPUT_SCHEMA, EXPLAIN_APEX_METHOD_INPUT_SCHEMA, EXPLAIN_FORMULA_INPUT_SCHEMA, EXPORT_MANIFEST_INPUT_SCHEMA, UNUSED_FIELDS_DEEP_INPUT_SCHEMA, FIELD_CLEANUP_CANDIDATES_INPUT_SCHEMA, PROCESS_BUILDER_MIGRATION_CANDIDATES_INPUT_SCHEMA, UNASSIGNED_PERMISSION_SETS_INPUT_SCHEMA, INSTALLED_PACKAGE_CATALOG_INPUT_SCHEMA, ANNOTATIONS_INPUT_SCHEMA, PROPOSE_ANNOTATION_INPUT_SCHEMA, COMPONENT_HISTORY_INPUT_SCHEMA, COMPONENT_AS_OF_INPUT_SCHEMA, FIND_COMPONENT_USAGES_INPUT_SCHEMA, EMPTY_QUEUES_AND_GROUPS_INPUT_SCHEMA, TECH_DEBT_SCORE_INPUT_SCHEMA, CODE_QUALITY_AUDIT_INPUT_SCHEMA, GOVERNOR_LIMIT_RISKS_INPUT_SCHEMA, FIND_HARDCODED_VALUES_INPUT_SCHEMA, CRUD_FLS_AUDIT_INPUT_SCHEMA, TEST_COVERAGE_GAPS_INPUT_SCHEMA, WHAT_IF_CHANGE_FIELD_VALUE_INPUT_SCHEMA, VALUE_CHANGE_AUDIT_INPUT_SCHEMA, WHAT_IF_CHANGE_FIELD_TYPE_INPUT_SCHEMA, WHAT_IF_REMOVE_PICKLIST_VALUE_INPUT_SCHEMA, WHAT_IF_MAKE_FIELD_REQUIRED_INPUT_SCHEMA, WHAT_IF_DEACTIVATE_FLOW_INPUT_SCHEMA, WHAT_IF_DISABLE_TRIGGER_INPUT_SCHEMA, WHAT_IF_CHANGE_METHOD_SIGNATURE_INPUT_SCHEMA, WHAT_IF_MERGE_PROFILES_INPUT_SCHEMA, WHAT_IF_SPLIT_PROFILE_INPUT_SCHEMA, GENERATE_DATA_DICTIONARY_INPUT_SCHEMA, GENERATE_ADMIN_HANDBOOK_INPUT_SCHEMA, GENERATE_ARCHITECTURE_OVERVIEW_INPUT_SCHEMA, GENERATE_SHARING_SUMMARY_INPUT_SCHEMA, GENERATE_COMPLIANCE_REPORT_INPUT_SCHEMA, GENERATE_ONBOARDING_DOC_INPUT_SCHEMA, CALL_GRAPH_INPUT_SCHEMA, DOWNSTREAM_EFFECTS_INPUT_SCHEMA, TEST_COVERAGE_FOR_METHOD_INPUT_SCHEMA, MEANINGFUL_TEST_AUDIT_INPUT_SCHEMA, METHOD_REACHABILITY_INPUT_SCHEMA, TESTS_FOR_CHANGE_INPUT_SCHEMA, PACKAGE_IMPACT_INPUT_SCHEMA, CDC_SUBSCRIBERS_INPUT_SCHEMA, ASYNC_CHAIN_DEPTH_INPUT_SCHEMA, SCHEDULED_JOB_CATALOG_INPUT_SCHEMA, OUTBOUND_MESSAGE_CATALOG_INPUT_SCHEMA, ENDPOINT_CATALOG_INPUT_SCHEMA, FIELD_MEANING_INPUT_SCHEMA, DISAMBIGUATE_CONCEPTS_INPUT_SCHEMA, FIELD_PROVENANCE_INPUT_SCHEMA, FIND_FIELD_ANYWHERE_INPUT_SCHEMA, FIND_SEMANTIC_FIELD_INPUT_SCHEMA, FIND_HARDCODED_VALUES_ANYWHERE_INPUT_SCHEMA, FIND_CLONE_PATTERNS_INPUT_SCHEMA, FIND_DEAD_CODE_INPUT_SCHEMA, CPQ_RULE_CHAIN_INPUT_SCHEMA, CPQ_QUOTE_TEMPLATE_BREAKDOWN_INPUT_SCHEMA, CPQ_DEPENDENCY_MAP_INPUT_SCHEMA, COMPARE_VAULTS_INPUT_SCHEMA, PROMOTION_READINESS_INPUT_SCHEMA, COMPARE_OBJECT_ACROSS_VAULTS_INPUT_SCHEMA, COMPARE_PROFILE_ACROSS_VAULTS_INPUT_SCHEMA, FIELD_MAPPING_BETWEEN_OBJECTS_INPUT_SCHEMA, INTEGRATION_PROCEDURE_CHAIN_INPUT_SCHEMA, OMNISCRIPT_FLOW_INPUT_SCHEMA, OMNIUICARD_WIDGET_BREAKDOWN_INPUT_SCHEMA, DATATRANSFORM_FIELD_MAP_INPUT_SCHEMA, DECISION_TABLE_BROWSE_INPUT_SCHEMA, FIND_DEPENDENCY_CYCLES_INPUT_SCHEMA, APEX_TEST_COVERAGE_INPUT_SCHEMA, AUTOMATION_BUILD_ADVISOR_INPUT_SCHEMA, APEX_BUILD_ADVISOR_INPUT_SCHEMA, FIELD_CHANGE_ADVISOR_INPUT_SCHEMA, LIVE_DRIFT_CHECK_INPUT_SCHEMA, ORG_HISTORY_INPUT_SCHEMA, WHAT_CHANGED_SINCE_REFRESH_INPUT_SCHEMA, V01_TOOLS, KNOWN_TOOL_NAMES, dispatchTool, runTool, advertisedTools, registerTools, MAX_RESPONSE_BYTES, RESPONSE_BUDGET_DEFAULT_BYTES, responseBudgetBytes, SLIM_STRING_THRESHOLD_BYTES2, SLIM_STRING_KEEP_CHARS2, TRUNCATE_KEEP_MIN, utf8Bytes2, truncateDataArrays, slimDataStrings, jsonResult, NARROWING_KNOB_RE, narrowingKnobs;
|
|
69509
|
+
var SEARCH_COMPONENTS_INPUT_SCHEMA, CAPABILITIES_INPUT_SCHEMA, SYNTHESIZE_ANSWER_INPUT_SCHEMA, ORG_PULSE_INPUT_SCHEMA, ORG_CARD_INPUT_SCHEMA, LIST_ANALYSES_INPUT_SCHEMA, DESCRIBE_ANALYSIS_INPUT_SCHEMA, RUN_ANALYSIS_INPUT_SCHEMA, FLEET_FIND_INPUT_SCHEMA, FLEET_DRIFT_RANKING_INPUT_SCHEMA, RESOLVE_INPUT_SCHEMA, GET_COMPONENT_INPUT_SCHEMA, GET_EDGES_INPUT_SCHEMA, LIST_COMPONENTS_INPUT_SCHEMA, GET_SUBGRAPH_INPUT_SCHEMA, SEARCH_APEX_SOURCE_INPUT_SCHEMA, SEARCH_FLOW_METADATA_INPUT_SCHEMA, NAMING_CONVENTION_REPORT_INPUT_SCHEMA, GET_MANIFEST_INPUT_SCHEMA, COVERAGE_REPORT_INPUT_SCHEMA, RETRIEVE_BLINDSPOT_REPORT_INPUT_SCHEMA, BASELINE_ACKNOWLEDGE_INPUT_SCHEMA, BASELINE_STATUS_INPUT_SCHEMA, TREND_INPUT_SCHEMA, CHURN_INPUT_SCHEMA, LIVE_ENABLED_PROPERTY, LIVE_DESCRIBE_INPUT_SCHEMA, LIVE_COUNT_INPUT_SCHEMA, LIVE_STALE_CHECK_INPUT_SCHEMA, LIVE_SAMPLE_INPUT_SCHEMA, LIVE_FIELD_POPULATION_INPUT_SCHEMA, LIVE_ORG_LIMITS_INPUT_SCHEMA, LIVE_PICKLIST_USAGE_INPUT_SCHEMA, LIVE_AUTOMATION_FIRED_INPUT_SCHEMA, LIVE_INACTIVE_USERS_INPUT_SCHEMA, LIVE_PERMSET_HOLDERS_INPUT_SCHEMA, LIVE_ZOMBIE_ACCOUNTS_INPUT_SCHEMA, LIVE_GROUP_MEMBERS_INPUT_SCHEMA, LIVE_USER_PERMSETS_INPUT_SCHEMA, LIVE_LICENSE_USAGE_INPUT_SCHEMA, LIVE_GROUP_COUNT_INPUT_SCHEMA, LIVE_STALE_RECORDS_INPUT_SCHEMA, LIVE_RECENT_ACTIVITY_INPUT_SCHEMA, LIVE_AGGREGATE_INPUT_SCHEMA, LIVE_DUPLICATE_CHECK_INPUT_SCHEMA, LIVE_OWNER_BREAKDOWN_INPUT_SCHEMA, LIVE_STORAGE_BY_OBJECT_INPUT_SCHEMA, LIVE_REPORT_USAGE_INPUT_SCHEMA, LIVE_FOLDER_ACCESS_INPUT_SCHEMA, LIVE_EMAIL_TEMPLATE_USAGE_INPUT_SCHEMA, LIVE_ORG_HEALTH_INPUT_SCHEMA, LIVE_CONSENT_INPUT_SCHEMA, ROUTE_QUESTION_INPUT_SCHEMA, SYNTHESIS_INPUT_SCHEMA, PERMISSION_RISK_REPORT_INPUT_SCHEMA, HEALTH_CHECK_INPUT_SCHEMA, GET_IMPACT_INPUT_SCHEMA, BLAST_RADIUS_LIVE_INPUT_SCHEMA, FIND_FORMULA_REFERENCES_INPUT_SCHEMA, FIND_APEX_USAGES_INPUT_SCHEMA, FIND_CODE_USAGES_INPUT_SCHEMA, EFFECTIVE_PERMISSIONS_INPUT_SCHEMA, LIST_VIEW_SHARING_INPUT_SCHEMA, WHO_CAN_RUN_INPUT_SCHEMA, WHO_CAN_ACCESS_OBJECT_INPUT_SCHEMA, WHY_CANT_USER_SEE_RECORD_INPUT_SCHEMA, LAYOUT_FOR_USER_INPUT_SCHEMA, LAYOUT_ASSIGNMENTS_INPUT_SCHEMA, USER_ABILITY_INPUT_SCHEMA, PROFILE_SECURITY_INPUT_SCHEMA, LIGHTNING_PAGES_INPUT_SCHEMA, APP_ACCESS_INPUT_SCHEMA, TAB_AVAILABILITY_INPUT_SCHEMA, LIFECYCLE_PROCESS_INPUT_SCHEMA, INTEGRATION_MAP_INPUT_SCHEMA, EVENT_SUBSCRIBERS_INPUT_SCHEMA, LOOKUP_RECORD_INPUT_SCHEMA, GUIDANCE_INPUT_SCHEMA, EXPLAIN_FIELD_INPUT_SCHEMA, SAFE_TO_DELETE_FIELD_INPUT_SCHEMA, UNUSED_COMPONENTS_INPUT_SCHEMA, DIFF_SNAPSHOTS_INPUT_SCHEMA, COMPARE_COMPONENTS_INPUT_SCHEMA, PII_INVENTORY_INPUT_SCHEMA, FIELD_ACCESS_AUDIT_INPUT_SCHEMA, OBJECT_ACCESS_AUDIT_INPUT_SCHEMA, RECORDTYPE_AVAILABILITY_INPUT_SCHEMA, FIELD_360_INPUT_SCHEMA, FIELD_LINEAGE_INPUT_SCHEMA, ORG_OVERVIEW_INPUT_SCHEMA, DOMAIN_CLUSTERS_INPUT_SCHEMA, CHANGED_SINCE_INPUT_SCHEMA, LAST_MODIFIED_INPUT_SCHEMA, WHAT_HAPPENS_ON_SAVE_INPUT_SCHEMA, WHY_FIELD_CHANGED_INPUT_SCHEMA, ORDER_OF_EXECUTION_INPUT_SCHEMA, EXPLAIN_FLOW_INPUT_SCHEMA, EXPLAIN_APEX_METHOD_INPUT_SCHEMA, EXPLAIN_FORMULA_INPUT_SCHEMA, EXPORT_MANIFEST_INPUT_SCHEMA, UNUSED_FIELDS_DEEP_INPUT_SCHEMA, FIELD_CLEANUP_CANDIDATES_INPUT_SCHEMA, PROCESS_BUILDER_MIGRATION_CANDIDATES_INPUT_SCHEMA, UNASSIGNED_PERMISSION_SETS_INPUT_SCHEMA, INSTALLED_PACKAGE_CATALOG_INPUT_SCHEMA, ANNOTATIONS_INPUT_SCHEMA, PROPOSE_ANNOTATION_INPUT_SCHEMA, COMPONENT_HISTORY_INPUT_SCHEMA, COMPONENT_AS_OF_INPUT_SCHEMA, FIND_COMPONENT_USAGES_INPUT_SCHEMA, EMPTY_QUEUES_AND_GROUPS_INPUT_SCHEMA, TECH_DEBT_SCORE_INPUT_SCHEMA, CODE_QUALITY_AUDIT_INPUT_SCHEMA, GOVERNOR_LIMIT_RISKS_INPUT_SCHEMA, FIND_HARDCODED_VALUES_INPUT_SCHEMA, CRUD_FLS_AUDIT_INPUT_SCHEMA, TEST_COVERAGE_GAPS_INPUT_SCHEMA, WHAT_IF_CHANGE_FIELD_VALUE_INPUT_SCHEMA, VALUE_CHANGE_AUDIT_INPUT_SCHEMA, WHAT_IF_CHANGE_FIELD_TYPE_INPUT_SCHEMA, WHAT_IF_REMOVE_PICKLIST_VALUE_INPUT_SCHEMA, WHAT_IF_MAKE_FIELD_REQUIRED_INPUT_SCHEMA, WHAT_IF_DEACTIVATE_FLOW_INPUT_SCHEMA, WHAT_IF_DISABLE_TRIGGER_INPUT_SCHEMA, WHAT_IF_CHANGE_METHOD_SIGNATURE_INPUT_SCHEMA, WHAT_IF_MERGE_PROFILES_INPUT_SCHEMA, WHAT_IF_SPLIT_PROFILE_INPUT_SCHEMA, GENERATE_DATA_DICTIONARY_INPUT_SCHEMA, GENERATE_ADMIN_HANDBOOK_INPUT_SCHEMA, GENERATE_ARCHITECTURE_OVERVIEW_INPUT_SCHEMA, GENERATE_SHARING_SUMMARY_INPUT_SCHEMA, GENERATE_COMPLIANCE_REPORT_INPUT_SCHEMA, GENERATE_ONBOARDING_DOC_INPUT_SCHEMA, CALL_GRAPH_INPUT_SCHEMA, DOWNSTREAM_EFFECTS_INPUT_SCHEMA, TEST_COVERAGE_FOR_METHOD_INPUT_SCHEMA, MEANINGFUL_TEST_AUDIT_INPUT_SCHEMA, METHOD_REACHABILITY_INPUT_SCHEMA, TESTS_FOR_CHANGE_INPUT_SCHEMA, PACKAGE_IMPACT_INPUT_SCHEMA, CDC_SUBSCRIBERS_INPUT_SCHEMA, ASYNC_CHAIN_DEPTH_INPUT_SCHEMA, SCHEDULED_JOB_CATALOG_INPUT_SCHEMA, OUTBOUND_MESSAGE_CATALOG_INPUT_SCHEMA, ENDPOINT_CATALOG_INPUT_SCHEMA, FIELD_MEANING_INPUT_SCHEMA, DISAMBIGUATE_CONCEPTS_INPUT_SCHEMA, FIELD_PROVENANCE_INPUT_SCHEMA, FIND_FIELD_ANYWHERE_INPUT_SCHEMA, FIND_SEMANTIC_FIELD_INPUT_SCHEMA, FIND_HARDCODED_VALUES_ANYWHERE_INPUT_SCHEMA, FIND_CLONE_PATTERNS_INPUT_SCHEMA, FIND_DEAD_CODE_INPUT_SCHEMA, CPQ_RULE_CHAIN_INPUT_SCHEMA, CPQ_QUOTE_TEMPLATE_BREAKDOWN_INPUT_SCHEMA, CPQ_DEPENDENCY_MAP_INPUT_SCHEMA, COMPARE_VAULTS_INPUT_SCHEMA, PROMOTION_READINESS_INPUT_SCHEMA, COMPARE_OBJECT_ACROSS_VAULTS_INPUT_SCHEMA, COMPARE_PROFILE_ACROSS_VAULTS_INPUT_SCHEMA, FIELD_MAPPING_BETWEEN_OBJECTS_INPUT_SCHEMA, INTEGRATION_PROCEDURE_CHAIN_INPUT_SCHEMA, OMNISCRIPT_FLOW_INPUT_SCHEMA, OMNIUICARD_WIDGET_BREAKDOWN_INPUT_SCHEMA, DATATRANSFORM_FIELD_MAP_INPUT_SCHEMA, DECISION_TABLE_BROWSE_INPUT_SCHEMA, FIND_DEPENDENCY_CYCLES_INPUT_SCHEMA, APEX_TEST_COVERAGE_INPUT_SCHEMA, AUTOMATION_BUILD_ADVISOR_INPUT_SCHEMA, APEX_BUILD_ADVISOR_INPUT_SCHEMA, FIELD_CHANGE_ADVISOR_INPUT_SCHEMA, LIVE_DRIFT_CHECK_INPUT_SCHEMA, ORG_HISTORY_INPUT_SCHEMA, WHAT_CHANGED_SINCE_REFRESH_INPUT_SCHEMA, V01_TOOLS, KNOWN_TOOL_NAMES, dispatchTool, runTool, advertisedTools, registerTools, MAX_RESPONSE_BYTES, RESPONSE_BUDGET_DEFAULT_BYTES, responseBudgetBytes, SLIM_STRING_THRESHOLD_BYTES2, SLIM_STRING_KEEP_CHARS2, TRUNCATE_KEEP_MIN, utf8Bytes2, truncateDataArrays, slimDataStrings, jsonResult, NARROWING_KNOB_RE, narrowingKnobs;
|
|
68122
69510
|
var init_tools = __esm({
|
|
68123
69511
|
"../mcp/dist/src/tools/index.js"() {
|
|
68124
69512
|
"use strict";
|
|
@@ -68490,7 +69878,11 @@ var init_tools = __esm({
|
|
|
68490
69878
|
hasFutureMethod: { type: "boolean" },
|
|
68491
69879
|
hasInvocableMethod: { type: "boolean" },
|
|
68492
69880
|
hasAuraEnabledMethod: { type: "boolean" },
|
|
68493
|
-
isTest: { type: "boolean" }
|
|
69881
|
+
isTest: { type: "boolean" },
|
|
69882
|
+
// Description-presence filters. Answer "which <type> have no description".
|
|
69883
|
+
// Only meaningful for a type whose extractor captures `<description>`.
|
|
69884
|
+
missingDescription: { type: "boolean" },
|
|
69885
|
+
hasDescription: { type: "boolean" }
|
|
68494
69886
|
}
|
|
68495
69887
|
});
|
|
68496
69888
|
GET_SUBGRAPH_INPUT_SCHEMA = Object.freeze({
|
|
@@ -68657,6 +70049,83 @@ var init_tools = __esm({
|
|
|
68657
70049
|
...LIVE_ENABLED_PROPERTY
|
|
68658
70050
|
}
|
|
68659
70051
|
});
|
|
70052
|
+
LIVE_PERMSET_HOLDERS_INPUT_SCHEMA = Object.freeze({
|
|
70053
|
+
type: "object",
|
|
70054
|
+
required: ["name"],
|
|
70055
|
+
properties: {
|
|
70056
|
+
name: {
|
|
70057
|
+
type: "string",
|
|
70058
|
+
minLength: 1,
|
|
70059
|
+
description: "Exact PermissionSet.Name / PermissionSetGroup DeveloperName / Profile.Name (labels accepted)."
|
|
70060
|
+
},
|
|
70061
|
+
kind: {
|
|
70062
|
+
type: "string",
|
|
70063
|
+
enum: ["permissionSet", "permissionSetGroup", "profile", "auto"],
|
|
70064
|
+
description: "What `name` names. Default 'auto' probes PermissionSet \u2192 PermissionSetGroup \u2192 Profile by exact name/label; no-match or ambiguity errors honestly."
|
|
70065
|
+
},
|
|
70066
|
+
includeInactiveAssignees: { type: "boolean" },
|
|
70067
|
+
includeViaGroups: {
|
|
70068
|
+
type: "boolean",
|
|
70069
|
+
description: "For kind permissionSet: also count holders receiving the set through a PermissionSetGroup containing it (default true)."
|
|
70070
|
+
},
|
|
70071
|
+
groupBy: { type: "string", enum: ["none", "profile"] },
|
|
70072
|
+
limit: { type: "integer", minimum: 1, maximum: 500 },
|
|
70073
|
+
afterId: {
|
|
70074
|
+
type: "string",
|
|
70075
|
+
description: "Keyset paging token \u2014 the `nextAfterId` from the previous page."
|
|
70076
|
+
},
|
|
70077
|
+
...LIVE_ENABLED_PROPERTY
|
|
70078
|
+
}
|
|
70079
|
+
});
|
|
70080
|
+
LIVE_ZOMBIE_ACCOUNTS_INPUT_SCHEMA = Object.freeze({
|
|
70081
|
+
type: "object",
|
|
70082
|
+
properties: {
|
|
70083
|
+
minDaysInactive: {
|
|
70084
|
+
type: "integer",
|
|
70085
|
+
minimum: 0,
|
|
70086
|
+
maximum: 3650,
|
|
70087
|
+
description: "Additionally require last login older than N days (default 0 = ignore login age)."
|
|
70088
|
+
},
|
|
70089
|
+
includeAllUserTypes: { type: "boolean" },
|
|
70090
|
+
limit: { type: "integer", minimum: 1, maximum: 500 },
|
|
70091
|
+
...LIVE_ENABLED_PROPERTY
|
|
70092
|
+
}
|
|
70093
|
+
});
|
|
70094
|
+
LIVE_GROUP_MEMBERS_INPUT_SCHEMA = Object.freeze({
|
|
70095
|
+
type: "object",
|
|
70096
|
+
required: ["name"],
|
|
70097
|
+
properties: {
|
|
70098
|
+
name: {
|
|
70099
|
+
type: "string",
|
|
70100
|
+
minLength: 1,
|
|
70101
|
+
description: "Exact Group DeveloperName or Name (label) of the queue / public group."
|
|
70102
|
+
},
|
|
70103
|
+
groupType: {
|
|
70104
|
+
type: "string",
|
|
70105
|
+
enum: ["Queue", "Regular", "auto"],
|
|
70106
|
+
description: "What kind of Group `name` names. Default 'auto' matches both; an ambiguous name across both errors honestly."
|
|
70107
|
+
},
|
|
70108
|
+
expandNested: {
|
|
70109
|
+
type: "boolean",
|
|
70110
|
+
description: "Expand exactly ONE level of nested public groups (default false). Role entries are never expanded."
|
|
70111
|
+
},
|
|
70112
|
+
limit: { type: "integer", minimum: 1, maximum: 500 },
|
|
70113
|
+
...LIVE_ENABLED_PROPERTY
|
|
70114
|
+
}
|
|
70115
|
+
});
|
|
70116
|
+
LIVE_USER_PERMSETS_INPUT_SCHEMA = Object.freeze({
|
|
70117
|
+
type: "object",
|
|
70118
|
+
required: ["user"],
|
|
70119
|
+
properties: {
|
|
70120
|
+
user: {
|
|
70121
|
+
type: "string",
|
|
70122
|
+
minLength: 1,
|
|
70123
|
+
description: "Exact Username (preferred \u2014 unique) or exact User.Name. An ambiguous name returns candidates, never a guess."
|
|
70124
|
+
},
|
|
70125
|
+
limit: { type: "integer", minimum: 1, maximum: 500 },
|
|
70126
|
+
...LIVE_ENABLED_PROPERTY
|
|
70127
|
+
}
|
|
70128
|
+
});
|
|
68660
70129
|
LIVE_LICENSE_USAGE_INPUT_SCHEMA = Object.freeze({
|
|
68661
70130
|
type: "object",
|
|
68662
70131
|
properties: {
|
|
@@ -70281,7 +71750,7 @@ var init_tools = __esm({
|
|
|
70281
71750
|
},
|
|
70282
71751
|
{
|
|
70283
71752
|
name: "sfi.list_components",
|
|
70284
|
-
description: 'List components of a given type (optionally narrowed by parentId), sorted by id. Paginated via limit/offset; `hasMore` hints at additional pages (a truncated page returns a `nextCursor` to resume). Grant-heavy rows (Profile / PermissionSet, whose nodes carry tens of KB of declarative grants) are slimmed to scalar properties \u2014 each such row is marked `properties.propertiesTruncated: true` and the page carries a top-level `propertiesSlimmed: true` \u2014 so the whole inventory fits per page; fetch full detail per component via sfi.get_component. For `type: \'ApexClass\'`, optional boolean filters list interface/async/API implementers at the DB layer (correct pagination, not a post-filtered page): `isBatchable` / `isQueueable` / `isSchedulable` / `isRestResource` / `hasFutureMethod` / `hasInvocableMethod` / `hasAuraEnabledMethod` / `isTest` \u2014 e.g. `{ type: \'ApexClass\', isBatchable: true }` returns every Batchable class. When manifest coverage for the requested `type` is not `complete`, a structured `coverageCaveat` flags the inventory as potentially incomplete (scoped refresh, errored retrieve, not modeled) \u2014 including on non-empty pages. When the FIRST page is empty, a `retrievalHint` (FRESH-02) says WHY \u2014 "none in the org" (retrieved, none found) vs "not retrieved" (a scoped refresh skipped the type \u2014 run /sfi-refresh) vs "not modeled" \u2014 so an empty list is never a silent `[]` read as "the org has none". (The hint is suppressed when a boolean filter is active, since an empty filtered result is not a coverage gap.)',
|
|
71753
|
+
description: 'List components of a given type (optionally narrowed by parentId), sorted by id. Paginated via limit/offset; `hasMore` hints at additional pages (a truncated page returns a `nextCursor` to resume). Grant-heavy rows (Profile / PermissionSet, whose nodes carry tens of KB of declarative grants) are slimmed to scalar properties \u2014 each such row is marked `properties.propertiesTruncated: true` and the page carries a top-level `propertiesSlimmed: true` \u2014 so the whole inventory fits per page; fetch full detail per component via sfi.get_component. For `type: \'ApexClass\'`, optional boolean filters list interface/async/API implementers at the DB layer (correct pagination, not a post-filtered page): `isBatchable` / `isQueueable` / `isSchedulable` / `isRestResource` / `hasFutureMethod` / `hasInvocableMethod` / `hasAuraEnabledMethod` / `isTest` \u2014 e.g. `{ type: \'ApexClass\', isBatchable: true }` returns every Batchable class. `missingDescription: true` (or `hasDescription: true`) filters to components that lack (or carry) a non-empty `properties.description`, with `totalCount` as the authoritative tally \u2014 only meaningful for a type whose extractor captures a source `<description>` (a type that carries none in source will match ALL of its nodes, meaning "no description in source", not "left blank"). When manifest coverage for the requested `type` is not `complete`, a structured `coverageCaveat` flags the inventory as potentially incomplete (scoped refresh, errored retrieve, not modeled) \u2014 including on non-empty pages. When the FIRST page is empty, a `retrievalHint` (FRESH-02) says WHY \u2014 "none in the org" (retrieved, none found) vs "not retrieved" (a scoped refresh skipped the type \u2014 run /sfi-refresh) vs "not modeled" \u2014 so an empty list is never a silent `[]` read as "the org has none". (The hint is suppressed when a boolean filter is active, since an empty filtered result is not a coverage gap.)',
|
|
70285
71754
|
inputSchema: LIST_COMPONENTS_INPUT_SCHEMA
|
|
70286
71755
|
},
|
|
70287
71756
|
{
|
|
@@ -70316,7 +71785,7 @@ var init_tools = __esm({
|
|
|
70316
71785
|
},
|
|
70317
71786
|
{
|
|
70318
71787
|
name: "sfi.coverage_report",
|
|
70319
|
-
description: "Report the vault's self-assessed metadata coverage: covered, partial, not-modeled, and \u2014 during a staged refresh \u2014 pending families (queued by the in-progress tiered build, with `stagedBuild` tier progress; pending types count as missing coverage, so absence answers about them must stay qualified). Use before absence-based or destructive answers.",
|
|
71788
|
+
description: "Report the vault's self-assessed metadata coverage: covered, partial, not-modeled, and \u2014 during a staged refresh \u2014 pending families (queued by the in-progress tiered build, with `stagedBuild` tier progress; pending types count as missing coverage, so absence answers about them must stay qualified). Use before absence-based or destructive answers. The `assignmentData` section covers runtime assignment data (User / PermissionSetAssignment / GroupMember): NOT in the vault by design (a runtime data object, not a retrieve gap) \u2014 it names the consent-gated live tools that answer those questions (sfi.live_permset_holders, sfi.live_user_permsets, sfi.live_group_members, sfi.live_zombie_accounts), whether live consent is currently granted, and whether/when a counts-only facts snapshot (`sfi refresh --with-data-shape`) was captured.",
|
|
70320
71789
|
inputSchema: COVERAGE_REPORT_INPUT_SCHEMA
|
|
70321
71790
|
},
|
|
70322
71791
|
{
|
|
@@ -70326,7 +71795,7 @@ var init_tools = __esm({
|
|
|
70326
71795
|
},
|
|
70327
71796
|
{
|
|
70328
71797
|
name: "sfi.health_check",
|
|
70329
|
-
description: 'Report self-assessed server health, render consistency, and coverage completeness, plus a freshness block (vault age, a stale flag, the most recent refresh\'s change count, and a yellow-flag nudge when the vault is old or local source drifted). While a staged refresh (`sfi refresh --staged`) is mid-build, status is degraded with explicit tier progress ("building tier i/n") until the final tier clears the marker.',
|
|
71798
|
+
description: 'Report self-assessed server health, render consistency, and coverage completeness, plus a freshness block (vault age, a stale flag, the most recent refresh\'s change count, and a yellow-flag nudge when the vault is old or local source drifted). While a staged refresh (`sfi refresh --staged`) is mid-build, status is degraded with explicit tier progress ("building tier i/n") until the final tier clears the marker. Also carries an INFORMATIONAL `assignmentData` block (runtime assignment data is live-first by design \u2014 its absence never degrades status; a stale counts snapshot >30 days old earns an advisory only).',
|
|
70330
71799
|
inputSchema: HEALTH_CHECK_INPUT_SCHEMA
|
|
70331
71800
|
},
|
|
70332
71801
|
{
|
|
@@ -70406,9 +71875,29 @@ var init_tools = __esm({
|
|
|
70406
71875
|
},
|
|
70407
71876
|
{
|
|
70408
71877
|
name: "sfi.live_inactive_users",
|
|
70409
|
-
description: "Opt-in live org: active users who haven't logged in within N days (default 30) or never have \u2014 the license-reclamation / dormant-account question. Standard (human) users by default; reports the true total (`totalInactive`) plus a capped detail page, oldest-dormant first. `limit` pages the detail rows (default 100, hard cap 500) and a per-response ~36 KB byte budget trims the page further when a wide page would exceed it (the response carries both the structured rows and a rendered table, so it can't trip the global ~45 KB limit); `capped` flips true when more remain and a `note` appears when the page was byte-trimmed. Read-only; LastLoginDate is live-only state.",
|
|
71878
|
+
description: "Opt-in live org: active users who haven't logged in within N days (default 30) or never have \u2014 the license-reclamation / dormant-account question. Standard (human) users by default; reports the true total (`totalInactive`) plus a capped detail page, oldest-dormant first. `limit` pages the detail rows (default 100, hard cap 500) and a per-response ~36 KB byte budget trims the page further when a wide page would exceed it (the response carries both the structured rows and a rendered table, so it can't trip the global ~45 KB limit); `capped` flips true when more remain and a `note` appears when the page was byte-trimmed. Read-only; LastLoginDate is live-only state. Dormancy ONLY \u2014 for active users with login access but ZERO permission-set/PSG assignments (the perm-set-less variant of this sweep), use sfi.live_zombie_accounts.",
|
|
70410
71879
|
inputSchema: LIVE_INACTIVE_USERS_INPUT_SCHEMA
|
|
70411
71880
|
},
|
|
71881
|
+
{
|
|
71882
|
+
name: "sfi.live_permset_holders",
|
|
71883
|
+
description: "Opt-in live org: WHO HOLDS a permission set, permission set group, or profile \u2014 the name-by-name assignment roster (PermissionSetAssignment / User rows are runtime state, never in the vault; vault sfi.effective_permissions describes what a grantor GRANTS, not who holds it). `kind` defaults to 'auto' (probes PermissionSet \u2192 PermissionSetGroup \u2192 Profile by exact name/label; ambiguity or no-match is an honest error, never a guess). For a permission set the default `includeViaGroups: true` ALSO counts users who receive it through a PermissionSetGroup containing it (querying PermissionSetGroupComponent), reported separately as `directHolders` / `viaGroupHolders` with a deduped `totalAssignees` \u2014 direct-only rosters silently miss via-group holders (the classic wrong answer). kind 'profile' returns the User roster for the profile. Expired assignments are excluded and disclosed (`expiredExcluded`); active assignees only by default. True count first; `limit` (default 100, cap 500) + ~36 KB byte budget page the detail; keyset paging via `afterId`/`nextAfterId`; `groupBy: 'profile'` (default) adds per-profile buckets. Reverse direction (what does USER X hold) = sfi.live_user_permsets. Read-only; point-in-time as of queriedAt \u2014 never cached beyond the live-session TTL; roster is CURRENT org state, unlike vault answers.",
|
|
71884
|
+
inputSchema: LIVE_PERMSET_HOLDERS_INPUT_SCHEMA
|
|
71885
|
+
},
|
|
71886
|
+
{
|
|
71887
|
+
name: "sfi.live_zombie_accounts",
|
|
71888
|
+
description: 'Opt-in live org: ZOMBIE accounts \u2014 active users with login access but ZERO permission-set/PSG assignments, via a single SOQL anti-join (Id NOT IN PermissionSetAssignment WHERE PermissionSet.IsOwnedByProfile = false; the IsOwnedByProfile filter is load-bearing \u2014 every user carries a system PSA row for their profile-owned set). When the org rejects the anti-join it falls back to a DISCLOSED client-side diff of two bounded queries (`method: \'client-diff\'`, capped scans flagged as a lower bound). Optional `minDaysInactive` additionally requires dormancy; Standard (human) users by default. True count first (`totalZombies`); `limit` (default 100, cap 500) + ~36 KB byte budget page the detail. HONESTY: a "zombie" still holds every permission its PROFILE grants \u2014 this reports "no permission-set/PSG assignments", NOT "no access". This is the perm-set-less variant of sfi.live_inactive_users, which covers dormancy only (and for who DOES hold a set, use sfi.live_permset_holders). Read-only; point-in-time.',
|
|
71889
|
+
inputSchema: LIVE_ZOMBIE_ACCOUNTS_INPUT_SCHEMA
|
|
71890
|
+
},
|
|
71891
|
+
{
|
|
71892
|
+
name: "sfi.live_group_members",
|
|
71893
|
+
description: "Opt-in live org: WHO IS IN a queue or public group \u2014 the runtime GroupMember roster (metadata XML only carries DECLARED members, so the vault's member counts routinely say 0 while Setup-managed membership is live-only; vault sfi.empty_queues_and_groups reads the declared counts). Resolves the Group by exact DeveloperName/Name (`groupType` defaults to 'auto'; ambiguity or no-match is an honest error, never a guess). GroupMember.UserOrGroupId is polymorphic: users (005) are resolved name-by-name, nested groups (00G) are LISTED but not expanded, and Role / RoleAndSubordinates entries are surfaced by UserRole name and NEVER expanded to users. `expandNested: true` expands exactly ONE level of nested public groups, stamped `expansion: 'partial-one-level'` \u2014 deeper nesting and role-hierarchy subordinates are not enumerated; never treat a partial expansion as the full effective membership. Queues also return `supportedObjects` (QueueSobject \u2014 which sObjects the queue can own). Output cross-checks the vault: `vaultDeclaredMemberCount` vs `liveDirectMemberCount` with a `drift` boolean. True count first (`totalDirectMembers`); `limit` (default 100, cap 500) + ~36 KB byte budget page the user rows. Read-only; point-in-time as of queriedAt.",
|
|
71894
|
+
inputSchema: LIVE_GROUP_MEMBERS_INPUT_SCHEMA
|
|
71895
|
+
},
|
|
71896
|
+
{
|
|
71897
|
+
name: "sfi.live_user_permsets",
|
|
71898
|
+
description: "Opt-in live org: WHAT does USER X hold \u2014 the reverse of sfi.live_permset_holders (input = a user, output = the grantors they hold: profile + direct permission sets + permission set groups, with expirations). Resolves the user by exact Username (preferred \u2014 unique) then exact Name; an ambiguous name returns an honest candidate list, never a guess. PSA rows are split `directPermsets` vs `viaGroups` (grouped by PermissionSetGroup); the query pins PermissionSet.IsOwnedByProfile = false so the user's profile-owned system PSA row never masquerades as a direct assignment \u2014 the profile is reported as `user.profileName` instead. Expired assignments are excluded and disclosed (`expiredExcluded`). DUAL PROVENANCE: this answers WHICH grantors the user holds (live, point-in-time as of queriedAt); vault sfi.effective_permissions answers WHAT those grantors grant \u2014 pair them, each half stamped with its own provenance. True count first (`totalAssignments`); `limit` (default 200, cap 500) + ~36 KB byte budget page the detail. Read-only.",
|
|
71899
|
+
inputSchema: LIVE_USER_PERMSETS_INPUT_SCHEMA
|
|
71900
|
+
},
|
|
70412
71901
|
{
|
|
70413
71902
|
name: "sfi.live_license_usage",
|
|
70414
71903
|
description: "Opt-in live org: license / cost optimization. Returns UserLicense and PermissionSetLicense utilization (total / used / available / utilizationPct; unlimited licenses surface total: -1 \u2192 available/pct null) plus `reclaimableSeats` \u2014 active Standard users dormant past `inactiveDays` (default 90), grouped by their user license, the paid-seats-nobody-uses question. Honesty axis (verbatim): reclaimable seats is a PROXY (inactivity, not actual feature usage; some dormant seats are held intentionally; per-feature-license usage is not covered). READ-ONLY: never deprovisions or reassigns a license \u2014 verify each seat before reclaiming. License counts and LastLoginDate are live-only state.",
|
|
@@ -70501,7 +71990,7 @@ var init_tools = __esm({
|
|
|
70501
71990
|
},
|
|
70502
71991
|
{
|
|
70503
71992
|
name: "sfi.effective_permissions",
|
|
70504
|
-
description: "Compute a user's EFFECTIVE access \u2014 the UNION of a profile + assigned permission sets, max-wins, with each permission attributed to the container(s) that grant it. `why_cant_user_see_record` evaluates a single record question against a bundle; nothing else rolls the containers up into one combined ability \u2014 this does. Input: `profileId` and/or `permissionSetIds[]` (at least one). A `PermissionSetGroup:` id may be passed in `permissionSetIds[]` \u2014 it is EXPANDED into its member permission sets (declared membership) and unioned in, so a PSG-assigned user gets a real answer (a permset reachable both directly and via a group is unioned once, not double-counted). It composes each container's outgoing `grantedBy` edges (object + field + apex), `properties.userPermissions` (system perms), and `properties.recordTypeVisibilities` (record-type visibility). `objectPermissions[]` carries the OR'd `allowCreate`/`allowRead`/`allowEdit`/`allowDelete`/`viewAllRecords`/`modifyAllRecords` per object plus `grantedBy` (the containers contributing a flag); `systemPermissions[]` lists each user-permission with its `grantedBy`; `customPermissions[]` (CR-CAP-10) lists each granted custom permission with its `grantedBy` + `targetMissing` (true when the granted name has no `CustomPermission` definition in the vault \u2014 managed-package / not-retrieved; declared but not resolvable, and NOT folded into systemPermissions); `recordTypeVisibilities[]` unions each container's declared record-type visibility (max-wins \u2014 visible=true wins; `<visible>` omitted in older metadata counts as visible, only an explicit false hides), each entry `{recordType, visible, grantedBy}` \u2014 record-type visibility is part of THIS union now, no longer only the separate `recordtype_availability` surface (that tool remains for the per-object grouped view); a container carrying no extracted `recordTypeVisibilities` property (a vault refreshed before record-type extraction) contributes nothing and is DISCLOSED (re-run /sfi-refresh), never fabricated as 'no record types'; `summary` reports objects / fieldsWithFls / apexClasses / systemPermissions / customPermissions / recordTypeVisibilities counts. The object list PAGES (`limit` default 100 / max 200, `offset`/`hasMore`/`truncated`). `declared` confidence. `disclosures` is explicit about the boundaries: permission-set GROUP membership IS expanded, but muting permission sets are DISCLOSED, not subtracted (effective access may be lower); app/tab visibility is a SEPARATE surface (now extracted \u2014 see `app_access` / `tab_availability`), not part of this union; field-level detail is summarised (use `field_access_audit`); object permission is NOT record access (record visibility needs OWD + sharing); custom permissions are declared grants, NOT system userPermissions, so they are never double-counted. Missing containers are ignored with a disclosure; if none exist \u2192 `component-not-found`.",
|
|
71993
|
+
description: "Compute a user's EFFECTIVE access \u2014 the UNION of a profile + assigned permission sets, max-wins, with each permission attributed to the container(s) that grant it. `why_cant_user_see_record` evaluates a single record question against a bundle; nothing else rolls the containers up into one combined ability \u2014 this does. Input: `profileId` and/or `permissionSetIds[]` (at least one). A `PermissionSetGroup:` id may be passed in `permissionSetIds[]` \u2014 it is EXPANDED into its member permission sets (declared membership) and unioned in, so a PSG-assigned user gets a real answer (a permset reachable both directly and via a group is unioned once, not double-counted). It composes each container's outgoing `grantedBy` edges (object + field + apex), `properties.userPermissions` (system perms), and `properties.recordTypeVisibilities` (record-type visibility). `objectPermissions[]` carries the OR'd `allowCreate`/`allowRead`/`allowEdit`/`allowDelete`/`viewAllRecords`/`modifyAllRecords` per object plus `grantedBy` (the containers contributing a flag); `systemPermissions[]` lists each user-permission with its `grantedBy`; `customPermissions[]` (CR-CAP-10) lists each granted custom permission with its `grantedBy` + `targetMissing` (true when the granted name has no `CustomPermission` definition in the vault \u2014 managed-package / not-retrieved; declared but not resolvable, and NOT folded into systemPermissions); `recordTypeVisibilities[]` unions each container's declared record-type visibility (max-wins \u2014 visible=true wins; `<visible>` omitted in older metadata counts as visible, only an explicit false hides), each entry `{recordType, visible, grantedBy}` \u2014 record-type visibility is part of THIS union now, no longer only the separate `recordtype_availability` surface (that tool remains for the per-object grouped view); a container carrying no extracted `recordTypeVisibilities` property (a vault refreshed before record-type extraction) contributes nothing and is DISCLOSED (re-run /sfi-refresh), never fabricated as 'no record types'; `summary` reports objects / fieldsWithFls / apexClasses / systemPermissions / customPermissions / recordTypeVisibilities counts. The object list PAGES (`limit` default 100 / max 200, `offset`/`hasMore`/`truncated`). `declared` confidence. `disclosures` is explicit about the boundaries: permission-set GROUP membership IS expanded, but muting permission sets are DISCLOSED, not subtracted (effective access may be lower); app/tab visibility is a SEPARATE surface (now extracted \u2014 see `app_access` / `tab_availability`), not part of this union; field-level detail is summarised (use `field_access_audit`); object permission is NOT record access (record visibility needs OWD + sharing); custom permissions are declared grants, NOT system userPermissions, so they are never double-counted. Missing containers are ignored with a disclosure; if none exist \u2192 `component-not-found`. DIRECTION: this answers WHAT a given container bundle GRANTS (vault metadata); WHICH sets/PSGs a specific USER actually holds right now is runtime assignment state \u2014 use sfi.live_user_permsets (live, read-only) and feed its grantors back into this tool for a dual-provenance answer.",
|
|
70505
71994
|
inputSchema: EFFECTIVE_PERMISSIONS_INPUT_SCHEMA
|
|
70506
71995
|
},
|
|
70507
71996
|
{
|
|
@@ -70761,12 +72250,12 @@ var init_tools = __esm({
|
|
|
70761
72250
|
},
|
|
70762
72251
|
{
|
|
70763
72252
|
name: "sfi.unassigned_permission_sets",
|
|
70764
|
-
description: "v2.4 hygiene tool: list PermissionSets unassigned to users. The tool ships TWO output paths: (1) when v1.7 R2 Tooling-API enrichment has run, reads `properties.assignedUserCount` as the authoritative answer (PermissionSets with count 0 surface in `unassigned[]`); (2) when enrichment has not run, falls back to a structural check \u2014 PermissionSets with no outgoing `grantedBy` edges surface as `orphanedFromComponents[]`. The `unassignedCount` field counts confirmed unassigned; `unknownAssignmentCount` separately tallies PermissionSets where assignment cannot be determined. `enrichmentStatus` reports which path the answer came from (`tooling-api-fresh` / `tooling-api-stale` / `structural-only` / `no-assignment-data`). Honesty axis (v2.4 constitutional): NEVER counts unknownAssignmentCount toward unassignedCount \u2014 separates 'no data' from 'no assignments'. Emits a `coverageCaveat` when the PermissionSet family was not retrieved (distinct from the unknownAssignmentCount enrichment axis); empty results under it are 'not retrieved'. When the vault holds a captured permission-holder aggregate (`refresh --with-data-shape`), the response embeds a `dataShape` holders block (`data_snapshot`, COUNTS ONLY \u2014 no identities): a container absent from the org-wide aggregate had FACTUALLY zero active assignments at the capture stamp, upgrading the metadata inference.",
|
|
72253
|
+
description: "v2.4 hygiene tool: list PermissionSets unassigned to users. The tool ships TWO output paths: (1) when v1.7 R2 Tooling-API enrichment has run, reads `properties.assignedUserCount` as the authoritative answer (PermissionSets with count 0 surface in `unassigned[]`); (2) when enrichment has not run, falls back to a structural check \u2014 PermissionSets with no outgoing `grantedBy` edges surface as `orphanedFromComponents[]`. The `unassignedCount` field counts confirmed unassigned; `unknownAssignmentCount` separately tallies PermissionSets where assignment cannot be determined. `enrichmentStatus` reports which path the answer came from (`tooling-api-fresh` / `tooling-api-stale` / `structural-only` / `no-assignment-data`). Honesty axis (v2.4 constitutional): NEVER counts unknownAssignmentCount toward unassignedCount \u2014 separates 'no data' from 'no assignments'. Emits a `coverageCaveat` when the PermissionSet family was not retrieved (distinct from the unknownAssignmentCount enrichment axis); empty results under it are 'not retrieved'. When the vault holds a captured permission-holder aggregate (`refresh --with-data-shape`), the response embeds a `dataShape` holders block (`data_snapshot`, COUNTS ONLY \u2014 no identities): a container absent from the org-wide aggregate had FACTUALLY zero active assignments at the capture stamp, upgrading the metadata inference. For the current name-by-name holder list of one specific container, use sfi.live_permset_holders (opt-in, read-only).",
|
|
70765
72254
|
inputSchema: UNASSIGNED_PERMISSION_SETS_INPUT_SCHEMA
|
|
70766
72255
|
},
|
|
70767
72256
|
{
|
|
70768
72257
|
name: "sfi.empty_queues_and_groups",
|
|
70769
|
-
description: "v2.4 hygiene tool: list Queue and Group nodes with zero members. Walks `properties.memberCount` (the v1.1+ extractor convention) and falls back to `properties.queueMembers` / `properties.groupMembers` array length. The 'routing trap' case \u2014 a Queue with zero members but multiple incoming AssignmentRule references \u2014 surfaces with `incomingAssignmentRuleCount > 0`; admins must reassign routing before deletion. The `isLikelyStale` flag combines zero members + incoming refs + `lastModifiedAt > 180 days`. Member resolution that cannot decide ('unknown') is counted in `unknownMemberCountQueues` / `unknownMemberCountGroups`, NEVER toward emptiness. Honesty axis (verbatim): runtime membership changes via the Setup UI since the last vault refresh are not reflected. Emits a `coverageCaveat` (scoped to the type filter) when Queue/Group was not retrieved; empty lists under it are 'not checked', not 'none'.",
|
|
72258
|
+
description: "v2.4 hygiene tool: list Queue and Group nodes with zero members. Walks `properties.memberCount` (the v1.1+ extractor convention) and falls back to `properties.queueMembers` / `properties.groupMembers` array length. The 'routing trap' case \u2014 a Queue with zero members but multiple incoming AssignmentRule references \u2014 surfaces with `incomingAssignmentRuleCount > 0`; admins must reassign routing before deletion. The `isLikelyStale` flag combines zero members + incoming refs + `lastModifiedAt > 180 days`. Member resolution that cannot decide ('unknown') is counted in `unknownMemberCountQueues` / `unknownMemberCountGroups`, NEVER toward emptiness. Honesty axis (verbatim): runtime membership changes via the Setup UI since the last vault refresh are not reflected \u2014 for the CURRENT runtime roster (and a measured vault-vs-live drift check), use sfi.live_group_members (opt-in, read-only). Emits a `coverageCaveat` (scoped to the type filter) when Queue/Group was not retrieved; empty lists under it are 'not checked', not 'none'.",
|
|
70770
72259
|
inputSchema: EMPTY_QUEUES_AND_GROUPS_INPUT_SCHEMA
|
|
70771
72260
|
},
|
|
70772
72261
|
{
|
|
@@ -71250,6 +72739,14 @@ var init_tools = __esm({
|
|
|
71250
72739
|
return runTool(ctx, args, liveOrgLimitsInputSchema, liveOrgLimitsHandler);
|
|
71251
72740
|
case "sfi.live_inactive_users":
|
|
71252
72741
|
return runTool(ctx, args, liveInactiveUsersInputSchema, liveInactiveUsersHandler);
|
|
72742
|
+
case "sfi.live_permset_holders":
|
|
72743
|
+
return runTool(ctx, args, livePermsetHoldersInputSchema, livePermsetHoldersHandler);
|
|
72744
|
+
case "sfi.live_zombie_accounts":
|
|
72745
|
+
return runTool(ctx, args, liveZombieAccountsInputSchema, liveZombieAccountsHandler);
|
|
72746
|
+
case "sfi.live_group_members":
|
|
72747
|
+
return runTool(ctx, args, liveGroupMembersInputSchema, liveGroupMembersHandler);
|
|
72748
|
+
case "sfi.live_user_permsets":
|
|
72749
|
+
return runTool(ctx, args, liveUserPermsetsInputSchema, liveUserPermsetsHandler);
|
|
71253
72750
|
case "sfi.live_license_usage":
|
|
71254
72751
|
return runTool(ctx, args, liveLicenseUsageInputSchema, liveLicenseUsageHandler);
|
|
71255
72752
|
case "sfi.live_consent":
|
|
@@ -73190,7 +74687,7 @@ var init_package_version = __esm({
|
|
|
73190
74687
|
"use strict";
|
|
73191
74688
|
readCliPackageVersion = () => {
|
|
73192
74689
|
if (true)
|
|
73193
|
-
return "0.1.
|
|
74690
|
+
return "0.1.25";
|
|
73194
74691
|
for (const rel of ["../package.json", "../../package.json"]) {
|
|
73195
74692
|
try {
|
|
73196
74693
|
const raw = readFileSync3(fileURLToPath(new URL(rel, import.meta.url)), "utf8");
|
|
@@ -143086,13 +144583,13 @@ var registerMcpCommand = (program) => {
|
|
|
143086
144583
|
process.env["SFI_PLUGIN_VERSION"] = readCliPackageVersion();
|
|
143087
144584
|
process.stderr.write(`sfi mcp: serving vault ${vaultRoot}${targetOrg !== null ? ` (org: ${targetOrg})` : " (no targetOrg in config)"}
|
|
143088
144585
|
`);
|
|
143089
|
-
|
|
143090
|
-
const notice = formatUpdateNotice(
|
|
144586
|
+
void checkForUpdate(readCliPackageVersion()).then((result) => {
|
|
144587
|
+
const notice = formatUpdateNotice(result);
|
|
143091
144588
|
if (notice !== null)
|
|
143092
144589
|
process.stderr.write(`sfi mcp: ${notice}
|
|
143093
144590
|
`);
|
|
143094
|
-
}
|
|
143095
|
-
}
|
|
144591
|
+
}, () => {
|
|
144592
|
+
});
|
|
143096
144593
|
const shutdownOnce = makeShutdownOnce(ctx);
|
|
143097
144594
|
for (const signal of SHUTDOWN_SIGNALS) {
|
|
143098
144595
|
process.on(signal, () => {
|
|
@@ -143114,7 +144611,7 @@ var makeShutdownOnce = (ctx) => {
|
|
|
143114
144611
|
|
|
143115
144612
|
// dist/src/commands/demo.js
|
|
143116
144613
|
init_refresh();
|
|
143117
|
-
var buildVersion = () => true ? "0.1.
|
|
144614
|
+
var buildVersion = () => true ? "0.1.25" : "dev";
|
|
143118
144615
|
var SHUTDOWN_SIGNALS2 = ["SIGINT", "SIGTERM"];
|
|
143119
144616
|
var resolveDemoSource = () => {
|
|
143120
144617
|
let dir = dirname21(fileURLToPath2(import.meta.url));
|
|
@@ -144099,7 +145596,7 @@ init_vault_git();
|
|
|
144099
145596
|
init_watch();
|
|
144100
145597
|
var readVersion = () => {
|
|
144101
145598
|
if (true)
|
|
144102
|
-
return "0.1.
|
|
145599
|
+
return "0.1.25";
|
|
144103
145600
|
const pkgUrl = new URL("../../package.json", import.meta.url);
|
|
144104
145601
|
const raw = readFileSync6(fileURLToPath3(pkgUrl), "utf8");
|
|
144105
145602
|
const parsed = JSON.parse(raw);
|